Skip to content

Commit be142f0

Browse files
committed
fix(live_bot): use whole-share qty for GTC OCO/SL-TP orders
Alpaca rejects fractional-share orders with time_in_force='gtc'. Round down sl_tp_qty to nearest integer before submitting OCO brackets and stop-loss fallbacks so they can use 'gtc' and survive overnight and over the weekend. The fractional remainder (~<$1) is left unprotected — negligible risk for a weekly strategy. Fixes the P0 bug where every SL/TP order on both bots failed with 'fractional orders must be DAY orders', leaving all positions unprotected.
1 parent ee24b81 commit be142f0

1 file changed

Lines changed: 39 additions & 15 deletions

File tree

examples/live_bot.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import json
1515
import logging
1616
import logging.handlers
17+
import math
1718
import os
1819
import signal
1920
import 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

Comments
 (0)