opentradex 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.env.example +8 -0
  2. package/CLAUDE.md +98 -0
  3. package/README.md +246 -0
  4. package/SOUL.md +79 -0
  5. package/SPEC.md +317 -0
  6. package/SUBMISSION.md +30 -0
  7. package/architecture.excalidraw +170 -0
  8. package/architecture.png +0 -0
  9. package/bin/opentradex.mjs +4 -0
  10. package/data/.gitkeep +0 -0
  11. package/data/strategy_notes.md +158 -0
  12. package/gossip/__init__.py +0 -0
  13. package/gossip/dashboard.py +150 -0
  14. package/gossip/db.py +358 -0
  15. package/gossip/kalshi.py +492 -0
  16. package/gossip/news.py +235 -0
  17. package/gossip/trader.py +646 -0
  18. package/main.py +287 -0
  19. package/package.json +47 -0
  20. package/requirements.txt +7 -0
  21. package/src/cli.mjs +124 -0
  22. package/src/index.mjs +420 -0
  23. package/web/AGENTS.md +5 -0
  24. package/web/CLAUDE.md +1 -0
  25. package/web/README.md +36 -0
  26. package/web/components.json +25 -0
  27. package/web/eslint.config.mjs +18 -0
  28. package/web/next.config.ts +7 -0
  29. package/web/package-lock.json +11626 -0
  30. package/web/package.json +37 -0
  31. package/web/postcss.config.mjs +7 -0
  32. package/web/public/file.svg +1 -0
  33. package/web/public/globe.svg +1 -0
  34. package/web/public/next.svg +1 -0
  35. package/web/public/vercel.svg +1 -0
  36. package/web/public/window.svg +1 -0
  37. package/web/src/app/api/agent/route.ts +77 -0
  38. package/web/src/app/api/agent/stream/route.ts +87 -0
  39. package/web/src/app/api/markets/route.ts +15 -0
  40. package/web/src/app/api/news/live/route.ts +77 -0
  41. package/web/src/app/api/news/reddit/route.ts +118 -0
  42. package/web/src/app/api/news/route.ts +10 -0
  43. package/web/src/app/api/news/tiktok/route.ts +115 -0
  44. package/web/src/app/api/news/truthsocial/route.ts +116 -0
  45. package/web/src/app/api/news/twitter/route.ts +186 -0
  46. package/web/src/app/api/portfolio/route.ts +50 -0
  47. package/web/src/app/api/prices/route.ts +18 -0
  48. package/web/src/app/api/trades/route.ts +10 -0
  49. package/web/src/app/favicon.ico +0 -0
  50. package/web/src/app/globals.css +170 -0
  51. package/web/src/app/layout.tsx +36 -0
  52. package/web/src/app/page.tsx +366 -0
  53. package/web/src/components/AgentLog.tsx +71 -0
  54. package/web/src/components/LiveStream.tsx +394 -0
  55. package/web/src/components/MarketScanner.tsx +111 -0
  56. package/web/src/components/NewsFeed.tsx +561 -0
  57. package/web/src/components/PortfolioStrip.tsx +139 -0
  58. package/web/src/components/PositionsPanel.tsx +219 -0
  59. package/web/src/components/TopBar.tsx +127 -0
  60. package/web/src/components/ui/badge.tsx +52 -0
  61. package/web/src/components/ui/button.tsx +60 -0
  62. package/web/src/components/ui/card.tsx +103 -0
  63. package/web/src/components/ui/scroll-area.tsx +55 -0
  64. package/web/src/components/ui/separator.tsx +25 -0
  65. package/web/src/components/ui/tabs.tsx +82 -0
  66. package/web/src/components/ui/tooltip.tsx +66 -0
  67. package/web/src/lib/db.ts +81 -0
  68. package/web/src/lib/types.ts +130 -0
  69. package/web/src/lib/utils.ts +6 -0
  70. package/web/tsconfig.json +34 -0
@@ -0,0 +1,646 @@
1
+ """
2
+ Trading engine — paper + live execution, Kelly sizing, portfolio management.
3
+
4
+ CLI tool invoked by Claude Code agent:
5
+ python3 gossip/trader.py portfolio
6
+ python3 gossip/trader.py trade TICKER --side yes --contracts 3 --estimate 0.72 --confidence high --reasoning "..."
7
+ python3 gossip/trader.py exit TICKER --reasoning "..."
8
+ python3 gossip/trader.py settle TICKER --outcome yes
9
+ python3 gossip/trader.py history
10
+ python3 gossip/trader.py size TICKER --estimate 0.72 (dry-run: shows recommended size)
11
+
12
+ All output is JSON to stdout.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import asyncio
19
+ import json
20
+ import math
21
+ import os
22
+ import sys
23
+ from dataclasses import dataclass, field, asdict
24
+ from datetime import datetime, timezone
25
+ from pathlib import Path
26
+
27
+ from dotenv import load_dotenv
28
+
29
+ load_dotenv(Path(__file__).resolve().parent.parent / ".env")
30
+
31
+ DATA_DIR = Path(__file__).resolve().parent.parent / "data"
32
+ TRADES_FILE = DATA_DIR / "trades.json"
33
+ PORTFOLIO_FILE = DATA_DIR / "portfolio.json"
34
+
35
+ def log(msg: str) -> None:
36
+ print(msg, file=sys.stderr)
37
+
38
+ def get_db():
39
+ from gossip.db import GossipDB
40
+ return GossipDB()
41
+
42
+
43
+ # --- State management ---
44
+
45
+ @dataclass
46
+ class Trade:
47
+ timestamp: str
48
+ ticker: str
49
+ title: str
50
+ category: str
51
+ side: str # "yes" or "no"
52
+ action: str # "buy" or "sell"
53
+ contracts: int
54
+ entry_price: float
55
+ cost: float
56
+ fee: float
57
+ estimated_prob: float
58
+ edge: float
59
+ confidence: str
60
+ reasoning: str
61
+ news_trigger: str = ""
62
+ sources: list[str] = field(default_factory=list)
63
+ settled: bool = False
64
+ outcome: str = "" # "win", "loss", ""
65
+ pnl: float = 0.0
66
+ exit_reasoning: str = ""
67
+
68
+ @dataclass
69
+ class Portfolio:
70
+ bankroll: float = 30.0
71
+ total_pnl: float = 0.0
72
+ total_trades: int = 0
73
+ wins: int = 0
74
+ losses: int = 0
75
+ trades: list[Trade] = field(default_factory=list)
76
+
77
+ @property
78
+ def open_positions(self) -> list[Trade]:
79
+ return [t for t in self.trades if not t.settled and t.action == "buy"]
80
+
81
+ @property
82
+ def win_rate(self) -> float:
83
+ if self.total_trades == 0:
84
+ return 0.0
85
+ return self.wins / self.total_trades
86
+
87
+ @property
88
+ def deployed_capital(self) -> float:
89
+ return sum(t.cost + t.fee for t in self.open_positions)
90
+
91
+
92
+ def load_portfolio() -> Portfolio:
93
+ if not TRADES_FILE.exists():
94
+ return Portfolio(bankroll=float(os.getenv("BANKROLL", "30.0")))
95
+
96
+ try:
97
+ data = json.loads(TRADES_FILE.read_text())
98
+ p = Portfolio()
99
+ p.bankroll = data.get("bankroll", float(os.getenv("BANKROLL", "30.0")))
100
+ p.total_pnl = data.get("total_pnl", 0.0)
101
+ p.total_trades = data.get("total_trades", 0)
102
+ p.wins = data.get("wins", 0)
103
+ p.losses = data.get("losses", 0)
104
+ for t in data.get("trades", []):
105
+ p.trades.append(Trade(**t))
106
+ return p
107
+ except Exception as e:
108
+ log(f"Error loading portfolio: {e}")
109
+ return Portfolio(bankroll=float(os.getenv("BANKROLL", "30.0")))
110
+
111
+
112
+ def save_portfolio(p: Portfolio) -> None:
113
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
114
+ data = {
115
+ "bankroll": round(p.bankroll, 2),
116
+ "total_pnl": round(p.total_pnl, 2),
117
+ "total_trades": p.total_trades,
118
+ "wins": p.wins,
119
+ "losses": p.losses,
120
+ "trades": [asdict(t) for t in p.trades],
121
+ }
122
+ TRADES_FILE.write_text(json.dumps(data, indent=2))
123
+
124
+
125
+ # --- Sizing ---
126
+
127
+ def kalshi_fee(contracts: int, price: float) -> float:
128
+ return math.ceil(0.07 * contracts * price * (1 - price)) / 100
129
+
130
+
131
+ def kelly_size(
132
+ estimated_prob: float,
133
+ market_price: float,
134
+ bankroll: float,
135
+ side: str = "yes",
136
+ kelly_fraction: float = 0.5,
137
+ max_position_pct: float = 0.30,
138
+ ) -> dict:
139
+ """Half-Kelly position sizing."""
140
+ if side == "yes":
141
+ p = estimated_prob
142
+ price = market_price
143
+ else:
144
+ p = 1 - estimated_prob
145
+ price = 1 - market_price
146
+
147
+ if price <= 0 or price >= 1 or p <= price:
148
+ return {"contracts": 0, "edge": 0, "reason": "no edge"}
149
+
150
+ edge = p - price
151
+ # Kelly: f* = (p * b - q) / b where b = (1-price)/price, q = 1-p
152
+ b = (1 - price) / price
153
+ q = 1 - p
154
+ kelly_f = (p * b - q) / b
155
+ adjusted_f = kelly_f * kelly_fraction
156
+
157
+ max_bet = bankroll * max_position_pct
158
+ bet_size = min(bankroll * adjusted_f, max_bet)
159
+
160
+ contracts = max(1, int(bet_size / price))
161
+ cost = contracts * price
162
+ fee = kalshi_fee(contracts, price)
163
+
164
+ if cost + fee > bankroll:
165
+ contracts = max(1, int((bankroll - 0.01) / (price + kalshi_fee(1, price) / 1)))
166
+ cost = contracts * price
167
+ fee = kalshi_fee(contracts, price)
168
+
169
+ return {
170
+ "contracts": contracts,
171
+ "edge": round(edge, 4),
172
+ "kelly_f": round(kelly_f, 4),
173
+ "adjusted_f": round(adjusted_f, 4),
174
+ "bet_size": round(bet_size, 2),
175
+ "cost": round(cost, 2),
176
+ "fee": round(fee, 4),
177
+ "total_cost": round(cost + fee, 2),
178
+ "expected_value": round(contracts * edge, 2),
179
+ }
180
+
181
+
182
+ # --- Risk checks ---
183
+
184
+ def check_risk(portfolio: Portfolio, ticker: str, cost: float) -> dict:
185
+ """Run risk checks before a trade."""
186
+ issues = []
187
+ max_pos_pct = float(os.getenv("MAX_POSITION_PCT", "0.30"))
188
+ min_edge = float(os.getenv("MIN_EDGE", "0.10"))
189
+
190
+ if cost > portfolio.bankroll * max_pos_pct:
191
+ issues.append(f"Position size ${cost:.2f} exceeds {max_pos_pct:.0%} of bankroll ${portfolio.bankroll:.2f}")
192
+
193
+ if cost > portfolio.bankroll:
194
+ issues.append(f"Insufficient bankroll: need ${cost:.2f}, have ${portfolio.bankroll:.2f}")
195
+
196
+ if len(portfolio.open_positions) >= 5:
197
+ issues.append(f"Max 5 concurrent positions reached ({len(portfolio.open_positions)} open)")
198
+
199
+ existing = [t for t in portfolio.open_positions if t.ticker == ticker]
200
+ if existing:
201
+ issues.append(f"Already have open position on {ticker}")
202
+
203
+ return {"ok": len(issues) == 0, "issues": issues}
204
+
205
+
206
+ # --- Trade execution ---
207
+
208
+ async def execute_trade(
209
+ ticker: str,
210
+ side: str,
211
+ contracts: int,
212
+ estimated_prob: float,
213
+ confidence: str,
214
+ reasoning: str,
215
+ news_trigger: str = "",
216
+ sources: list[str] | None = None,
217
+ ) -> dict:
218
+ from gossip.kalshi import get_market_detail, get_orderbook as fetch_orderbook, kalshi_fee as kfee
219
+
220
+ detail = await get_market_detail(ticker)
221
+ market = detail.get("market", {})
222
+ if not market or "error" in detail:
223
+ return {"error": f"Market {ticker} not found"}
224
+
225
+ title = market.get("title", "?")
226
+ category = market.get("category", "?")
227
+
228
+ # Use orderbook for real best bid/ask, fall back to market summary
229
+ orderbook = detail.get("orderbook", {})
230
+ if not orderbook:
231
+ orderbook = await fetch_orderbook(ticker)
232
+
233
+ # Orderbook uses yes_dollars/no_dollars (each entry: [price_str, quantity_str])
234
+ yes_bids = orderbook.get("yes_dollars", orderbook.get("yes", [])) if orderbook else []
235
+ no_bids = orderbook.get("no_dollars", orderbook.get("no", [])) if orderbook else []
236
+
237
+ # Market summary prices
238
+ summary_bid = float(market.get("yes_bid_dollars", "0") or "0")
239
+ summary_ask = float(market.get("yes_ask_dollars", "0") or "0")
240
+
241
+ # Best bid/ask from orderbook — prices are already in dollars (e.g. "0.47")
242
+ if yes_bids:
243
+ ob_yes_bid = max(float(b[0]) for b in yes_bids)
244
+ else:
245
+ ob_yes_bid = summary_bid
246
+
247
+ if no_bids:
248
+ best_no_bid = max(float(b[0]) for b in no_bids)
249
+ ob_yes_ask = round(1.0 - best_no_bid, 4) # NO bid of 0.53 = YES ask of 0.47
250
+ else:
251
+ ob_yes_ask = summary_ask
252
+
253
+ bid = ob_yes_bid if ob_yes_bid > 0 else summary_bid
254
+ ask = ob_yes_ask if ob_yes_ask > 0 else summary_ask
255
+
256
+ log(f"TRADE PRICING for {ticker}: yes_bid={bid:.4f} yes_ask={ask:.4f} (summary: bid={summary_bid:.4f} ask={summary_ask:.4f})")
257
+
258
+ if side == "yes":
259
+ entry_price = ask
260
+ edge = estimated_prob - ask
261
+ else:
262
+ entry_price = 1.0 - bid
263
+ edge = (1 - estimated_prob) - (1 - bid)
264
+
265
+ if entry_price <= 0 or entry_price >= 1:
266
+ return {"error": f"Invalid entry price {entry_price:.4f} for {side} side (bid={bid:.4f} ask={ask:.4f})"}
267
+
268
+ cost = entry_price * contracts
269
+ fee = kalshi_fee(contracts, entry_price)
270
+
271
+ portfolio = load_portfolio()
272
+
273
+ risk = check_risk(portfolio, ticker, cost + fee)
274
+ if not risk["ok"]:
275
+ return {"error": "Risk check failed", "issues": risk["issues"]}
276
+
277
+ price_detail = f"yes_bid={bid:.4f} yes_ask={ask:.4f} | entry={entry_price:.4f} ({side})"
278
+ full_reasoning = f"{reasoning}\n[Prices at execution: {price_detail}]"
279
+
280
+ trade = Trade(
281
+ timestamp=datetime.now(timezone.utc).isoformat(),
282
+ ticker=ticker,
283
+ title=title,
284
+ category=category,
285
+ side=side,
286
+ action="buy",
287
+ contracts=contracts,
288
+ entry_price=round(entry_price, 4),
289
+ cost=round(cost, 2),
290
+ fee=round(fee, 4),
291
+ estimated_prob=estimated_prob,
292
+ edge=round(edge, 4),
293
+ confidence=confidence,
294
+ reasoning=full_reasoning,
295
+ news_trigger=news_trigger,
296
+ sources=sources or [],
297
+ )
298
+
299
+ # Live execution if enabled
300
+ live_trading = os.getenv("LIVE_TRADING", "false").lower() == "true"
301
+ order_result = None
302
+ if live_trading:
303
+ from gossip.kalshi import place_order
304
+ price_cents = int(entry_price * 100)
305
+ order_result = await place_order(
306
+ ticker=ticker,
307
+ action="buy",
308
+ side=side,
309
+ count=contracts,
310
+ price_cents=price_cents,
311
+ order_type="limit",
312
+ )
313
+ if "error" in order_result:
314
+ return {"error": f"Live order failed: {order_result}", "mode": "live"}
315
+ log(f"LIVE ORDER placed: {order_result}")
316
+
317
+ portfolio.trades.append(trade)
318
+ portfolio.bankroll = round(portfolio.bankroll - cost - fee, 2)
319
+ portfolio.total_trades += 1
320
+ save_portfolio(portfolio)
321
+
322
+ try:
323
+ db = get_db()
324
+ db.insert_trade(
325
+ ticker=ticker, title=title, category=category, side=side,
326
+ contracts=contracts, entry_price=round(entry_price, 4),
327
+ cost=round(cost, 2), fee=round(fee, 4),
328
+ estimated_prob=estimated_prob, edge=round(edge, 4),
329
+ confidence=confidence, reasoning=full_reasoning,
330
+ news_trigger=news_trigger, sources=sources or [],
331
+ )
332
+ except Exception as e:
333
+ log(f"DB write failed (trade still recorded in JSON): {e}")
334
+
335
+ return {
336
+ "status": "executed",
337
+ "mode": "live" if live_trading else "paper",
338
+ "order": order_result,
339
+ "ticker": ticker,
340
+ "side": side,
341
+ "contracts": contracts,
342
+ "entry_price": trade.entry_price,
343
+ "cost": trade.cost,
344
+ "fee": trade.fee,
345
+ "edge": trade.edge,
346
+ "yes_bid": bid,
347
+ "yes_ask": ask,
348
+ "summary_bid": summary_bid,
349
+ "summary_ask": summary_ask,
350
+ "bankroll_remaining": portfolio.bankroll,
351
+ "title": title,
352
+ }
353
+
354
+
355
+ async def exit_position(ticker: str, reasoning: str) -> dict:
356
+ from gossip.kalshi import get_market_detail
357
+
358
+ portfolio = load_portfolio()
359
+ open_trade = None
360
+ for t in reversed(portfolio.trades):
361
+ if t.ticker == ticker and not t.settled:
362
+ open_trade = t
363
+ break
364
+
365
+ if not open_trade:
366
+ return {"error": f"No open position on {ticker}"}
367
+
368
+ detail = await get_market_detail(ticker)
369
+ market = detail.get("market", {})
370
+ bid = float(market.get("yes_bid_dollars", "0") or "0")
371
+ ask = float(market.get("yes_ask_dollars", "0") or "0")
372
+
373
+ if open_trade.side == "yes":
374
+ exit_price = bid
375
+ else:
376
+ exit_price = 1.0 - ask
377
+
378
+ pnl = round((exit_price - open_trade.entry_price) * open_trade.contracts, 2)
379
+
380
+ open_trade.settled = True
381
+ open_trade.outcome = "win" if pnl > 0 else "loss"
382
+ open_trade.pnl = pnl
383
+ open_trade.exit_reasoning = reasoning
384
+
385
+ portfolio.bankroll = round(portfolio.bankroll + (exit_price * open_trade.contracts), 2)
386
+ portfolio.total_pnl = round(portfolio.total_pnl + pnl, 2)
387
+ if pnl > 0:
388
+ portfolio.wins += 1
389
+ else:
390
+ portfolio.losses += 1
391
+
392
+ save_portfolio(portfolio)
393
+
394
+ return {
395
+ "status": "exited",
396
+ "ticker": ticker,
397
+ "pnl": pnl,
398
+ "exit_price": exit_price,
399
+ "bankroll": portfolio.bankroll,
400
+ }
401
+
402
+
403
+ def settle_market(ticker: str, outcome_yes: bool) -> dict:
404
+ portfolio = load_portfolio()
405
+
406
+ for t in reversed(portfolio.trades):
407
+ if t.ticker == ticker and not t.settled:
408
+ t.settled = True
409
+ won = (t.side == "yes" and outcome_yes) or (t.side == "no" and not outcome_yes)
410
+ if won:
411
+ t.outcome = "win"
412
+ t.pnl = round((1.0 - t.entry_price) * t.contracts, 2)
413
+ portfolio.wins += 1
414
+ else:
415
+ t.outcome = "loss"
416
+ t.pnl = round(-t.entry_price * t.contracts, 2)
417
+ portfolio.losses += 1
418
+
419
+ portfolio.total_pnl = round(portfolio.total_pnl + t.pnl, 2)
420
+ portfolio.bankroll = round(portfolio.bankroll + t.entry_price * t.contracts + t.pnl, 2)
421
+ save_portfolio(portfolio)
422
+
423
+ return {
424
+ "status": "settled",
425
+ "ticker": ticker,
426
+ "outcome": "yes" if outcome_yes else "no",
427
+ "result": t.outcome,
428
+ "pnl": t.pnl,
429
+ "bankroll": portfolio.bankroll,
430
+ }
431
+
432
+ return {"error": f"No open trade on {ticker}"}
433
+
434
+
435
+ async def check_settlements() -> list[dict]:
436
+ """Check if any open positions have resolved on Kalshi and auto-settle them."""
437
+ from gossip.kalshi import get_market_detail
438
+
439
+ portfolio = load_portfolio()
440
+ settled = []
441
+ for t in portfolio.trades:
442
+ if t.settled:
443
+ continue
444
+ detail = await get_market_detail(t.ticker)
445
+ market = detail.get("market", {})
446
+ result = market.get("result", "")
447
+ status = market.get("status", "")
448
+ if result in ("yes", "no") or status == "settled":
449
+ outcome_yes = result == "yes"
450
+ won = (t.side == "yes" and outcome_yes) or (t.side == "no" and not outcome_yes)
451
+ t.settled = True
452
+ if won:
453
+ t.outcome = "win"
454
+ t.pnl = round((1.0 - t.entry_price) * t.contracts, 2)
455
+ portfolio.wins += 1
456
+ else:
457
+ t.outcome = "loss"
458
+ t.pnl = round(-t.entry_price * t.contracts, 2)
459
+ portfolio.losses += 1
460
+ portfolio.total_pnl = round(portfolio.total_pnl + t.pnl, 2)
461
+ portfolio.bankroll = round(portfolio.bankroll + t.entry_price * t.contracts + t.pnl, 2)
462
+ settled.append({
463
+ "ticker": t.ticker,
464
+ "result": result,
465
+ "outcome": t.outcome,
466
+ "pnl": t.pnl,
467
+ })
468
+ log(f"AUTO-SETTLED {t.ticker}: {t.outcome} (pnl: {t.pnl:+.2f})")
469
+
470
+ if settled:
471
+ save_portfolio(portfolio)
472
+ return settled
473
+
474
+
475
+ async def get_position_prices() -> list[dict]:
476
+ """Fetch current prices for all open positions and calculate unrealized P&L."""
477
+ from gossip.kalshi import get_market_detail
478
+
479
+ portfolio = load_portfolio()
480
+ positions = []
481
+ for t in portfolio.open_positions:
482
+ detail = await get_market_detail(t.ticker)
483
+ market = detail.get("market", {})
484
+ bid = float(market.get("yes_bid_dollars", "0") or "0")
485
+ ask = float(market.get("yes_ask_dollars", "0") or "0")
486
+ mid = (bid + ask) / 2 if bid > 0 or ask > 0 else 0
487
+
488
+ if t.side == "yes":
489
+ current_value = bid * t.contracts
490
+ mark_price = bid
491
+ else:
492
+ current_value = (1.0 - ask) * t.contracts
493
+ mark_price = 1.0 - ask
494
+
495
+ unrealized_pnl = round(current_value - t.cost, 2)
496
+ positions.append({
497
+ "ticker": t.ticker,
498
+ "title": t.title,
499
+ "side": t.side,
500
+ "contracts": t.contracts,
501
+ "entry_price": t.entry_price,
502
+ "mark_price": round(mark_price, 4),
503
+ "mid": round(mid, 4),
504
+ "cost": t.cost,
505
+ "current_value": round(current_value, 2),
506
+ "unrealized_pnl": unrealized_pnl,
507
+ "pnl_pct": round(unrealized_pnl / t.cost * 100, 1) if t.cost > 0 else 0,
508
+ "status": market.get("status", ""),
509
+ "result": market.get("result", ""),
510
+ })
511
+ return positions
512
+
513
+
514
+ # --- CLI ---
515
+
516
+ async def main():
517
+ parser = argparse.ArgumentParser(description="Open Trademaxxxing engine")
518
+ sub = parser.add_subparsers(dest="command")
519
+
520
+ sub.add_parser("portfolio", help="Show portfolio")
521
+
522
+ trade_p = sub.add_parser("trade", help="Execute paper trade")
523
+ trade_p.add_argument("ticker")
524
+ trade_p.add_argument("--side", choices=["yes", "no"], required=True)
525
+ trade_p.add_argument("--contracts", type=int, default=None)
526
+ trade_p.add_argument("--estimate", type=float, required=True)
527
+ trade_p.add_argument("--confidence", choices=["low", "medium", "high"], default="medium")
528
+ trade_p.add_argument("--reasoning", type=str, required=True)
529
+ trade_p.add_argument("--news", type=str, default="")
530
+ trade_p.add_argument("--sources", type=str, default="")
531
+
532
+ exit_p = sub.add_parser("exit", help="Exit position")
533
+ exit_p.add_argument("ticker")
534
+ exit_p.add_argument("--reasoning", type=str, required=True)
535
+
536
+ settle_p = sub.add_parser("settle", help="Settle resolved market")
537
+ settle_p.add_argument("ticker")
538
+ settle_p.add_argument("--outcome", choices=["yes", "no"], required=True)
539
+
540
+ size_p = sub.add_parser("size", help="Calculate position size (dry run)")
541
+ size_p.add_argument("ticker")
542
+ size_p.add_argument("--estimate", type=float, required=True)
543
+ size_p.add_argument("--side", choices=["yes", "no"], default="yes")
544
+
545
+ sub.add_parser("history", help="Trade history")
546
+ sub.add_parser("check-settled", help="Auto-settle resolved markets")
547
+ sub.add_parser("prices", help="Current prices + unrealized P&L for open positions")
548
+
549
+ args = parser.parse_args()
550
+
551
+ if args.command == "portfolio":
552
+ p = load_portfolio()
553
+ result = {
554
+ "bankroll": p.bankroll,
555
+ "total_pnl": p.total_pnl,
556
+ "total_trades": p.total_trades,
557
+ "wins": p.wins,
558
+ "losses": p.losses,
559
+ "win_rate": round(p.win_rate, 3),
560
+ "deployed_capital": round(p.deployed_capital, 2),
561
+ "open_positions": [
562
+ {
563
+ "ticker": t.ticker,
564
+ "title": t.title,
565
+ "side": t.side,
566
+ "contracts": t.contracts,
567
+ "entry_price": t.entry_price,
568
+ "edge": t.edge,
569
+ "confidence": t.confidence,
570
+ "reasoning": t.reasoning[:200],
571
+ }
572
+ for t in p.open_positions
573
+ ],
574
+ }
575
+ print(json.dumps(result, indent=2))
576
+
577
+ elif args.command == "trade":
578
+ if args.contracts is None:
579
+ from gossip.kalshi import get_market_detail
580
+ detail = await get_market_detail(args.ticker)
581
+ market = detail.get("market", {})
582
+ bid = float(market.get("yes_bid_dollars", "0") or "0")
583
+ ask = float(market.get("yes_ask_dollars", "0") or "0")
584
+ market_price = ask if args.side == "yes" else (1 - bid)
585
+ p = load_portfolio()
586
+ sizing = kelly_size(args.estimate, market_price, p.bankroll, args.side)
587
+ contracts = sizing["contracts"]
588
+ else:
589
+ contracts = args.contracts
590
+
591
+ sources = [s.strip() for s in args.sources.split(",") if s.strip()] if args.sources else []
592
+ result = await execute_trade(
593
+ ticker=args.ticker,
594
+ side=args.side,
595
+ contracts=contracts,
596
+ estimated_prob=args.estimate,
597
+ confidence=args.confidence,
598
+ reasoning=args.reasoning,
599
+ news_trigger=args.news,
600
+ sources=sources,
601
+ )
602
+ print(json.dumps(result, indent=2))
603
+
604
+ elif args.command == "exit":
605
+ result = await exit_position(args.ticker, args.reasoning)
606
+ print(json.dumps(result, indent=2))
607
+
608
+ elif args.command == "settle":
609
+ result = settle_market(args.ticker, args.outcome == "yes")
610
+ print(json.dumps(result, indent=2))
611
+
612
+ elif args.command == "size":
613
+ from gossip.kalshi import get_market_detail
614
+ detail = await get_market_detail(args.ticker)
615
+ market = detail.get("market", {})
616
+ bid = float(market.get("yes_bid_dollars", "0") or "0")
617
+ ask = float(market.get("yes_ask_dollars", "0") or "0")
618
+ market_price = ask if args.side == "yes" else (1 - bid)
619
+ p = load_portfolio()
620
+ result = kelly_size(args.estimate, market_price, p.bankroll, args.side)
621
+ result["ticker"] = args.ticker
622
+ result["market_price"] = market_price
623
+ result["estimated_prob"] = args.estimate
624
+ result["bankroll"] = p.bankroll
625
+ print(json.dumps(result, indent=2))
626
+
627
+ elif args.command == "history":
628
+ p = load_portfolio()
629
+ trades = [asdict(t) for t in p.trades[-20:]]
630
+ print(json.dumps(trades, indent=2))
631
+
632
+ elif args.command == "check-settled":
633
+ settled = await check_settlements()
634
+ print(json.dumps({"settled": settled, "count": len(settled)}, indent=2))
635
+
636
+ elif args.command == "prices":
637
+ positions = await get_position_prices()
638
+ total_unrealized = sum(p["unrealized_pnl"] for p in positions)
639
+ print(json.dumps({"positions": positions, "total_unrealized_pnl": round(total_unrealized, 2)}, indent=2))
640
+
641
+ else:
642
+ parser.print_help()
643
+
644
+
645
+ if __name__ == "__main__":
646
+ asyncio.run(main())