Files
momentum devandClaude Fable 5 1cefb51c63 feat: Tradier broker mirror — config-switch redundancy for Alpaca execution
tradier_broker/ mirrors every trading call the live loop makes on
AlpacaPaperClient (account, clock, positions, orders, submit incl.
bracket->OTOCO, cancel, replace, close) with identical signatures and
Alpaca-shaped returns, so trading_execution / exit_manager / flatten /
execution / sync run unchanged. Market data always delegates to Alpaca.

- BROKER=tradier (+ TRADIER_* env) swaps execution client in run_live_paper;
  default path untouched
- client_order_id -> tag preserves the lost-submit reconcile
- TradierApiError subclasses AlpacaApiError so the executor's
  rejection-vs-transport fork works as-is
- sandbox-verified: balances/clock/positions/orders, OTOCO preview
  (result:true), place -> tag lookup -> cancel round trip
- 18 unit tests (translation + client paths); executor integration checked
- docs/TRADIER-FALLBACK.md switch runbook

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 11:01:50 -05:00

96 lines
4.0 KiB
Python

"""Tradier sandbox smoke test — proves the mirror talks to the broker.
Read-only by default: balances, clock, positions, orders. Optional order
exercises (sandbox-safe):
python -m tradier_broker.smoke # read-only checks
python -m tradier_broker.smoke --preview AAPL # preview (never submits) a
# bracket + a market order
python -m tradier_broker.smoke --place-cancel AAPL # place a far-below-market
# limit buy, then cancel it
Requires TRADIER_ACCESS_TOKEN + TRADIER_ACCOUNT_ID in env/.env.
Refuses order exercises against the live base URL.
"""
from __future__ import annotations
import argparse
import json
import sys
import time
from dotenv import load_dotenv
from tradier_broker.client import TradierClient, build_order_form
from tradier_broker.settings import TradierSettings
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--preview", metavar="SYMBOL",
help="preview (validate, never submit) a bracket and a market order")
parser.add_argument("--place-cancel", metavar="SYMBOL",
help="place a $0.50-limit buy for 1 share, then cancel it")
args = parser.parse_args(argv)
load_dotenv()
settings = TradierSettings.from_env()
if not settings.is_configured:
print("TRADIER_ACCESS_TOKEN / TRADIER_ACCOUNT_ID not set")
return 1
print(f"env: {'SANDBOX' if settings.is_sandbox else 'LIVE'} "
f"account: {settings.account_id}")
client = TradierClient(settings)
account = client.get_account()
print(f"account: equity={account['equity']} cash={account['cash']} "
f"buying_power={account['buying_power']}")
clock = client.get_clock()
print(f"clock: state={clock['state']} is_open={clock['is_open']}")
positions = client.get_positions(fresh=True)
print(f"positions: {len(positions)}")
for p in positions:
print(f" {p['symbol']} qty={p['qty']} avg={p['avg_entry_price']} "
f"last={p['current_price']} upl={p['unrealized_pl']}")
orders = client.get_orders(status="all")
print(f"orders (session): {len(orders)}")
for o in orders[:10]:
print(f" {o['id']} {o['symbol']} {o['side']} {o['qty']} {o['type']} "
f"status={o['status']} tag={o['client_order_id'] or '-'}")
if (args.preview or args.place_cancel) and not settings.is_sandbox:
print("refusing order exercises against LIVE base URL")
return 1
if args.preview:
sym = args.preview.upper()
for label, kw in (
("bracket limit", dict(order_type="limit", limit_price=1.00,
stop_loss_price=0.80, take_profit_price=1.50)),
("plain market", dict(order_type="market")),
):
form = build_order_form(sym, 1, "buy", kw.pop("order_type"), "day",
kw.get("limit_price"), None,
kw.get("stop_loss_price"),
kw.get("take_profit_price"), None)
form["preview"] = "true"
payload = client._acct("POST", "/orders", form=form) # noqa: SLF001
print(f"preview {label}: {json.dumps(payload)[:400]}")
if args.place_cancel:
sym = args.place_cancel.upper()
resp = client.submit_order(symbol=sym, qty=1, side="buy",
order_type="limit", limit_price=0.50,
client_order_id=f"smoke-{int(time.time())}")
print(f"placed: id={resp['id']} status={resp['status']}")
found = client.get_order_by_client_order_id(resp["client_order_id"])
print(f"lookup by tag: {'FOUND ' + str(found['status']) if found else 'NOT FOUND'}")
client.cancel_order(resp["id"])
print("cancelled ok")
return 0
if __name__ == "__main__":
sys.exit(main())