From 3871bda6161e8f1999b694c0295d56f2ab859e75 Mon Sep 17 00:00:00 2001 From: momentum dev Date: Tue, 4 Aug 2026 10:52:01 -0500 Subject: [PATCH] fix: label broker sync events by actual execution broker (tradier stack) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- alpaca_paper/sync.py | 25 ++++++++++++------- dashboard_api.py | 3 ++- .../alpaca_paper/test_execution_and_sync.py | 12 +++++++++ trading_mode.py | 15 ++++++++--- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/alpaca_paper/sync.py b/alpaca_paper/sync.py index 2614b05..b00b4e2 100644 --- a/alpaca_paper/sync.py +++ b/alpaca_paper/sync.py @@ -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, ) diff --git a/dashboard_api.py b/dashboard_api.py index 9a265cc..71e0bae 100644 --- a/dashboard_api.py +++ b/dashboard_api.py @@ -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 diff --git a/tests/unit/alpaca_paper/test_execution_and_sync.py b/tests/unit/alpaca_paper/test_execution_and_sync.py index 097b8c9..281f5cc 100644 --- a/tests/unit/alpaca_paper/test_execution_and_sync.py +++ b/tests/unit/alpaca_paper/test_execution_and_sync.py @@ -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() diff --git a/trading_mode.py b/trading_mode.py index 84c9d11..85644ad 100644 --- a/trading_mode.py +++ b/trading_mode.py @@ -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))