Circuit breaker: auto-flatten on breach
When the daily-loss breaker trips it now also flattens the book: cancels unfilled (armed) entries and market-closes every open position via client.close_position (which cancels resting bracket legs). Configurable via TRADING_FLATTEN_ON_BREACH (default on). Best-effort per symbol; emits a 'halted_and_flattened' risk_rule_triggered event with the flatten summary. Verified: trip at -4% closes 2 seeded positions + cancels 1 armed entry. 67/67 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
37c6131b8e
commit
6c339121f0
2 files changed
+56
-5
No files matched your search
+3
-3
@@ -58,8 +58,9 @@ TRADING_MAX_CONCURRENT_POSITIONS=3
|
||||
- **Manual approval instead of auto:** `TRADING_AUTO_APPROVE=0`, drop
|
||||
`--no-dashboard`, approve each signal at `http://127.0.0.1:8765`.
|
||||
- **Observe only (no orders):** `TRADING_EXECUTION_ENABLED=0`.
|
||||
- The breaker halts **new** entries at -3%; it does **not** auto-flatten open
|
||||
positions — exit those from the dashboard.
|
||||
- The breaker at -3% halts **new** entries **and flattens the book** — cancels
|
||||
unfilled entries + market-closes open positions. Set
|
||||
`TRADING_FLATTEN_ON_BREACH=0` for halt-only (leave positions open).
|
||||
|
||||
## 5. Tuning (env vars)
|
||||
| var | default | meaning |
|
||||
@@ -84,7 +85,6 @@ PY
|
||||
## Known limitations (honest)
|
||||
- `quality_score` is a constant 1.0, `float_rotation` is 0.0, `spread_pct` is
|
||||
null — these fields are not real metrics yet.
|
||||
- The circuit breaker halts new entries but does not auto-flatten open positions.
|
||||
- News-catalyst discovery (RSS) exists but isn't wired into the watchlist.
|
||||
- Free Alpaca **IEX** data is thinner than SIP, especially premarket — some
|
||||
sub-$20 names show few/no bars until volume builds.
|
||||
|
||||
+53
-2
@@ -55,6 +55,8 @@ class ExecutionSettings:
|
||||
risk_per_trade_pct: float = 0.01
|
||||
default_equity: float = 100_000.0
|
||||
max_daily_loss_pct: float = 0.03
|
||||
# on a daily-loss breach, also flatten open positions + cancel unfilled entries
|
||||
flatten_on_breach: bool = True
|
||||
# --- entry mechanism (Ross-Cameron-style; all tunable) -----------------
|
||||
# reward target as a multiple of risk (entry + reward_multiple * (entry-stop))
|
||||
reward_multiple: float = 2.0
|
||||
@@ -93,6 +95,7 @@ class ExecutionSettings:
|
||||
risk_per_trade_pct=float(values.get("TRADING_RISK_PER_TRADE_PCT", "0.01")),
|
||||
default_equity=float(values.get("TRADING_DEFAULT_EQUITY", "100000")),
|
||||
max_daily_loss_pct=float(values.get("TRADING_MAX_DAILY_LOSS_PCT", "0.03")),
|
||||
flatten_on_breach=flag("TRADING_FLATTEN_ON_BREACH", "1"),
|
||||
reward_multiple=float(values.get("TRADING_REWARD_MULTIPLE", "2.0")),
|
||||
entry_order_type=order_type,
|
||||
entry_timeout_bars=int(values.get("TRADING_ENTRY_TIMEOUT_BARS", "2")),
|
||||
@@ -162,6 +165,38 @@ class TradingExecutionService:
|
||||
if p.get("symbol")
|
||||
}
|
||||
|
||||
def _flatten_all(self, reason: str) -> dict:
|
||||
"""Cancel unfilled entries and market-close every open position.
|
||||
|
||||
Used when the daily-loss circuit breaker trips: stop the bleeding by
|
||||
cancelling resting entries and flattening the book. Best-effort — errors
|
||||
are collected, not raised, so one bad symbol can't block the rest.
|
||||
``close_position`` also cancels the position's resting bracket legs.
|
||||
"""
|
||||
result: dict = {"cancelled_entries": 0, "closed_positions": [], "errors": []}
|
||||
# 1) cancel unfilled (armed) entries we are tracking
|
||||
for order_id in list(self._armed):
|
||||
armed = self._armed.pop(order_id)
|
||||
symbol = armed.get("symbol", "")
|
||||
try:
|
||||
self.executor.cancel_entry(
|
||||
order_id, armed.get("broker_order_id"), symbol, reason
|
||||
)
|
||||
result["cancelled_entries"] += 1
|
||||
except Exception as exc: # noqa: BLE001
|
||||
result["errors"].append(f"cancel {symbol}: {exc}")
|
||||
self._requested_symbols.discard(symbol)
|
||||
# 2) market-close open positions
|
||||
client = getattr(self.executor, "client", None)
|
||||
for symbol in sorted(self._held_symbols()):
|
||||
try:
|
||||
if client is not None and hasattr(client, "close_position"):
|
||||
client.close_position(symbol)
|
||||
result["closed_positions"].append(symbol)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
result["errors"].append(f"close {symbol}: {exc}")
|
||||
return result
|
||||
|
||||
def _daily_loss_breach(self) -> bool:
|
||||
"""True once the day's loss hits the circuit-breaker threshold.
|
||||
|
||||
@@ -189,6 +224,19 @@ class TradingExecutionService:
|
||||
pnl_pct = (equity - baseline) / baseline
|
||||
if pnl_pct <= -max_loss:
|
||||
self._halted = True
|
||||
flat = (
|
||||
self._flatten_all("daily_loss_circuit_breaker")
|
||||
if self.settings.flatten_on_breach
|
||||
else {"closed_positions": [], "cancelled_entries": 0}
|
||||
)
|
||||
extra = ""
|
||||
if self.settings.flatten_on_breach:
|
||||
n_closed = len(flat["closed_positions"])
|
||||
n_cancel = flat["cancelled_entries"]
|
||||
extra = (
|
||||
f" — flattened {n_closed} position(s), "
|
||||
f"cancelled {n_cancel} unfilled entr{'y' if n_cancel == 1 else 'ies'}"
|
||||
)
|
||||
self.store.emit(
|
||||
RiskRuleTriggeredEvent(
|
||||
timestamp=datetime.now(),
|
||||
@@ -196,7 +244,7 @@ class TradingExecutionService:
|
||||
correlation_id=self.session_id,
|
||||
message=(
|
||||
f"Daily-loss circuit breaker tripped: {pnl_pct:+.2%} "
|
||||
f"(limit -{max_loss:.0%}) — halting new entries"
|
||||
f"(limit -{max_loss:.0%}) — halting new entries{extra}"
|
||||
),
|
||||
rule_type="daily_loss",
|
||||
rule_value=max_loss,
|
||||
@@ -204,8 +252,11 @@ class TradingExecutionService:
|
||||
"equity": equity,
|
||||
"baseline_equity": baseline,
|
||||
"pnl_pct": round(pnl_pct, 4),
|
||||
"flatten": flat,
|
||||
},
|
||||
action_taken="halted_new_entries",
|
||||
action_taken="halted_and_flattened"
|
||||
if self.settings.flatten_on_breach
|
||||
else "halted_new_entries",
|
||||
)
|
||||
)
|
||||
return self._halted
|
||||
|
||||
Reference in new issue
Block a user