fix: label broker sync events by actual execution broker (tradier stack)

Tradier stack emitted account/positions/orders events hardcoded as
broker_name=alpaca_paper while the executor filters snapshots by
client.broker_name (tradier) — position cap and re-entry block read zero
positions, and the dashboard showed 'Alpaca Paper' over real Tradier values.
Mode label now defaults from BROKER/TRADIER_ENV (tradier_live) instead of
alpaca_paper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Fable 5 committed 2026-08-04 10:52:01 -05:00
1 parent 485cec0372
commit 3871bda616
4 files changed
+41 -14

No files matched your search

+16 -9
View File
@@ -45,6 +45,13 @@ class AlpacaPaperSync:
self.client = client or AlpacaPaperClient()
self.session_id = session_id
self.mode = mode
# Label events by the ACTUAL execution broker (BROKER=tradier swaps the
# client): the executor filters position snapshots by client.broker_name,
# so a hardcoded "alpaca_paper" here made those queries return empty on
# the Tradier stack (position cap / re-entry block saw zero positions).
self.broker_name = getattr(self.client, "broker_name", BROKER_NAME)
self.account_desc = (
"Tradier" if self.broker_name == "tradier" else "Alpaca Paper")
# last seen open positions, to detect closes by diffing snapshots. None
# until the first sync, when it's seeded from the last PERSISTED snapshot
# so a close that happened while the loop was down is still journaled.
@@ -57,8 +64,8 @@ class AlpacaPaperSync:
timestamp=datetime.now(),
mode=self.mode,
correlation_id=self.session_id,
message=f"alpaca_paper sync degraded: {reason}",
broker_name=BROKER_NAME,
message=f"{self.broker_name} sync degraded: {reason}",
broker_name=self.broker_name,
previous_health="healthy",
new_health="degraded",
health_reason=reason,
@@ -78,10 +85,10 @@ class AlpacaPaperSync:
timestamp=datetime.now(),
mode=self.mode,
correlation_id=self.session_id,
message=f"Alpaca paper equity {account.get('equity')}",
broker_name=BROKER_NAME,
message=f"{self.account_desc} equity {account.get('equity')}",
broker_name=self.broker_name,
account_id=account_id,
account_desc="Alpaca Paper",
account_desc=self.account_desc,
total_equity=float(account.get("equity") or 0),
cash_balance=float(account.get("cash") or 0),
buying_power=float(account.get("buying_power") or 0),
@@ -116,8 +123,8 @@ class AlpacaPaperSync:
timestamp=datetime.now(),
mode=self.mode,
correlation_id=self.session_id,
message=f"Alpaca paper positions: {len(positions)}",
broker_name=BROKER_NAME,
message=f"{self.account_desc} positions: {len(positions)}",
broker_name=self.broker_name,
account_id="paper",
positions=positions,
)
@@ -166,8 +173,8 @@ class AlpacaPaperSync:
timestamp=datetime.now(),
mode=self.mode,
correlation_id=self.session_id,
message=f"Alpaca paper orders: {len(orders)}",
broker_name=BROKER_NAME,
message=f"{self.account_desc} orders: {len(orders)}",
broker_name=self.broker_name,
account_id="paper",
orders=orders,
)
+2 -1
View File
@@ -32,7 +32,8 @@ def main() -> None:
# WATCHER_EVENT_DB_PATH is a legacy fallback only (the path is ignored).
db_path = os.environ.get("DATABASE_URL") or os.environ.get(
"WATCHER_EVENT_DB_PATH", "momentum")
mode = os.environ.get("TRADING_EXECUTION_MODE", "alpaca_paper")
from trading_mode import TradingModeSettings
mode = TradingModeSettings.from_env().execution_mode
# Sparklines: the standalone dashboard needs the SAME research-DB reader the
# embedded (orchestrator) mode passes — without it DashboardState.bars() short-
# circuits to []. The missing wire was purely that this entrypoint never built
@@ -123,6 +123,18 @@ def test_execution_request_payload_round_trip():
assert clone.stop_loss_price == 9.5
def test_sync_labels_events_by_client_broker(store):
client = FakeClient()
client.broker_name = "tradier"
sync = AlpacaPaperSync(store, client=client, session_id="t")
sync.sync_all()
accounts = query_account_summary_snapshot(store, broker_name="tradier")
assert accounts and accounts[0]["account_desc"] == "Tradier"
# executor filters by client.broker_name — events must match it
assert query_account_positions_snapshot(store, broker_name="tradier")
assert query_account_summary_snapshot(store, broker_name="alpaca_paper") == []
def test_sync_all_populates_snapshots(store):
sync = AlpacaPaperSync(store, client=FakeClient(), session_id="t")
sync.sync_all()
+11 -4
View File
@@ -5,7 +5,7 @@ from dataclasses import dataclass
from typing import Literal, cast
TradingMode = Literal["schwab_live", "alpaca_paper"]
TradingMode = Literal["schwab_live", "alpaca_paper", "tradier_live", "tradier_sandbox"]
@dataclass(frozen=True)
@@ -17,7 +17,14 @@ class TradingModeSettings:
values = dict(os.environ)
if env is not None:
values.update(env)
mode = values.get("TRADING_EXECUTION_MODE", "alpaca_paper")
if mode not in {"schwab_live", "alpaca_paper"}:
mode = "alpaca_paper"
# BROKER=tradier swaps the execution client, so the default mode label
# must follow it — else the Tradier stack reports "alpaca_paper".
if values.get("BROKER", "alpaca").strip().lower() == "tradier":
sandbox = values.get("TRADIER_ENV", "sandbox").strip().lower() != "live"
default = "tradier_sandbox" if sandbox else "tradier_live"
else:
default = "alpaca_paper"
mode = values.get("TRADING_EXECUTION_MODE", default)
if mode not in {"schwab_live", "alpaca_paper", "tradier_live", "tradier_sandbox"}:
mode = default
return cls(execution_mode=cast(TradingMode, mode))