diff --git a/admin_api.py b/admin_api.py
index b1725e4..d1e746a 100644
--- a/admin_api.py
+++ b/admin_api.py
@@ -244,16 +244,18 @@ def checks() -> dict:
except Exception as exc: # noqa: BLE001
out["db"] = {"ok": False, "error": str(exc)[:120]}
try:
- from alpaca_paper.client import AlpacaPaperClient
- from alpaca_paper.settings import AlpacaPaperSettings
- c = AlpacaPaperClient(AlpacaPaperSettings.from_env())
- clock = c.get_clock()
- acct = c.get_account()
- out["alpaca"] = {"ok": True, "market_open": clock.get("is_open"),
- "equity": acct.get("equity"),
- "mode": ("LIVE" if "paper" not in
- os.environ.get("ALPACA_BASE_URL", "paper")
- else "paper")}
+ # active execution broker (BROKER env) — JSON key stays "alpaca" for
+ # UI compatibility; "name" carries the real broker label
+ from tradier_broker.factory import make_trading_client
+ c, label = make_trading_client()
+ if c is None:
+ out["alpaca"] = {"ok": False, "error": label}
+ else:
+ clock = c.get_clock()
+ acct = c.get_account()
+ out["alpaca"] = {"ok": True, "market_open": clock.get("is_open"),
+ "equity": acct.get("equity"), "name": label,
+ "mode": "LIVE" if label.endswith("LIVE") else "paper"}
except Exception as exc: # noqa: BLE001
out["alpaca"] = {"ok": False, "error": str(exc)[:120]}
try:
diff --git a/api/static/admin.html b/api/static/admin.html
index 054f54c..4840220 100644
--- a/api/static/admin.html
+++ b/api/static/admin.html
@@ -111,7 +111,7 @@ async function load(){
const c=s.checks||{};
$('#health').innerHTML=[
`
${dot(c.db?.ok)}
Database${c.db?.ok?`event ${c.db.newest_event_age_s}s ago`:c.db?.error||''}
`,
- `${dot(c.alpaca?.ok)}
Alpaca${c.alpaca?.ok?`$${(+c.alpaca.equity).toLocaleString()} · market ${c.alpaca.market_open?'open':'closed'}`:c.alpaca?.error||''}
`,
+ `${dot(c.alpaca?.ok)}
${c.alpaca?.name||'Broker'}${c.alpaca?.ok?`$${(+c.alpaca.equity).toLocaleString()} · market ${c.alpaca.market_open?'open':'closed'}`:c.alpaca?.error||''}
`,
`${dot(c.ollama?.ok)}
Ollama${c.ollama?.ok?'enrichment up':'down (fails safe)'}
`,
``].join('');
$('#mode').className='badge '+(c.alpaca?.mode==='LIVE'?'live':'paper');
diff --git a/backfill_journal.py b/backfill_journal.py
index 3daefe2..8bd3d11 100644
--- a/backfill_journal.py
+++ b/backfill_journal.py
@@ -45,16 +45,18 @@ def main() -> None:
args = ap.parse_args()
load_dotenv()
- from alpaca_paper.client import AlpacaPaperClient
from storage.event_schema import EventMode, PositionClosedEvent
from storage.event_store import EventStore
from storage.journal_backfill import flatten_fills, reconstruct_round_trips
+ from tradier_broker.factory import make_trading_client
store = EventStore("momentum")
- client = AlpacaPaperClient()
+ client, label = make_trading_client()
+ if client is None:
+ raise SystemExit(f"[backfill_journal] {label} — aborting")
raw = client.get_orders(status="closed", limit=500, nested=True)
- print(f"fetched {len(raw)} historical orders from Alpaca")
+ print(f"fetched {len(raw)} historical orders from {label}")
trips = reconstruct_round_trips(flatten_fills(raw))
print(f"reconstructed {len(trips)} completed round-trips")
diff --git a/research/eod_reconcile.py b/research/eod_reconcile.py
index dcb407d..8989034 100644
--- a/research/eod_reconcile.py
+++ b/research/eod_reconcile.py
@@ -119,15 +119,16 @@ def main() -> int:
from runtime.env_overrides import load as _load_overrides
_load_overrides()
- from alpaca_paper.client import AlpacaPaperClient
- from alpaca_paper.settings import AlpacaPaperSettings
from storage.event_store import EventStore
+ from tradier_broker.factory import make_trading_client
- settings = AlpacaPaperSettings.from_env()
- if not settings.is_configured:
- print("[eod_reconcile] no Alpaca keys — skipping", flush=True)
+ # reconcile against the ACTIVE execution broker (BROKER env), never a
+ # hardwired one — drift-checking the wrong broker is silently meaningless
+ client, label = make_trading_client()
+ if client is None:
+ print(f"[eod_reconcile] {label} — skipping", flush=True)
return 0
- client = AlpacaPaperClient(settings)
+ print(f"[eod_reconcile] reconciling against {label}", flush=True)
store = EventStore(os.environ.get("WATCHER_EVENT_DB_PATH", "momentum"))
try:
from runtime.notifier import send_telegram
diff --git a/tests/test_broker_factory.py b/tests/test_broker_factory.py
new file mode 100644
index 0000000..dbd251b
--- /dev/null
+++ b/tests/test_broker_factory.py
@@ -0,0 +1,56 @@
+"""make_trading_client honors BROKER env with no silent cross-broker fallback."""
+
+import unittest
+from unittest.mock import patch
+
+from alpaca_paper.client import AlpacaPaperClient
+from tradier_broker.client import TradierClient
+from tradier_broker.factory import make_trading_client
+
+ALPACA = {"ALPACA_API_KEY": "k", "ALPACA_SECRET_KEY": "s"}
+TRADIER = {"TRADIER_ACCESS_TOKEN": "t", "TRADIER_ACCOUNT_ID": "a"}
+
+
+def _env(extra):
+ env = {"BROKER": "", "TRADIER_ACCESS_TOKEN": "", "TRADIER_ACCOUNT_ID": "",
+ "TRADIER_ENV": "", "ALPACA_API_KEY": "", "ALPACA_SECRET_KEY": "",
+ "APCA_API_KEY_ID": "", "APCA_API_SECRET_KEY": "",
+ "APCA_API_BASE_URL": ""}
+ env.update(extra)
+ return patch.dict("os.environ", env)
+
+
+class FactoryTests(unittest.TestCase):
+ def test_default_is_alpaca(self):
+ with _env(ALPACA):
+ client, label = make_trading_client()
+ self.assertIsInstance(client, AlpacaPaperClient)
+ self.assertEqual(label, "alpaca-paper")
+
+ def test_tradier_selected(self):
+ with _env({**ALPACA, **TRADIER, "BROKER": "tradier"}):
+ client, label = make_trading_client()
+ self.assertIsInstance(client, TradierClient)
+ self.assertEqual(label, "tradier-sandbox")
+ self.assertIsInstance(client.data_client, AlpacaPaperClient)
+
+ def test_tradier_live_label(self):
+ with _env({**ALPACA, **TRADIER, "BROKER": "tradier", "TRADIER_ENV": "live"}):
+ _, label = make_trading_client()
+ self.assertEqual(label, "tradier-LIVE")
+
+ def test_no_fallback_when_tradier_keys_missing(self):
+ with _env({**ALPACA, "BROKER": "tradier"}):
+ client, label = make_trading_client()
+ self.assertIsNone(client)
+ self.assertIn("TRADIER_ACCESS_TOKEN", label)
+
+ def test_no_alpaca_keys(self):
+ with _env({}):
+ client, label = make_trading_client()
+ self.assertIsNone(client)
+ self.assertEqual(label, "no Alpaca keys")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tradier_broker/factory.py b/tradier_broker/factory.py
new file mode 100644
index 0000000..d781baf
--- /dev/null
+++ b/tradier_broker/factory.py
@@ -0,0 +1,46 @@
+"""Broker-aware trading-client factory.
+
+Single place that honors BROKER env for every standalone tool that talks to
+the EXECUTION broker (eod_reconcile, backfill_journal, admin health). The
+live loop wires the same logic inline in run_live_paper (with its own boot
+messaging); both must agree on the BROKER=tradier contract.
+
+No silent cross-broker fallback: if BROKER=tradier but Tradier keys are
+missing, callers get (None, reason) — reconciling or journaling against the
+WRONG broker is worse than skipping.
+"""
+
+from __future__ import annotations
+
+import os
+
+from alpaca_paper.client import AlpacaPaperClient
+from alpaca_paper.settings import AlpacaPaperSettings
+from tradier_broker.client import TradierClient
+from tradier_broker.settings import TradierSettings
+
+
+def make_trading_client(data_client: AlpacaPaperClient | None = None):
+ """Return (client, label) for the active execution broker, or
+ (None, reason) when its keys are missing. `label` is human-readable
+ ('alpaca-paper', 'tradier-sandbox', 'tradier-LIVE')."""
+ broker = os.environ.get("BROKER", "alpaca").strip().lower()
+ alpaca = AlpacaPaperSettings.from_env()
+ if data_client is None and alpaca.is_configured:
+ data_client = AlpacaPaperClient(alpaca)
+
+ if broker == "tradier":
+ ts = TradierSettings.from_env()
+ if not ts.is_configured:
+ return None, ("BROKER=tradier but TRADIER_ACCESS_TOKEN/"
+ "TRADIER_ACCOUNT_ID missing")
+ label = "tradier-sandbox" if ts.is_sandbox else "tradier-LIVE"
+ return TradierClient(ts, data_client=data_client), label
+
+ if not alpaca.is_configured:
+ return None, "no Alpaca keys"
+ # empty APCA_API_BASE_URL means the client falls back to the paper host —
+ # never label that LIVE
+ host = alpaca.trading_base_url or "paper"
+ label = "alpaca-paper" if "paper" in host else "alpaca-LIVE"
+ return (data_client or AlpacaPaperClient(alpaca)), label