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>
96 lines
4.0 KiB
Python
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())
|