probable-trader 1.0.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.
@@ -0,0 +1,474 @@
1
+ #!/usr/bin/env python3
2
+ """Probable Markets CLI — Agent-native trading skill for BSC prediction markets.
3
+
4
+ Usage: python3 scripts/prob.py <command> [options]
5
+ """
6
+
7
+ import argparse
8
+ import asyncio
9
+ import json
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ # Ensure lib is importable
14
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
15
+
16
+ from lib.config import ProbableConfig, load_config, init_config_dir
17
+ from lib.safety import ActionResult, require_confirm, output_result
18
+ from lib.db import ProbableDB
19
+ from lib.client_wrapper import ProbableClient
20
+
21
+
22
+ def get_client(args) -> ProbableClient:
23
+ cfg = load_config()
24
+ db = ProbableDB(cfg.db_path)
25
+ return ProbableClient(cfg=cfg, db=db)
26
+
27
+
28
+ # ── Config commands ──────────────────────────────────────────────
29
+
30
+ def cmd_config_show(args):
31
+ cfg = load_config()
32
+ result = ActionResult(success=True, action="config.show", data=cfg.to_dict(mask_secrets=True))
33
+ output_result(result, args.json)
34
+
35
+
36
+ def cmd_config_init(args):
37
+ path = init_config_dir()
38
+ result = ActionResult(success=True, action="config.init", data={"path": str(path)})
39
+ output_result(result, args.json)
40
+
41
+
42
+ # ── Doctor ───────────────────────────────────────────────────────
43
+
44
+ def cmd_doctor(args):
45
+ client = get_client(args)
46
+ result = client.doctor()
47
+ output_result(result, args.json)
48
+
49
+
50
+ # ── Setup (onboard) ─────────────────────────────────────────────
51
+
52
+ def cmd_setup(args):
53
+ client = get_client(args)
54
+
55
+ # Doctor first
56
+ doc = client.doctor()
57
+ if not doc.success:
58
+ output_result(doc, args.json)
59
+ return
60
+
61
+ blocked = require_confirm("setup", {"action": "enable_trading (approve tokens)"}, args.confirm, args.dry_run)
62
+ if blocked:
63
+ output_result(blocked, args.json)
64
+ return
65
+
66
+ result = client.enable_trading()
67
+ output_result(result, args.json)
68
+
69
+
70
+ # ── Onboard ──────────────────────────────────────────────────────
71
+
72
+ def cmd_onboard(args):
73
+ cfg = load_config()
74
+ details = {"action": "Generate API key via L1 auth (EIP-712 signing)"}
75
+ blocked = require_confirm("onboard", details, args.confirm, args.dry_run)
76
+ if blocked:
77
+ output_result(blocked, args.json)
78
+ return
79
+
80
+ from lib.onboard import run_onboard
81
+ result = run_onboard(cfg)
82
+ output_result(result, args.json)
83
+
84
+
85
+ # ── Market commands ──────────────────────────────────────────────
86
+
87
+ def cmd_market_list(args):
88
+ client = get_client(args)
89
+ result = client.get_markets(
90
+ topic_type=args.type,
91
+ page=args.page,
92
+ limit=args.limit,
93
+ status=args.status,
94
+ sort_by=args.sort_by,
95
+ )
96
+ output_result(result, args.json)
97
+
98
+
99
+ def cmd_market_get(args):
100
+ client = get_client(args)
101
+ result = client.get_market(int(args.market_id))
102
+ output_result(result, args.json)
103
+
104
+
105
+ def cmd_market_search(args):
106
+ client = get_client(args)
107
+ # Search is implemented as filtered list — get all and filter client-side
108
+ result = client.get_markets(page=1, limit=20)
109
+ if result.success and result.data:
110
+ query = args.query.lower()
111
+ items = result.data if isinstance(result.data, list) else result.data.get("items", result.data.get("data", []))
112
+ if isinstance(items, list):
113
+ filtered = [m for m in items if query in json.dumps(m, default=str).lower()]
114
+ result = ActionResult(success=True, action="market.search",
115
+ data={"query": args.query, "results": filtered, "count": len(filtered)})
116
+ output_result(result, args.json)
117
+
118
+
119
+ # ── Book / Price / History / Fee ─────────────────────────────────
120
+
121
+ def cmd_book(args):
122
+ client = get_client(args)
123
+ result = client.get_orderbook(args.token_id)
124
+ output_result(result, args.json)
125
+
126
+
127
+ def cmd_price(args):
128
+ client = get_client(args)
129
+ result = client.get_latest_price(args.token_id)
130
+ output_result(result, args.json)
131
+
132
+
133
+ def cmd_history(args):
134
+ client = get_client(args)
135
+ result = client.get_price_history(args.token_id, interval=args.interval)
136
+ output_result(result, args.json)
137
+
138
+
139
+ def cmd_fee_rates(args):
140
+ client = get_client(args)
141
+ result = client.get_fee_rates(args.token_id)
142
+ output_result(result, args.json)
143
+
144
+
145
+ # ── Order commands ───────────────────────────────────────────────
146
+
147
+ def cmd_order_place(args):
148
+ client = get_client(args)
149
+ details = {
150
+ "market_id": args.market_id, "token_id": args.token_id,
151
+ "side": args.side, "price": args.price, "amount": args.amount,
152
+ "order_type": args.type or "limit",
153
+ }
154
+ blocked = require_confirm("order.place", details, args.confirm, args.dry_run)
155
+ if blocked:
156
+ output_result(blocked, args.json)
157
+ return
158
+ result = client.place_order(
159
+ market_id=int(args.market_id), token_id=args.token_id,
160
+ side=args.side, price=args.price, amount=args.amount,
161
+ order_type=args.type or "limit",
162
+ )
163
+ output_result(result, args.json)
164
+
165
+
166
+ def cmd_order_get(args):
167
+ client = get_client(args)
168
+ result = client.get_order_by_id(args.order_id)
169
+ output_result(result, args.json)
170
+
171
+
172
+ def cmd_order_list(args):
173
+ client = get_client(args)
174
+ result = client.get_my_orders(
175
+ market_id=int(args.market_id) if args.market_id else 0,
176
+ status=args.status or "",
177
+ limit=10, page=1,
178
+ )
179
+ output_result(result, args.json)
180
+
181
+
182
+ def cmd_order_cancel(args):
183
+ client = get_client(args)
184
+ blocked = require_confirm("order.cancel", {"order_id": args.order_id}, args.confirm, args.dry_run)
185
+ if blocked:
186
+ output_result(blocked, args.json)
187
+ return
188
+ result = client.cancel_order(args.order_id)
189
+ output_result(result, args.json)
190
+
191
+
192
+ def cmd_order_cancel_all(args):
193
+ client = get_client(args)
194
+ details = {"market_id": args.market_id}
195
+ blocked = require_confirm("order.cancel_all", details, args.confirm, args.dry_run)
196
+ if blocked:
197
+ output_result(blocked, args.json)
198
+ return
199
+ mid = int(args.market_id) if args.market_id else None
200
+ result = client.cancel_all_orders(market_id=mid)
201
+ output_result(result, args.json)
202
+
203
+
204
+ # ── Positions / Trades / Balance ─────────────────────────────────
205
+
206
+ def cmd_positions(args):
207
+ client = get_client(args)
208
+ mid = int(args.market_id) if args.market_id else 0
209
+ result = client.get_my_positions(market_id=mid)
210
+ output_result(result, args.json)
211
+
212
+
213
+ def cmd_trades(args):
214
+ client = get_client(args)
215
+ mid = int(args.market_id) if args.market_id else None
216
+ result = client.get_my_trades(market_id=mid, page=args.page)
217
+ output_result(result, args.json)
218
+
219
+
220
+ def cmd_balance(args):
221
+ client = get_client(args)
222
+ result = client.get_my_balances()
223
+ output_result(result, args.json)
224
+
225
+
226
+ # ── Split / Merge / Redeem ───────────────────────────────────────
227
+
228
+ def cmd_split(args):
229
+ client = get_client(args)
230
+ details = {"market_id": args.market_id, "amount": args.amount}
231
+ blocked = require_confirm("split", details, args.confirm, args.dry_run)
232
+ if blocked:
233
+ output_result(blocked, args.json)
234
+ return
235
+ result = client.split(int(args.market_id), int(args.amount))
236
+ output_result(result, args.json)
237
+
238
+
239
+ def cmd_merge(args):
240
+ client = get_client(args)
241
+ details = {"market_id": args.market_id, "amount": args.amount}
242
+ blocked = require_confirm("merge", details, args.confirm, args.dry_run)
243
+ if blocked:
244
+ output_result(blocked, args.json)
245
+ return
246
+ result = client.merge(int(args.market_id), int(args.amount))
247
+ output_result(result, args.json)
248
+
249
+
250
+ def cmd_redeem(args):
251
+ client = get_client(args)
252
+ details = {"market_id": args.market_id}
253
+ blocked = require_confirm("redeem", details, args.confirm, args.dry_run)
254
+ if blocked:
255
+ output_result(blocked, args.json)
256
+ return
257
+ result = client.redeem(int(args.market_id))
258
+ output_result(result, args.json)
259
+
260
+
261
+ # ── WebSocket commands ───────────────────────────────────────────
262
+
263
+ def cmd_ws_book(args):
264
+ from lib.ws_client import subscribe_book
265
+ asyncio.run(subscribe_book(args.token_id, duration=args.duration, json_mode=args.json))
266
+
267
+
268
+ def cmd_ws_user(args):
269
+ cfg = load_config()
270
+ from lib.ws_client import subscribe_user
271
+ asyncio.run(subscribe_user(cfg, duration=args.duration, json_mode=args.json))
272
+
273
+
274
+ # ── Report command ───────────────────────────────────────────────
275
+
276
+ def cmd_report_daily(args):
277
+ cfg = load_config()
278
+ db = ProbableDB(cfg.db_path)
279
+ from lib.report import generate_daily_report
280
+ result = generate_daily_report(db, fmt=args.format)
281
+ if args.output:
282
+ Path(args.output).write_text(result.to_json() if args.json else json.dumps(result.data, indent=2, default=str))
283
+ print(f"Report written to {args.output}")
284
+ else:
285
+ output_result(result, args.json)
286
+
287
+
288
+ # ── Argument parser ──────────────────────────────────────────────
289
+
290
+ def build_parser() -> argparse.ArgumentParser:
291
+ # Shared parent with global flags — allows --json/--confirm/--dry-run anywhere
292
+ parent = argparse.ArgumentParser(add_help=False)
293
+ parent.add_argument("--json", action="store_true", help="JSON-only output")
294
+ parent.add_argument("--confirm", action="store_true", help="Confirm destructive actions")
295
+ parent.add_argument("--dry-run", action="store_true", help="Preview without executing")
296
+
297
+ p = argparse.ArgumentParser(prog="prob.py", description="Probable Markets CLI", parents=[parent])
298
+
299
+ sub = p.add_subparsers(dest="command", help="Available commands")
300
+
301
+ # config
302
+ cfg_p = sub.add_parser("config", help="Configuration", parents=[parent])
303
+ cfg_sub = cfg_p.add_subparsers(dest="config_cmd")
304
+ cfg_sub.add_parser("show", help="Show current config", parents=[parent])
305
+ cfg_sub.add_parser("init", help="Create .probable/ directory", parents=[parent])
306
+
307
+ # doctor / onboard / setup
308
+ sub.add_parser("doctor", help="Health check", parents=[parent])
309
+ sub.add_parser("onboard", help="Generate API key (L1 auth, only needs private key)", parents=[parent])
310
+ sub.add_parser("setup", help="Enable trading approvals (on-chain)", parents=[parent])
311
+
312
+ # market
313
+ mkt_p = sub.add_parser("market", help="Market discovery", parents=[parent])
314
+ mkt_sub = mkt_p.add_subparsers(dest="market_cmd")
315
+
316
+ mkt_list = mkt_sub.add_parser("list", help="List markets", parents=[parent])
317
+ mkt_list.add_argument("--type", choices=["binary", "categorical", "all"], default=None)
318
+ mkt_list.add_argument("--status", choices=["activated", "resolved"], default=None)
319
+ mkt_list.add_argument("--sort-by", dest="sort_by",
320
+ choices=["time", "volume", "volume_24h", "volume_7d", "cutoff"], default=None)
321
+ mkt_list.add_argument("--page", type=int, default=1)
322
+ mkt_list.add_argument("--limit", type=int, default=20)
323
+
324
+ mkt_get = mkt_sub.add_parser("get", help="Get market details", parents=[parent])
325
+ mkt_get.add_argument("market_id", help="Market ID")
326
+
327
+ mkt_search = mkt_sub.add_parser("search", help="Search markets", parents=[parent])
328
+ mkt_search.add_argument("query", help="Search query")
329
+
330
+ # book
331
+ book_p = sub.add_parser("book", help="Orderbook snapshot", parents=[parent])
332
+ book_p.add_argument("token_id", help="Token ID")
333
+
334
+ # price
335
+ price_p = sub.add_parser("price", help="Latest price", parents=[parent])
336
+ price_p.add_argument("token_id", help="Token ID")
337
+
338
+ # history
339
+ hist_p = sub.add_parser("history", help="Price history", parents=[parent])
340
+ hist_p.add_argument("token_id", help="Token ID")
341
+ hist_p.add_argument("--interval", choices=["1m", "1h", "1d", "1w", "max"], default="1h")
342
+
343
+ # fee-rates
344
+ fee_p = sub.add_parser("fee-rates", help="Fee rates from chain", parents=[parent])
345
+ fee_p.add_argument("token_id", help="Token ID")
346
+
347
+ # order
348
+ ord_p = sub.add_parser("order", help="Order management", parents=[parent])
349
+ ord_sub = ord_p.add_subparsers(dest="order_cmd")
350
+
351
+ ord_place = ord_sub.add_parser("place", help="Place order", parents=[parent])
352
+ ord_place.add_argument("--market-id", required=True, dest="market_id")
353
+ ord_place.add_argument("--token-id", required=True, dest="token_id")
354
+ ord_place.add_argument("--side", required=True, choices=["BUY", "SELL"])
355
+ ord_place.add_argument("--price", required=True)
356
+ ord_place.add_argument("--amount", required=True)
357
+ ord_place.add_argument("--type", choices=["limit", "market"], default="limit")
358
+
359
+ ord_get = ord_sub.add_parser("get", help="Get order details", parents=[parent])
360
+ ord_get.add_argument("order_id", help="Order ID")
361
+
362
+ ord_list = ord_sub.add_parser("list", help="List my orders", parents=[parent])
363
+ ord_list.add_argument("--market-id", dest="market_id", default=None)
364
+ ord_list.add_argument("--status", choices=["pending", "filled", "cancelled"], default=None)
365
+
366
+ ord_cancel = ord_sub.add_parser("cancel", help="Cancel order", parents=[parent])
367
+ ord_cancel.add_argument("order_id", help="Order ID")
368
+
369
+ ord_cancel_all = ord_sub.add_parser("cancel-all", help="Cancel all orders", parents=[parent])
370
+ ord_cancel_all.add_argument("--market-id", dest="market_id", default=None)
371
+
372
+ # positions / trades / balance
373
+ pos_p = sub.add_parser("positions", help="My positions", parents=[parent])
374
+ pos_p.add_argument("--market-id", dest="market_id", default=None)
375
+
376
+ trades_p = sub.add_parser("trades", help="My trades", parents=[parent])
377
+ trades_p.add_argument("--market-id", dest="market_id", default=None)
378
+ trades_p.add_argument("--page", type=int, default=1)
379
+
380
+ sub.add_parser("balance", help="My balances", parents=[parent])
381
+
382
+ # split / merge / redeem
383
+ split_p = sub.add_parser("split", help="Split collateral into outcome tokens", parents=[parent])
384
+ split_p.add_argument("--market-id", required=True, dest="market_id")
385
+ split_p.add_argument("--amount", required=True)
386
+
387
+ merge_p = sub.add_parser("merge", help="Merge outcome tokens into collateral", parents=[parent])
388
+ merge_p.add_argument("--market-id", required=True, dest="market_id")
389
+ merge_p.add_argument("--amount", required=True)
390
+
391
+ redeem_p = sub.add_parser("redeem", help="Redeem resolved market", parents=[parent])
392
+ redeem_p.add_argument("--market-id", required=True, dest="market_id")
393
+
394
+ # ws
395
+ ws_p = sub.add_parser("ws", help="WebSocket streams", parents=[parent])
396
+ ws_sub = ws_p.add_subparsers(dest="ws_cmd")
397
+
398
+ ws_book = ws_sub.add_parser("book", help="Stream orderbook updates", parents=[parent])
399
+ ws_book.add_argument("token_id", help="Token ID")
400
+ ws_book.add_argument("--duration", type=int, default=60, help="Duration in seconds")
401
+
402
+ ws_user = ws_sub.add_parser("user", help="Stream execution reports", parents=[parent])
403
+ ws_user.add_argument("--duration", type=int, default=60, help="Duration in seconds")
404
+
405
+ # report
406
+ rpt_p = sub.add_parser("report", help="Trading reports", parents=[parent])
407
+ rpt_sub = rpt_p.add_subparsers(dest="report_cmd")
408
+
409
+ rpt_daily = rpt_sub.add_parser("daily", help="Daily trading report", parents=[parent])
410
+ rpt_daily.add_argument("--format", choices=["markdown", "json", "both"], default="markdown")
411
+ rpt_daily.add_argument("--output", default=None, help="Output file path")
412
+
413
+ return p
414
+
415
+
416
+ DISPATCH = {
417
+ ("config", "show"): cmd_config_show,
418
+ ("config", "init"): cmd_config_init,
419
+ ("doctor", None): cmd_doctor,
420
+ ("onboard", None): cmd_onboard,
421
+ ("setup", None): cmd_setup,
422
+ ("market", "list"): cmd_market_list,
423
+ ("market", "get"): cmd_market_get,
424
+ ("market", "search"): cmd_market_search,
425
+ ("book", None): cmd_book,
426
+ ("price", None): cmd_price,
427
+ ("history", None): cmd_history,
428
+ ("fee-rates", None): cmd_fee_rates,
429
+ ("order", "place"): cmd_order_place,
430
+ ("order", "get"): cmd_order_get,
431
+ ("order", "list"): cmd_order_list,
432
+ ("order", "cancel"): cmd_order_cancel,
433
+ ("order", "cancel-all"): cmd_order_cancel_all,
434
+ ("positions", None): cmd_positions,
435
+ ("trades", None): cmd_trades,
436
+ ("balance", None): cmd_balance,
437
+ ("split", None): cmd_split,
438
+ ("merge", None): cmd_merge,
439
+ ("redeem", None): cmd_redeem,
440
+ ("ws", "book"): cmd_ws_book,
441
+ ("ws", "user"): cmd_ws_user,
442
+ ("report", "daily"): cmd_report_daily,
443
+ }
444
+
445
+
446
+ def main():
447
+ parser = build_parser()
448
+ args = parser.parse_args()
449
+
450
+ if not args.command:
451
+ parser.print_help()
452
+ sys.exit(1)
453
+
454
+ # Determine sub-command
455
+ sub_cmd = None
456
+ for attr in ("config_cmd", "market_cmd", "order_cmd", "ws_cmd", "report_cmd"):
457
+ if hasattr(args, attr):
458
+ sub_cmd = getattr(args, attr)
459
+ break
460
+
461
+ key = (args.command, sub_cmd)
462
+ fn = DISPATCH.get(key)
463
+ if fn is None:
464
+ # Try command-only dispatch
465
+ fn = DISPATCH.get((args.command, None))
466
+ if fn is None:
467
+ parser.print_help()
468
+ sys.exit(1)
469
+
470
+ fn(args)
471
+
472
+
473
+ if __name__ == "__main__":
474
+ main()