Add cash account support with T+1 settlement tracking and disable short selling by default

This commit is contained in:
Justin Oros
2026-02-04 15:47:43 -07:00
parent 1ce7da39fc
commit b29b74587a
3 changed files with 133 additions and 20 deletions
-1
View File
@@ -2,4 +2,3 @@ from .engine import run
if __name__ == "__main__": if __name__ == "__main__":
run() run()
+10 -7
View File
@@ -2,13 +2,13 @@
"DEBUG_MODE": true, "DEBUG_MODE": true,
"SYMBOL": "SPY", "SYMBOL": "SPY",
"BAR_TIMEFRAME": "5Min", "BAR_TIMEFRAME": "5Min",
"RISK_PER_TRADE": 0.01, "RISK_PER_TRADE": 0.005,
"SHORT_WINDOW": 10, "SHORT_WINDOW": 10,
"LONG_WINDOW": 30, "LONG_WINDOW": 30,
"MIN_NOTIONAL": 1.0, "MIN_NOTIONAL": 1.0,
"POLL_INTERVAL": 60, "POLL_INTERVAL": 60,
"MAX_DRAWDOWN": 0.08, "MAX_DRAWDOWN": 0.05,
"PDT_RULE": true, "PDT_RULE": false,
"USE_TRAILING_STOP": true, "USE_TRAILING_STOP": true,
"PROFIT_TARGET_1": 2.0, "PROFIT_TARGET_1": 2.0,
"PROFIT_TARGET_2": 4.0, "PROFIT_TARGET_2": 4.0,
@@ -23,7 +23,7 @@
"LIMIT_ORDER_TIMEOUT": 60, "LIMIT_ORDER_TIMEOUT": 60,
"ADX_THRESHOLD": 25, "ADX_THRESHOLD": 25,
"VOLUME_MULTIPLIER": 0.5, "VOLUME_MULTIPLIER": 0.5,
"ATR_STOP_MULTIPLIER": 2.0, "ATR_STOP_MULTIPLIER": 2.5,
"MAX_HOLD_TIME": 3600, "MAX_HOLD_TIME": 3600,
"REGIME_DETECTION": true, "REGIME_DETECTION": true,
"MULTIFRAME_FILTER": false, "MULTIFRAME_FILTER": false,
@@ -35,18 +35,21 @@
"VIX_THRESHOLD": 30, "VIX_THRESHOLD": 30,
"USE_VIX_FILTER": false, "USE_VIX_FILTER": false,
"USE_FIBONACCI": false, "USE_FIBONACCI": false,
"MAX_TRADES_PER_DAY": 5, "MAX_TRADES_PER_DAY": 3,
"SKIP_MONDAYS_FRIDAYS": false, "SKIP_MONDAYS_FRIDAYS": false,
"USE_200_SMA_FILTER": false, "USE_200_SMA_FILTER": false,
"REQUIRE_MACD_CONFIRMATION": false, "REQUIRE_MACD_CONFIRMATION": false,
"MIN_RISK_REWARD": 2.0, "MIN_RISK_REWARD": 2.0,
"PULLBACK_PERCENTAGE": 0.382, "PULLBACK_PERCENTAGE": 0.382,
"ENABLE_SHORT_SELLING": true, "ENABLE_SHORT_SELLING": false,
"RSI_BUY_MAX": 55, "RSI_BUY_MAX": 55,
"RSI_SELL_MIN": 45, "RSI_SELL_MIN": 45,
"RSI_SELL_MAX": 70, "RSI_SELL_MAX": 70,
"RSI_RANGE_OVERSOLD": 30, "RSI_RANGE_OVERSOLD": 30,
"RSI_RANGE_OVERBOUGHT": 70, "RSI_RANGE_OVERBOUGHT": 70,
"REQUIRE_MA_CROSSOVER": true, "REQUIRE_MA_CROSSOVER": true,
"CROSSOVER_LOOKBACK": 5 "CROSSOVER_LOOKBACK": 5,
"REQUIRE_CASH_ACCOUNT": true,
"T1_SETTLEMENT_ENABLED": true,
"CASH_RESERVE_PCT": 0.1
} }
+123 -12
View File
@@ -51,7 +51,7 @@ DEFAULT_CONFIG = {
"MIN_NOTIONAL": 1.0, "MIN_NOTIONAL": 1.0,
"POLL_INTERVAL": 60, "POLL_INTERVAL": 60,
"MAX_DRAWDOWN": 0.08, "MAX_DRAWDOWN": 0.08,
"PDT_RULE": True, "PDT_RULE": False,
"USE_TRAILING_STOP": True, "USE_TRAILING_STOP": True,
"PROFIT_TARGET_1": 2.0, "PROFIT_TARGET_1": 2.0,
"PROFIT_TARGET_2": 4.0, "PROFIT_TARGET_2": 4.0,
@@ -84,14 +84,17 @@ DEFAULT_CONFIG = {
"REQUIRE_MACD_CONFIRMATION": False, "REQUIRE_MACD_CONFIRMATION": False,
"MIN_RISK_REWARD": 2.0, "MIN_RISK_REWARD": 2.0,
"PULLBACK_PERCENTAGE": 0.382, "PULLBACK_PERCENTAGE": 0.382,
"ENABLE_SHORT_SELLING": True, "ENABLE_SHORT_SELLING": False,
"RSI_BUY_MAX": 55, "RSI_BUY_MAX": 55,
"RSI_SELL_MIN": 45, "RSI_SELL_MIN": 45,
"RSI_SELL_MAX": 70, "RSI_SELL_MAX": 70,
"RSI_RANGE_OVERSOLD": 30, "RSI_RANGE_OVERSOLD": 30,
"RSI_RANGE_OVERBOUGHT": 70, "RSI_RANGE_OVERBOUGHT": 70,
"REQUIRE_MA_CROSSOVER": True, "REQUIRE_MA_CROSSOVER": True,
"CROSSOVER_LOOKBACK": 5 "CROSSOVER_LOOKBACK": 5,
"REQUIRE_CASH_ACCOUNT": True,
"T1_SETTLEMENT_ENABLED": True,
"CASH_RESERVE_PCT": 0.1
} }
if not ENV_PATH.exists(): if not ENV_PATH.exists():
@@ -147,8 +150,44 @@ try:
os.getenv("APCA_API_BASE_URL", "https://paper-api.alpaca.markets"), os.getenv("APCA_API_BASE_URL", "https://paper-api.alpaca.markets"),
api_version="v2" api_version="v2"
) )
test_client.get_account() account = test_client.get_account()
logger.info("✅ API credentials validated") logger.info("✅ API credentials validated")
if REQUIRE_CASH_ACCOUNT:
account_type = getattr(account, 'account_blocked', False)
is_pattern_day_trader = getattr(account, 'pattern_day_trader', False)
daytrade_count = getattr(account, 'daytrade_count', 0)
account_status = getattr(account, 'status', 'UNKNOWN')
if account_status != 'ACTIVE':
logger.error(f"⚠️ Account status is {account_status}, must be ACTIVE")
sys.exit(1)
buying_power = float(getattr(account, 'buying_power', 0))
cash = float(getattr(account, 'cash', 0))
logger.info(f"💵 Account Type Check:")
logger.info(f" Cash: ${cash:.2f}")
logger.info(f" Buying Power: ${buying_power:.2f}")
logger.info(f" PDT Status: {is_pattern_day_trader}")
logger.info(f" Daytrade Count: {daytrade_count}")
if buying_power > cash * 1.5:
logger.warning("⚠️ WARNING: Buying power significantly exceeds cash")
logger.warning(" This may indicate a MARGIN account, not a CASH account")
logger.warning(" Please verify your account type in Alpaca dashboard")
logger.warning(" For cash accounts under $25k, you should NOT have margin enabled")
if REQUIRE_CASH_ACCOUNT:
logger.error("⚠️ REQUIRE_CASH_ACCOUNT is True but account appears to be margin")
logger.error(" Set REQUIRE_CASH_ACCOUNT to False in config.json to bypass this check")
sys.exit(1)
logger.info("✅ Cash account verified")
if T1_SETTLEMENT_ENABLED:
logger.info("✅ T+1 settlement tracking enabled")
logger.info(f" Keeping {CASH_RESERVE_PCT*100:.0f}% cash reserve for safety")
except Exception as e: except Exception as e:
logger.error(f"⚠️ Invalid API credentials: {e}") logger.error(f"⚠️ Invalid API credentials: {e}")
logger.error(" Please check your .env file and ensure your Alpaca API keys are correct") logger.error(" Please check your .env file and ensure your Alpaca API keys are correct")
@@ -198,6 +237,9 @@ RSI_RANGE_OVERSOLD = float(config.get("RSI_RANGE_OVERSOLD", 30))
RSI_RANGE_OVERBOUGHT = float(config.get("RSI_RANGE_OVERBOUGHT", 70)) RSI_RANGE_OVERBOUGHT = float(config.get("RSI_RANGE_OVERBOUGHT", 70))
REQUIRE_MA_CROSSOVER = bool(config.get("REQUIRE_MA_CROSSOVER", True)) REQUIRE_MA_CROSSOVER = bool(config.get("REQUIRE_MA_CROSSOVER", True))
CROSSOVER_LOOKBACK = int(config.get("CROSSOVER_LOOKBACK", 5)) CROSSOVER_LOOKBACK = int(config.get("CROSSOVER_LOOKBACK", 5))
REQUIRE_CASH_ACCOUNT = bool(config.get("REQUIRE_CASH_ACCOUNT", True))
T1_SETTLEMENT_ENABLED = bool(config.get("T1_SETTLEMENT_ENABLED", True))
CASH_RESERVE_PCT = float(config.get("CASH_RESERVE_PCT", 0.1))
api = AlpacaClient( api = AlpacaClient(
os.getenv('APCA_API_KEY_ID'), os.getenv('APCA_API_KEY_ID'),
@@ -206,6 +248,50 @@ api = AlpacaClient(
api_version='v2' api_version='v2'
) )
class SettlementTracker:
def __init__(self):
self.pending_settlements = {}
def add_trade(self, trade_date, amount):
settlement_date = self._get_next_trading_day(trade_date)
if settlement_date not in self.pending_settlements:
self.pending_settlements[settlement_date] = 0.0
self.pending_settlements[settlement_date] += amount
logger.info(f"💰 T+1: ${amount:.2f} settling on {settlement_date.strftime('%Y-%m-%d')}")
debug_print(f"Added ${amount:.2f} to settle on {settlement_date}")
def _get_next_trading_day(self, date):
next_day = date + timedelta(days=1)
while next_day.weekday() >= 5:
next_day += timedelta(days=1)
return next_day.date()
def settle_funds(self, current_date):
settled_amount = 0.0
current_date_only = current_date.date()
dates_to_remove = []
for settlement_date, amount in self.pending_settlements.items():
if settlement_date <= current_date_only:
settled_amount += amount
dates_to_remove.append(settlement_date)
for date in dates_to_remove:
del self.pending_settlements[date]
if settled_amount > 0:
logger.info(f"✅ Settled ${settled_amount:.2f} on {current_date_only}")
debug_print(f"Settled ${settled_amount:.2f}")
return settled_amount
def get_pending_amount(self):
return sum(self.pending_settlements.values())
def reset(self):
self.pending_settlements = {}
class SignalState: class SignalState:
def __init__(self): def __init__(self):
self.last_bullish_crossover_bar = -999 self.last_bullish_crossover_bar = -999
@@ -239,10 +325,23 @@ def fetch_equity():
debug_print(f"Current equity: ${equity:.2f}") debug_print(f"Current equity: ${equity:.2f}")
return equity return equity
def fetch_buying_power(): def fetch_buying_power(settlement_tracker=None):
debug_print("Fetching buying power") debug_print("Fetching buying power")
account = api.get_account() account = api.get_account()
bp = float(account.buying_power) bp = float(account.buying_power)
cash = float(account.cash)
if T1_SETTLEMENT_ENABLED and settlement_tracker:
pending = settlement_tracker.get_pending_amount()
available_cash = cash - pending
if CASH_RESERVE_PCT > 0:
reserve = cash * CASH_RESERVE_PCT
available_cash = max(0, available_cash - reserve)
debug_print(f"Cash: ${cash:.2f}, Pending: ${pending:.2f}, Available: ${available_cash:.2f}")
return available_cash
debug_print(f"Buying power: ${bp:.2f}") debug_print(f"Buying power: ${bp:.2f}")
return bp return bp
@@ -713,6 +812,12 @@ def main():
opening_equity = fetch_equity() opening_equity = fetch_equity()
logger.info(f"💵 Starting equity: ${opening_equity:.2f}") logger.info(f"💵 Starting equity: ${opening_equity:.2f}")
settlement_tracker = SettlementTracker()
if T1_SETTLEMENT_ENABLED:
current_date = datetime.now(EASTERN)
settlement_tracker.settle_funds(current_date)
position_active = False position_active = False
entry_price = 0 entry_price = 0
entry_time = None entry_time = None
@@ -862,7 +967,7 @@ def main():
if signal in ['buy', 'sell'] and not position_active: if signal in ['buy', 'sell'] and not position_active:
debug_print(f"Signal detected: {signal}, executing trade...") debug_print(f"Signal detected: {signal}, executing trade...")
buying_power = fetch_buying_power() buying_power = fetch_buying_power(settlement_tracker)
position_size = calculate_position_size(current_equity, signal_stop_loss, current_price) position_size = calculate_position_size(current_equity, signal_stop_loss, current_price)
if buying_power >= position_size: if buying_power >= position_size:
@@ -876,12 +981,9 @@ def main():
else: else:
execution_price = submit_market_buy(SYMBOL, position_size) execution_price = submit_market_buy(SYMBOL, position_size)
elif signal == 'sell': elif signal == 'sell':
if USE_LIMIT_ORDERS: logger.warning("⚠️ Sell signal ignored - short selling not allowed with cash account")
bid, ask = get_bid_ask(SYMBOL) debug_print("Short selling blocked for cash account")
limit_price = ask signal = None
execution_price = submit_limit_short_sell(SYMBOL, position_size, limit_price)
else:
execution_price = submit_short_sell(SYMBOL, position_size)
if execution_price: if execution_price:
trade_count += 1 trade_count += 1
@@ -892,6 +994,10 @@ def main():
position_active = True position_active = True
position_type = 'long' if signal == 'buy' else 'short' position_type = 'long' if signal == 'buy' else 'short'
if T1_SETTLEMENT_ENABLED and signal == 'buy':
trade_amount = position_size
settlement_tracker.add_trade(datetime.now(EASTERN), trade_amount)
if entry_price > 0: if entry_price > 0:
risk_amount = abs(entry_price - stop_loss) / entry_price risk_amount = abs(entry_price - stop_loss) / entry_price
else: else:
@@ -906,6 +1012,11 @@ def main():
else: else:
logger.warning(f"⚠️ Insufficient buying power: ${buying_power:.2f} < ${position_size:.2f}") logger.warning(f"⚠️ Insufficient buying power: ${buying_power:.2f} < ${position_size:.2f}")
debug_print(f"Insufficient buying power: ${buying_power:.2f} < ${position_size:.2f}") debug_print(f"Insufficient buying power: ${buying_power:.2f} < ${position_size:.2f}")
if T1_SETTLEMENT_ENABLED:
pending = settlement_tracker.get_pending_amount()
logger.info(f" Pending settlement: ${pending:.2f}")
debug_print(f"Funds tied up in T+1 settlement: ${pending:.2f}")
position_status = f"{position_type.upper()}" if position_active else "FLAT" position_status = f"{position_type.upper()}" if position_active else "FLAT"