1414import json
1515import logging
1616import logging .handlers
17+ import math
1718import os
1819import signal
1920import sys
@@ -928,6 +929,18 @@ def run_trading_cycle(
928929 )
929930 sl_tp_qty = actual_qty
930931
932+ # Alpaca requires whole-share quantities for GTC stop/limit orders.
933+ # Round down to nearest integer so SL/TP brackets can use "gtc"
934+ # (protecting the position overnight/over the weekend). The
935+ # fractional remainder is left unprotected — negligible risk.
936+ sl_tp_qty_whole = math .floor (sl_tp_qty )
937+ if sl_tp_qty_whole < 1 :
938+ logger .warning (
939+ f" Position qty { sl_tp_qty :.4f} < 1 whole share for "
940+ f"{ entry ['symbol' ]} — skipping GTC SL/TP (too small)"
941+ )
942+ continue
943+
931944 # Try ATR-based stops first, fall back to fixed %
932945 # Use pre-fetched OHLCV from ML pipeline to avoid 10 extra
933946 # yfinance calls per rebalance cycle.
@@ -971,7 +984,7 @@ def run_trading_cycle(
971984 oco_order = BrokerOrder (
972985 symbol = entry ["symbol" ],
973986 side = "sell" ,
974- qty = sl_tp_qty ,
987+ qty = sl_tp_qty_whole ,
975988 order_type = "limit" ,
976989 time_in_force = "gtc" ,
977990 order_class = "oco" ,
@@ -980,9 +993,9 @@ def run_trading_cycle(
980993 )
981994 oco_id = broker .submit_order (oco_order )
982995 logger .info (
983- f" OCO SL/TP attached: SELL { sl_tp_qty :.4f } { entry ['symbol' ]} "
996+ f" OCO SL/TP attached: SELL { sl_tp_qty_whole } { entry ['symbol' ]} "
984997 f"SL@${ sl_price } ({ sl_label } ) TP@${ tp_price } "
985- f"(from fill ${ fill_price :.2f} ) -> { oco_id } "
998+ f"(from fill ${ fill_price :.2f} , whole-share GTC ) -> { oco_id } "
986999 )
9871000 except Exception as e :
9881001 logger .error (f" Failed to attach SL/TP for { entry ['symbol' ]} : { e } " )
@@ -991,7 +1004,7 @@ def run_trading_cycle(
9911004 sl_order = BrokerOrder (
9921005 symbol = entry ["symbol" ],
9931006 side = "sell" ,
994- qty = sl_tp_qty ,
1007+ qty = sl_tp_qty_whole ,
9951008 order_type = "stop" ,
9961009 stop_price = sl_price ,
9971010 time_in_force = "gtc" ,
@@ -1005,21 +1018,32 @@ def run_trading_cycle(
10051018 f" No fill price available for { entry ['symbol' ]} — "
10061019 f"SL/TP not attached (using quoted price as fallback)"
10071020 )
1008- # Fallback: use the quoted price from the prices dict
1021+ # Fallback: use the quoted price from the prices dict.
1022+ # Use whole-share qty for GTC compatibility (Alpaca rejects
1023+ # fractional GTC stop orders).
10091024 if entry ["symbol" ] in prices :
10101025 try :
10111026 quoted = prices [entry ["symbol" ]]
10121027 sl_price = round (quoted * (1 - STOP_LOSS_PCT ), 2 )
1013- sl_order = BrokerOrder (
1014- symbol = entry ["symbol" ],
1015- side = "sell" ,
1016- qty = actual_qty ,
1017- order_type = "stop" ,
1018- stop_price = sl_price ,
1019- time_in_force = "gtc" ,
1020- )
1021- broker .submit_order (sl_order )
1022- logger .info (f" SL fallback @ ${ sl_price } (quoted price)" )
1028+ sl_qty_whole = math .floor (actual_qty )
1029+ if sl_qty_whole < 1 :
1030+ logger .warning (
1031+ f" Qty { actual_qty :.4f} < 1 whole share for "
1032+ f"{ entry ['symbol' ]} — skipping SL fallback"
1033+ )
1034+ else :
1035+ sl_order = BrokerOrder (
1036+ symbol = entry ["symbol" ],
1037+ side = "sell" ,
1038+ qty = sl_qty_whole ,
1039+ order_type = "stop" ,
1040+ stop_price = sl_price ,
1041+ time_in_force = "gtc" ,
1042+ )
1043+ broker .submit_order (sl_order )
1044+ logger .info (
1045+ f" SL fallback @ ${ sl_price } (quoted price, whole-share GTC)"
1046+ )
10231047 except Exception as e :
10241048 logger .error (f" SL fallback also failed for { entry ['symbol' ]} : { e } " )
10251049
0 commit comments