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>
This commit is contained in:
1 parent
48d75816b6
commit
1cefb51c63
8 files changed
+960
-1
No files matched your search
@@ -0,0 +1,95 @@
|
||||
"""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())
|
||||
Reference in new issue
Block a user