feat(ibkr-bot): mini test mode — validate paper env + order routing with 1 share

'mini' mode: confirms account is paper (DU prefix; aborts on live U-prefix), places BUY 1 SPY,
reports fill/routing. Validated against cluster ib-gateway: account DU9600528 [PAPER], order routed
(PreSubmitted, queued for next open — market closed on weekend). Confirms the full chain works:
cluster Gateway -> port-forward 4002 -> bot -> paper account.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-06-07 22:59:27 +02:00
parent 63d4f7f61d
commit 66e37955ce

View File

@@ -70,13 +70,37 @@ def main():
print(f"\nCONNECT FAILED on 127.0.0.1:{port} — is IB Gateway/TWS running + logged in to PAPER, API enabled?")
print(f" ({type(e).__name__}: {str(e)[:80]}) | try --port 7497 for TWS paper")
return
accts = ib.managedAccounts()
acct = accts[0] if accts else "?"
is_paper = acct.startswith("DU") # IBKR convention: DU* = paper/demo, U* = live
nlv = next((float(v.value) for v in ib.accountSummary() if v.tag == "NetLiquidation"), 0.0)
cash = next((float(v.value) for v in ib.accountSummary() if v.tag == "TotalCashValue"), 0.0)
pos = {p.contract.symbol: p.position for p in ib.positions()}
print(f"\nIBKR PAPER account: NLV ${nlv:,.0f} cash ${cash:,.0f} | positions: {pos or 'none'}")
print(f"\nIBKR account: {acct} [{'PAPER ✅' if is_paper else 'LIVE ⚠️ — NOT paper!'}] NLV ${nlv:,.0f} cash ${cash:,.0f} | positions: {pos or 'none'}")
if nlv <= 0:
print(" (no NLV — check the paper account is funded with paper cash)"); ib.disconnect(); return
if mode == "mini":
if not is_paper:
print(" ABORT: account is NOT a paper account (no DU prefix). Refusing to place a test order."); ib.disconnect(); return
from ib_async import Stock, MarketOrder
c = Stock("SPY", "SMART", "USD"); ib.qualifyContracts(c)
print("\nplacing MINI test order: BUY 1 SPY (market) on PAPER...")
o = MarketOrder("BUY", 1); o.account = acct
tr = ib.placeOrder(c, o)
for _ in range(20):
ib.sleep(1)
if tr.orderStatus.status in ("Filled", "Cancelled", "ApiCancelled", "Inactive"):
break
fills = [f"{f.execution.shares}@${f.execution.price}" for f in tr.fills]
print(f" order status: {tr.orderStatus.status} filled {tr.orderStatus.filled}/1 avgPx ${tr.orderStatus.avgFillPrice or 0:.2f} fills={fills or 'none'}")
ib.sleep(1)
newpos = {p.contract.symbol: p.position for p in ib.positions()}
print(f" positions now: {newpos or 'none'}")
print(f" → VALIDATED: this is the {'PAPER' if is_paper else 'LIVE'} env, order routing works." if tr.orderStatus.status == "Filled"
else " → order not filled yet (market may be closed — it'll fill at next open; status shows it routed).")
ib.disconnect(); return
orders = []
for t, w in tw.items():
tgt_sh = nlv * w / px[t]