pandora-cli-skills 1.1.41 → 1.1.42
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.
- package/README.md +27 -1
- package/README_FOR_SHARING.md +12 -0
- package/SKILL.md +17 -3
- package/cli/lib/arb_command_service.cjs +336 -0
- package/cli/lib/command_router.cjs +12 -0
- package/cli/lib/connectors/pandora_amm_connector.cjs +200 -0
- package/cli/lib/connectors/polymarket_connector.cjs +154 -0
- package/cli/lib/error_recovery_service.cjs +52 -0
- package/cli/lib/export_service.cjs +20 -0
- package/cli/lib/fork_preview_service.cjs +361 -0
- package/cli/lib/history_service.cjs +2 -0
- package/cli/lib/lifecycle_command_service.cjs +294 -0
- package/cli/lib/lp_command_service.cjs +7 -0
- package/cli/lib/mcp_tool_registry.cjs +27 -0
- package/cli/lib/mirror_handlers/deploy.cjs +7 -0
- package/cli/lib/mirror_handlers/go.cjs +13 -0
- package/cli/lib/mirror_handlers/sync.cjs +7 -0
- package/cli/lib/mirror_sync_service.cjs +3 -1
- package/cli/lib/odds_history_service.cjs +336 -0
- package/cli/lib/parsers/lifecycle_flags.cjs +93 -0
- package/cli/lib/parsers/odds_flags.cjs +154 -0
- package/cli/lib/parsers/risk_flags.cjs +82 -0
- package/cli/lib/parsers/sports_flags.cjs +19 -1
- package/cli/lib/polymarket_command_service.cjs +29 -3
- package/cli/lib/resolve_command_service.cjs +6 -0
- package/cli/lib/risk_command_service.cjs +111 -0
- package/cli/lib/risk_guard_service.cjs +286 -0
- package/cli/lib/risk_state_store.cjs +282 -0
- package/cli/lib/schema_command_service.cjs +27 -0
- package/cli/lib/sports_command_service.cjs +394 -141
- package/cli/lib/sports_creation_service.cjs +39 -3
- package/cli/lib/sports_model_input_service.cjs +98 -0
- package/cli/lib/trade_command_service.cjs +17 -0
- package/cli/lib/venue_connector_factory.cjs +87 -0
- package/cli/pandora.cjs +420 -1
- package/package.json +2 -2
- package/tests/cli/cli.integration.test.cjs +311 -0
- package/tests/cli/mcp.integration.test.cjs +111 -2
- package/tests/cli/sports.integration.test.cjs +139 -4
- package/tests/unit/export_service.test.cjs +87 -0
- package/tests/unit/fork_preview_service.test.cjs +238 -0
- package/tests/unit/new-features.test.cjs +99 -0
- package/tests/unit/odds_history_service.test.cjs +132 -0
- package/tests/unit/risk_guard_service.test.cjs +175 -0
- package/tests/unit/risk_state_store.test.cjs +166 -0
- package/tests/unit/sports_creation.test.cjs +47 -0
- package/tests/unit/venue_connector_factory.test.cjs +67 -0
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ npx pandora-cli-skills@latest --help
|
|
|
27
27
|
- `--fork`, `--fork-rpc-url`, `--fork-chain-id`.
|
|
28
28
|
- Event-driven streaming:
|
|
29
29
|
- `pandora stream prices|events` emits NDJSON lines on stdout.
|
|
30
|
+
- MCP tool surface includes risk/lifecycle/odds command families in addition to core read/trade flows.
|
|
30
31
|
|
|
31
32
|
## Quickstart
|
|
32
33
|
|
|
@@ -63,6 +64,27 @@ pandora --output json sports create plan --event-id <event-id> --selection home
|
|
|
63
64
|
pandora --output json sports resolve plan --event-id <event-id> --poll-address <0x...>
|
|
64
65
|
```
|
|
65
66
|
|
|
67
|
+
### Lifecycle Quickstart
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# lifecycle config must be JSON object (not YAML in current release)
|
|
71
|
+
pandora --output json lifecycle start --config ./configs/lifecycle.json
|
|
72
|
+
pandora --output json lifecycle status --id <lifecycle-id>
|
|
73
|
+
pandora --output json lifecycle resolve --id <lifecycle-id> --confirm
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Risk Controls
|
|
77
|
+
|
|
78
|
+
- Inspect/engage panic lock:
|
|
79
|
+
- `pandora --output json risk show`
|
|
80
|
+
- `pandora --output json risk panic --reason "incident"`
|
|
81
|
+
- `pandora --output json risk panic --clear`
|
|
82
|
+
- Current guardrail semantics:
|
|
83
|
+
- `max_position_usd` guards per-operation notional.
|
|
84
|
+
- `max_daily_loss_usd` currently maps to daily live notional cap (`counters.liveNotionalUsdc`).
|
|
85
|
+
- `max_open_markets` currently maps to daily live operation cap (`counters.liveOps`).
|
|
86
|
+
- All risk state and panic files are persisted with hardened permissions under `~/.pandora/`.
|
|
87
|
+
|
|
66
88
|
## Fork Mode Notes
|
|
67
89
|
|
|
68
90
|
- Runtime marker is included in payloads: `data.runtime.mode = "fork" | "live"`.
|
|
@@ -85,21 +107,25 @@ pandora --output json sports resolve plan --event-id <event-id> --poll-address <
|
|
|
85
107
|
- `pandora markets list|get`
|
|
86
108
|
- `pandora sports books list`
|
|
87
109
|
- `pandora sports events list|live`
|
|
88
|
-
- `pandora sports odds snapshot`
|
|
110
|
+
- `pandora sports odds snapshot|bulk`
|
|
89
111
|
- `pandora sports consensus`
|
|
90
112
|
- `pandora sports create plan|run`
|
|
91
113
|
- `pandora sports sync once|run|start|stop|status`
|
|
92
114
|
- `pandora sports resolve plan`
|
|
115
|
+
- `pandora lifecycle start|status|resolve`
|
|
93
116
|
- `pandora quote`
|
|
94
117
|
- `pandora trade`
|
|
95
118
|
- `pandora history`
|
|
96
119
|
- `pandora export`
|
|
97
120
|
- `pandora arbitrage`
|
|
121
|
+
- `pandora arb scan --output ndjson`
|
|
122
|
+
- `pandora odds record|history`
|
|
98
123
|
- `pandora autopilot run|once`
|
|
99
124
|
- `pandora mirror browse|plan|deploy|verify|lp-explain|hedge-calc|simulate|go|sync|status|close`
|
|
100
125
|
- `pandora polymarket check|approve|preflight|trade`
|
|
101
126
|
- `pandora resolve`
|
|
102
127
|
- `pandora lp add|remove|positions`
|
|
128
|
+
- `pandora risk show|panic`
|
|
103
129
|
- `pandora stream prices|events`
|
|
104
130
|
- `pandora schema`
|
|
105
131
|
- `pandora mcp`
|
package/README_FOR_SHARING.md
CHANGED
|
@@ -61,6 +61,17 @@ Prerequisite: Node.js `>=18`.
|
|
|
61
61
|
- Build manual resolve recommendation:
|
|
62
62
|
- `pandora --output json sports resolve plan --event-id <event-id> --poll-address <0x...>`
|
|
63
63
|
|
|
64
|
+
## Quickstart (Lifecycle + Risk)
|
|
65
|
+
- Lifecycle config must be JSON in this release:
|
|
66
|
+
- `pandora --output json lifecycle start --config ./configs/lifecycle.json`
|
|
67
|
+
- `pandora --output json lifecycle status --id <lifecycle-id>`
|
|
68
|
+
- `pandora --output json lifecycle resolve --id <lifecycle-id> --confirm`
|
|
69
|
+
- Risk controls:
|
|
70
|
+
- `pandora --output json risk show`
|
|
71
|
+
- `pandora --output json risk panic --reason "incident"`
|
|
72
|
+
- `pandora --output json risk panic --clear`
|
|
73
|
+
- semantic note: `max_daily_loss_usd` and `max_open_markets` are enforced as daily live-notional / daily operation counters in current implementation.
|
|
74
|
+
|
|
64
75
|
## New CLI capabilities
|
|
65
76
|
- Global machine-readable output:
|
|
66
77
|
- `pandora --output json doctor`
|
|
@@ -130,6 +141,7 @@ Prerequisite: Node.js `>=18`.
|
|
|
130
141
|
### MCP server (`pandora mcp`)
|
|
131
142
|
- Runs MCP server over stdio with tool discovery + execution.
|
|
132
143
|
- `tools/list` includes JSON-capable command tools (for example `markets.list`, `trade`, `mirror.plan`, `polymarket.check`).
|
|
144
|
+
- Also includes `odds.history|record` and `lifecycle.status|start|resolve` tools.
|
|
133
145
|
- `launch` and `clone-bet` are intentionally not exposed over MCP because they stream script output.
|
|
134
146
|
- MCP safety rails:
|
|
135
147
|
- mutating tools require explicit execute intent (`intent.execute=true`) for live execution.
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pandora-cli-skills
|
|
3
3
|
summary: Canonical skill and operator guide for Pandora CLI including mirror, polymarket, resolve, and LP flows.
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.42
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Pandora CLI & Skills
|
|
@@ -72,6 +72,14 @@ npm link
|
|
|
72
72
|
- RPC reachability and chain id match
|
|
73
73
|
- bytecode checks for `ORACLE` + `FACTORY` (`--check-usdc-code` optional)
|
|
74
74
|
|
|
75
|
+
## Lifecycle and risk notes
|
|
76
|
+
- `lifecycle start --config` expects a JSON object file in the current release.
|
|
77
|
+
- `risk show|panic` controls a global execution lock used by live write paths.
|
|
78
|
+
- Current risk-counter semantics:
|
|
79
|
+
- `max_position_usd` guards per-operation notional.
|
|
80
|
+
- `max_daily_loss_usd` is enforced as daily live notional (`counters.liveNotionalUsdc`).
|
|
81
|
+
- `max_open_markets` is enforced as daily live operation count (`counters.liveOps`).
|
|
82
|
+
|
|
75
83
|
## Complete command + flag reference (authoritative)
|
|
76
84
|
This section mirrors live CLI help output so agent runs can rely on one source of truth.
|
|
77
85
|
|
|
@@ -83,7 +91,11 @@ pandora [--output table|json] doctor [--dotenv-path <path>] [--skip-dotenv] [--c
|
|
|
83
91
|
pandora [--output table|json] setup [--force] [--dotenv-path <path>] [--example <path>] [--check-usdc-code] [--check-polymarket] [--rpc-timeout-ms <ms>]
|
|
84
92
|
pandora [--output table|json] markets list [--limit <n>] [--after <cursor>] [--before <cursor>] [--order-by <field>] [--order-direction asc|desc] [--chain-id <id>] [--creator <address>] [--poll-address <address>] [--market-type <type>] [--where-json <json>] [--active|--resolved|--expiring-soon] [--expiring-hours <n>] [--expand] [--with-odds]
|
|
85
93
|
pandora [--output table|json] markets get [--id <id> ...] [--stdin]
|
|
86
|
-
pandora [--output table|json] sports books list|events list|events live|odds snapshot|consensus|create plan|create run|sync once|sync run|sync start|sync stop|sync status|resolve plan [flags]
|
|
94
|
+
pandora [--output table|json] sports books list|events list|events live|odds snapshot|odds bulk|consensus|create plan|create run|sync once|sync run|sync start|sync stop|sync status|resolve plan [flags]
|
|
95
|
+
pandora [--output table|json] lifecycle start --config <path>|status --id <id>|resolve --id <id> --confirm
|
|
96
|
+
pandora arb scan --markets <csv> --output ndjson [--min-net-spread-pct <n>] [--fee-pct-per-leg <n>] [--amount-usdc <n>] [--iterations <n>] [--interval-ms <ms>]
|
|
97
|
+
pandora [--output table|json] odds record --competition <id> --interval <sec> [--max-samples <n>] [--event-id <id>] [--venues pandora_amm,polymarket]
|
|
98
|
+
pandora [--output table|json] odds history --event-id <id> --output csv|json [--limit <n>]
|
|
87
99
|
pandora [--output table|json] polls list [--limit <n>] [--after <cursor>] [--before <cursor>] [--order-by <field>] [--order-direction asc|desc] [--chain-id <id>] [--creator <address>] [--status <int>] [--category <int>] [--question-contains <text>] [--where-json <json>]
|
|
88
100
|
pandora [--output table|json] polls get --id <id>
|
|
89
101
|
pandora [--output table|json] events list [--type all|liquidity|oracle-fee|claim] [--limit <n>] [--after <cursor>] [--before <cursor>] [--order-direction asc|desc] [--chain-id <id>] [--wallet <address>] [--market-address <address>] [--poll-address <address>] [--tx-hash <hash>]
|
|
@@ -106,6 +118,7 @@ pandora [--output table|json] analyze --market-address <address> [--provider <na
|
|
|
106
118
|
pandora [--output table|json] suggest --wallet <address> --risk low|medium|high --budget <amount> [--count <n>] [--include-venues pandora,polymarket]
|
|
107
119
|
pandora [--output table|json] resolve --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]
|
|
108
120
|
pandora [--output table|json] lp add|remove|positions [--market-address <address>] [--wallet <address>] [--amount-usdc <n>] [--lp-tokens <n>] [--dry-run|--execute] [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--usdc <address>] [--deadline-seconds <n>] [--indexer-url <url>] [--timeout-ms <ms>]
|
|
121
|
+
pandora [--output table|json] risk show|panic [--risk-file <path>] [--clear] [--reason <text>] [--actor <id>]
|
|
109
122
|
pandora stream prices|events [--indexer-url <url>] [--indexer-ws-url <url>] [--timeout-ms <ms>] [--interval-ms <ms>] [--market-address <address>] [--chain-id <id>] [--limit <n>]
|
|
110
123
|
pandora [--output json] schema
|
|
111
124
|
pandora mcp
|
|
@@ -152,9 +165,10 @@ trade --condition-id <id>|--slug <slug>|--token-id <id> --token yes|no --amount-
|
|
|
152
165
|
| `sports events list` | List normalized soccer events. | `--competition`, `--kickoff-after`, `--kickoff-before`, `--limit` |
|
|
153
166
|
| `sports events live` | List only live/in-play events. | `--competition`, `--limit`, `--provider` |
|
|
154
167
|
| `sports odds snapshot` | Fetch event odds snapshot plus consensus context. | `--event-id`, `--trim-percent`, `--min-tier1-books`, `--min-total-books` |
|
|
168
|
+
| `sports odds bulk` | Fetch all odds for a competition and refresh local cache. | `--competition`, `--provider`, `--timeout-ms`, `--limit` |
|
|
155
169
|
| `sports consensus` | Compute trimmed-median consensus from live or offline checks. | `--event-id` or `--checks-json`, `--trim-percent`, `--book-priority` |
|
|
156
170
|
| `sports create plan` | Build conservative creation plan and safety gates. | `--event-id`, `--selection`, `--market-type`, `--creation-window-open-min`, `--creation-window-close-min` |
|
|
157
|
-
| `sports create run` | Dry-run or execute creation path. | `--event-id`, `--dry-run/--execute`, `--liquidity-usdc`, `--chain-id`, `--rpc-url` |
|
|
171
|
+
| `sports create run` | Dry-run or execute creation path. | `--event-id`, `--dry-run/--execute`, `--liquidity-usdc`, `--chain-id`, `--rpc-url`, `--model-file`, `--model-stdin` |
|
|
158
172
|
| `sports sync once|run|start|stop|status` | Evaluate and operate sports sync runtime state. | `--event-id` (required for `once|run|start`), `--risk-profile`, `--state-file`, `--paper/--execute-live` |
|
|
159
173
|
| `sports resolve plan` | Build manual-final resolution recommendation. | `--event-id` or `--checks-json/--checks-file`, `--poll-address`, `--settle-delay-ms`, `--consecutive-checks-required` |
|
|
160
174
|
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
const ARB_MARKET_FIELDS = [
|
|
2
|
+
'id',
|
|
3
|
+
'chainId',
|
|
4
|
+
'chainName',
|
|
5
|
+
'pollAddress',
|
|
6
|
+
'marketCloseTimestamp',
|
|
7
|
+
'yesChance',
|
|
8
|
+
'yesPct',
|
|
9
|
+
'reserveYes',
|
|
10
|
+
'reserveNo',
|
|
11
|
+
'totalVolume',
|
|
12
|
+
'currentTvl',
|
|
13
|
+
'createdAt',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const ARB_USAGE =
|
|
17
|
+
'pandora arb scan --markets <csv> --output ndjson [--min-net-spread-pct <n>] [--fee-pct-per-leg <n>] [--amount-usdc <n>] [--interval-ms <ms>] [--iterations <n>] [--indexer-url <url>] [--timeout-ms <ms>]';
|
|
18
|
+
|
|
19
|
+
function requireDep(deps, name) {
|
|
20
|
+
if (!deps || typeof deps[name] !== 'function') {
|
|
21
|
+
throw new Error(`createRunArbCommand requires deps.${name}()`);
|
|
22
|
+
}
|
|
23
|
+
return deps[name];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function roundNumber(value, digits = 6) {
|
|
27
|
+
const multiplier = 10 ** digits;
|
|
28
|
+
return Math.round(Number(value) * multiplier) / multiplier;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toFiniteNumber(value) {
|
|
32
|
+
const parsed = Number(value);
|
|
33
|
+
if (!Number.isFinite(parsed)) return null;
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeYesPct(market) {
|
|
38
|
+
if (!market || typeof market !== 'object') return null;
|
|
39
|
+
|
|
40
|
+
const explicit = toFiniteNumber(market.yesPct);
|
|
41
|
+
if (explicit !== null) {
|
|
42
|
+
if (explicit >= 0 && explicit <= 100) {
|
|
43
|
+
return explicit;
|
|
44
|
+
}
|
|
45
|
+
if (explicit >= 0 && explicit <= 1) {
|
|
46
|
+
return explicit * 100;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const chance = toFiniteNumber(market.yesChance);
|
|
51
|
+
if (chance !== null) {
|
|
52
|
+
if (chance >= 0 && chance <= 1) {
|
|
53
|
+
return chance * 100;
|
|
54
|
+
}
|
|
55
|
+
if (chance >= 0 && chance <= 100) {
|
|
56
|
+
return chance;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const reserveYes = toFiniteNumber(market.reserveYes);
|
|
61
|
+
const reserveNo = toFiniteNumber(market.reserveNo);
|
|
62
|
+
if (reserveYes !== null && reserveNo !== null) {
|
|
63
|
+
const total = reserveYes + reserveNo;
|
|
64
|
+
if (total > 0) {
|
|
65
|
+
return (reserveNo / total) * 100;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildMarketSnapshots(markets, orderedIds) {
|
|
73
|
+
const byId = new Map((Array.isArray(markets) ? markets : []).map((item) => [String(item && item.id), item]));
|
|
74
|
+
|
|
75
|
+
return orderedIds
|
|
76
|
+
.map((id) => {
|
|
77
|
+
const market = byId.get(id) || null;
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
market,
|
|
81
|
+
yesPct: market ? normalizeYesPct(market) : null,
|
|
82
|
+
};
|
|
83
|
+
})
|
|
84
|
+
.filter((item) => item.market && Number.isFinite(item.yesPct));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build deterministic pairwise arbitrage opportunities across provided markets.
|
|
89
|
+
* @param {object} options
|
|
90
|
+
* @returns {object[]}
|
|
91
|
+
*/
|
|
92
|
+
function buildArbOpportunities(options) {
|
|
93
|
+
const snapshots = Array.isArray(options.marketSnapshots) ? options.marketSnapshots : [];
|
|
94
|
+
const minNetSpreadPct = Number.isFinite(options.minNetSpreadPct) ? Number(options.minNetSpreadPct) : 0;
|
|
95
|
+
const feePctPerLeg = Number.isFinite(options.feePctPerLeg) ? Number(options.feePctPerLeg) : 0;
|
|
96
|
+
const amountUsdc = Number.isFinite(options.amountUsdc) ? Number(options.amountUsdc) : 0;
|
|
97
|
+
|
|
98
|
+
const opportunities = [];
|
|
99
|
+
for (let leftIndex = 0; leftIndex < snapshots.length; leftIndex += 1) {
|
|
100
|
+
for (let rightIndex = leftIndex + 1; rightIndex < snapshots.length; rightIndex += 1) {
|
|
101
|
+
const left = snapshots[leftIndex];
|
|
102
|
+
const right = snapshots[rightIndex];
|
|
103
|
+
if (!Number.isFinite(left.yesPct) || !Number.isFinite(right.yesPct)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const buyYes = left.yesPct <= right.yesPct ? left : right;
|
|
108
|
+
const buyNo = buyYes === left ? right : left;
|
|
109
|
+
const grossSpreadPct = roundNumber(buyNo.yesPct - buyYes.yesPct, 6);
|
|
110
|
+
const netSpreadPct = roundNumber(grossSpreadPct - feePctPerLeg * 2, 6);
|
|
111
|
+
if (netSpreadPct <= 0 || netSpreadPct < minNetSpreadPct) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const profitUsdc = roundNumber((amountUsdc * netSpreadPct) / 100, 6);
|
|
116
|
+
opportunities.push({
|
|
117
|
+
pair: `${buyYes.id}|${buyNo.id}`,
|
|
118
|
+
buyYesMarket: buyYes.id,
|
|
119
|
+
buyNoMarket: buyNo.id,
|
|
120
|
+
buyYesPct: roundNumber(buyYes.yesPct, 6),
|
|
121
|
+
buyNoPct: roundNumber(buyNo.yesPct, 6),
|
|
122
|
+
grossSpreadPct,
|
|
123
|
+
netSpreadPct,
|
|
124
|
+
netSpread: roundNumber(netSpreadPct / 100, 8),
|
|
125
|
+
amountUsdc: roundNumber(amountUsdc, 6),
|
|
126
|
+
profitUsdc,
|
|
127
|
+
profit: profitUsdc,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
opportunities.sort((left, right) => {
|
|
133
|
+
if (right.netSpreadPct !== left.netSpreadPct) {
|
|
134
|
+
return right.netSpreadPct - left.netSpreadPct;
|
|
135
|
+
}
|
|
136
|
+
return left.pair.localeCompare(right.pair);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return opportunities;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Parse `arb scan` command flags.
|
|
144
|
+
* @param {string[]} args
|
|
145
|
+
* @param {object} deps
|
|
146
|
+
* @returns {object}
|
|
147
|
+
*/
|
|
148
|
+
function parseArbScanFlags(args, deps) {
|
|
149
|
+
const { CliError, requireFlagValue, parseCsvList, parseNumber, parsePositiveNumber, parsePositiveInteger } = deps;
|
|
150
|
+
|
|
151
|
+
const action = args[0];
|
|
152
|
+
if (!action || action !== 'scan') {
|
|
153
|
+
throw new CliError('INVALID_ARGS', 'arb requires subcommand: scan.');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const options = {
|
|
157
|
+
action,
|
|
158
|
+
markets: [],
|
|
159
|
+
output: 'ndjson',
|
|
160
|
+
minNetSpreadPct: 0,
|
|
161
|
+
feePctPerLeg: 0,
|
|
162
|
+
amountUsdc: 100,
|
|
163
|
+
intervalMs: 5_000,
|
|
164
|
+
iterations: null,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const rest = args.slice(1);
|
|
168
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
169
|
+
const token = rest[i];
|
|
170
|
+
if (token === '--markets') {
|
|
171
|
+
options.markets = parseCsvList(requireFlagValue(rest, i, '--markets'), '--markets');
|
|
172
|
+
i += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (token === '--output') {
|
|
176
|
+
options.output = String(requireFlagValue(rest, i, '--output')).trim().toLowerCase();
|
|
177
|
+
i += 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (token === '--min-net-spread-pct') {
|
|
181
|
+
options.minNetSpreadPct = parseNumber(requireFlagValue(rest, i, '--min-net-spread-pct'), '--min-net-spread-pct');
|
|
182
|
+
i += 1;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (token === '--fee-pct-per-leg') {
|
|
186
|
+
options.feePctPerLeg = parseNumber(requireFlagValue(rest, i, '--fee-pct-per-leg'), '--fee-pct-per-leg');
|
|
187
|
+
i += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (token === '--amount-usdc') {
|
|
191
|
+
options.amountUsdc = parsePositiveNumber(requireFlagValue(rest, i, '--amount-usdc'), '--amount-usdc');
|
|
192
|
+
i += 1;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (token === '--interval-ms') {
|
|
196
|
+
options.intervalMs = parsePositiveInteger(requireFlagValue(rest, i, '--interval-ms'), '--interval-ms');
|
|
197
|
+
i += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (token === '--iterations') {
|
|
201
|
+
options.iterations = parsePositiveInteger(requireFlagValue(rest, i, '--iterations'), '--iterations');
|
|
202
|
+
i += 1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw new CliError('UNKNOWN_FLAG', `Unknown flag for arb scan: ${token}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!Array.isArray(options.markets) || options.markets.length < 2) {
|
|
210
|
+
throw new CliError('MISSING_REQUIRED_FLAG', 'arb scan requires at least two markets via --markets <csv>.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (options.output !== 'ndjson') {
|
|
214
|
+
throw new CliError('INVALID_FLAG_VALUE', 'arb scan currently supports only --output ndjson.');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (options.minNetSpreadPct < 0) {
|
|
218
|
+
throw new CliError('INVALID_FLAG_VALUE', '--min-net-spread-pct must be >= 0.');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (options.feePctPerLeg < 0) {
|
|
222
|
+
throw new CliError('INVALID_FLAG_VALUE', '--fee-pct-per-leg must be >= 0.');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
options.markets = Array.from(
|
|
226
|
+
new Set(
|
|
227
|
+
options.markets
|
|
228
|
+
.map((item) => String(item).trim())
|
|
229
|
+
.filter(Boolean),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (options.markets.length < 2) {
|
|
234
|
+
throw new CliError('INVALID_ARGS', 'arb scan requires at least two distinct market ids.');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return options;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function fetchMarketsById(indexerUrl, marketIds, timeoutMs, deps) {
|
|
241
|
+
const query = deps.buildGraphqlGetQuery('markets', ARB_MARKET_FIELDS);
|
|
242
|
+
const requests = marketIds.map(async (id) => {
|
|
243
|
+
const data = await deps.graphqlRequest(indexerUrl, query, { id }, timeoutMs);
|
|
244
|
+
return data && data.markets ? data.markets : null;
|
|
245
|
+
});
|
|
246
|
+
return Promise.all(requests);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Create runner for `pandora arb` commands.
|
|
251
|
+
* @param {object} deps
|
|
252
|
+
* @returns {(args: string[], context: {outputMode: 'table'|'json'}) => Promise<void>}
|
|
253
|
+
*/
|
|
254
|
+
function createRunArbCommand(deps) {
|
|
255
|
+
const CliError = requireDep(deps, 'CliError');
|
|
256
|
+
const includesHelpFlag = requireDep(deps, 'includesHelpFlag');
|
|
257
|
+
const emitSuccess = requireDep(deps, 'emitSuccess');
|
|
258
|
+
const commandHelpPayload = requireDep(deps, 'commandHelpPayload');
|
|
259
|
+
const parseIndexerSharedFlags = requireDep(deps, 'parseIndexerSharedFlags');
|
|
260
|
+
const maybeLoadIndexerEnv = requireDep(deps, 'maybeLoadIndexerEnv');
|
|
261
|
+
const resolveIndexerUrl = requireDep(deps, 'resolveIndexerUrl');
|
|
262
|
+
const requireFlagValue = requireDep(deps, 'requireFlagValue');
|
|
263
|
+
const parseCsvList = requireDep(deps, 'parseCsvList');
|
|
264
|
+
const parseNumber = requireDep(deps, 'parseNumber');
|
|
265
|
+
const parsePositiveNumber = requireDep(deps, 'parsePositiveNumber');
|
|
266
|
+
const parsePositiveInteger = requireDep(deps, 'parsePositiveInteger');
|
|
267
|
+
const buildGraphqlGetQuery = requireDep(deps, 'buildGraphqlGetQuery');
|
|
268
|
+
const graphqlRequest = requireDep(deps, 'graphqlRequest');
|
|
269
|
+
const sleepMs = requireDep(deps, 'sleepMs');
|
|
270
|
+
|
|
271
|
+
return async function runArbCommand(args, context) {
|
|
272
|
+
const shared = parseIndexerSharedFlags(args);
|
|
273
|
+
if (!shared.rest.length || includesHelpFlag(shared.rest)) {
|
|
274
|
+
if (context.outputMode === 'json') {
|
|
275
|
+
emitSuccess(context.outputMode, 'arb.help', commandHelpPayload(ARB_USAGE));
|
|
276
|
+
} else {
|
|
277
|
+
// eslint-disable-next-line no-console
|
|
278
|
+
console.log(`Usage: ${ARB_USAGE}`);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
maybeLoadIndexerEnv(shared);
|
|
284
|
+
const indexerUrl = resolveIndexerUrl(shared.indexerUrl);
|
|
285
|
+
const options = parseArbScanFlags(shared.rest, {
|
|
286
|
+
CliError,
|
|
287
|
+
requireFlagValue,
|
|
288
|
+
parseCsvList,
|
|
289
|
+
parseNumber,
|
|
290
|
+
parsePositiveNumber,
|
|
291
|
+
parsePositiveInteger,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const maxIterations = Number.isInteger(options.iterations) ? options.iterations : Number.POSITIVE_INFINITY;
|
|
295
|
+
let iteration = 0;
|
|
296
|
+
|
|
297
|
+
while (iteration < maxIterations) {
|
|
298
|
+
iteration += 1;
|
|
299
|
+
const markets = await fetchMarketsById(indexerUrl, options.markets, shared.timeoutMs, {
|
|
300
|
+
buildGraphqlGetQuery,
|
|
301
|
+
graphqlRequest,
|
|
302
|
+
});
|
|
303
|
+
const snapshots = buildMarketSnapshots(markets, options.markets);
|
|
304
|
+
const opportunities = buildArbOpportunities({
|
|
305
|
+
marketSnapshots: snapshots,
|
|
306
|
+
minNetSpreadPct: options.minNetSpreadPct,
|
|
307
|
+
feePctPerLeg: options.feePctPerLeg,
|
|
308
|
+
amountUsdc: options.amountUsdc,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
for (const opportunity of opportunities) {
|
|
312
|
+
// eslint-disable-next-line no-console
|
|
313
|
+
console.log(
|
|
314
|
+
JSON.stringify({
|
|
315
|
+
type: 'arb.scan.opportunity',
|
|
316
|
+
timestamp: new Date().toISOString(),
|
|
317
|
+
iteration,
|
|
318
|
+
indexerUrl,
|
|
319
|
+
...opportunity,
|
|
320
|
+
}),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (iteration < maxIterations) {
|
|
325
|
+
await sleepMs(options.intervalMs);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
module.exports = {
|
|
332
|
+
ARB_USAGE,
|
|
333
|
+
buildArbOpportunities,
|
|
334
|
+
createRunArbCommand,
|
|
335
|
+
parseArbScanFlags,
|
|
336
|
+
};
|
|
@@ -19,6 +19,9 @@ function createCommandRouter(deps = {}) {
|
|
|
19
19
|
runMarketsCommand,
|
|
20
20
|
runScanCommand,
|
|
21
21
|
runSportsCommand,
|
|
22
|
+
runLifecycleCommand,
|
|
23
|
+
runArbCommand,
|
|
24
|
+
runOddsCommand,
|
|
22
25
|
runQuoteCommand,
|
|
23
26
|
runTradeCommand,
|
|
24
27
|
runPollsCommand,
|
|
@@ -38,6 +41,7 @@ function createCommandRouter(deps = {}) {
|
|
|
38
41
|
runSuggestCommand,
|
|
39
42
|
runResolveCommand,
|
|
40
43
|
runLpCommand,
|
|
44
|
+
runRiskCommand,
|
|
41
45
|
runMcpCommand,
|
|
42
46
|
runStreamCommand,
|
|
43
47
|
runScriptCommand,
|
|
@@ -67,6 +71,9 @@ function createCommandRouter(deps = {}) {
|
|
|
67
71
|
requireFn('runMarketsCommand', runMarketsCommand);
|
|
68
72
|
requireFn('runScanCommand', runScanCommand);
|
|
69
73
|
requireFn('runSportsCommand', runSportsCommand);
|
|
74
|
+
requireFn('runLifecycleCommand', runLifecycleCommand);
|
|
75
|
+
requireFn('runArbCommand', runArbCommand);
|
|
76
|
+
requireFn('runOddsCommand', runOddsCommand);
|
|
70
77
|
requireFn('runQuoteCommand', runQuoteCommand);
|
|
71
78
|
requireFn('runTradeCommand', runTradeCommand);
|
|
72
79
|
requireFn('runPollsCommand', runPollsCommand);
|
|
@@ -86,6 +93,7 @@ function createCommandRouter(deps = {}) {
|
|
|
86
93
|
requireFn('runSuggestCommand', runSuggestCommand);
|
|
87
94
|
requireFn('runResolveCommand', runResolveCommand);
|
|
88
95
|
requireFn('runLpCommand', runLpCommand);
|
|
96
|
+
requireFn('runRiskCommand', runRiskCommand);
|
|
89
97
|
requireFn('runMcpCommand', runMcpCommand);
|
|
90
98
|
requireFn('runStreamCommand', runStreamCommand);
|
|
91
99
|
requireFn('runSchemaCommand', deps.runSchemaCommand);
|
|
@@ -156,6 +164,9 @@ function createCommandRouter(deps = {}) {
|
|
|
156
164
|
markets: async (handlerArgs, handlerContext) => runMarketsCommand(handlerArgs, handlerContext),
|
|
157
165
|
scan: async (handlerArgs, handlerContext) => runScanCommand(handlerArgs, handlerContext),
|
|
158
166
|
sports: async (handlerArgs, handlerContext) => runSportsCommand(handlerArgs, handlerContext),
|
|
167
|
+
lifecycle: async (handlerArgs, handlerContext) => runLifecycleCommand(handlerArgs, handlerContext),
|
|
168
|
+
arb: async (handlerArgs, handlerContext) => runArbCommand(handlerArgs, handlerContext),
|
|
169
|
+
odds: async (handlerArgs, handlerContext) => runOddsCommand(handlerArgs, handlerContext),
|
|
159
170
|
quote: async (handlerArgs, handlerContext) => runQuoteCommand(handlerArgs, handlerContext),
|
|
160
171
|
trade: async (handlerArgs, handlerContext) => runTradeCommand(handlerArgs, handlerContext),
|
|
161
172
|
polls: async (handlerArgs, handlerContext) => runPollsCommand(handlerArgs, handlerContext),
|
|
@@ -175,6 +186,7 @@ function createCommandRouter(deps = {}) {
|
|
|
175
186
|
suggest: async (handlerArgs, handlerContext) => runSuggestCommand(handlerArgs, handlerContext),
|
|
176
187
|
resolve: async (handlerArgs, handlerContext) => runResolveCommand(handlerArgs, handlerContext),
|
|
177
188
|
lp: async (handlerArgs, handlerContext) => runLpCommand(handlerArgs, handlerContext),
|
|
189
|
+
risk: async (handlerArgs, handlerContext) => runRiskCommand(handlerArgs, handlerContext),
|
|
178
190
|
mcp: async (handlerArgs, handlerContext) => {
|
|
179
191
|
if (handlerContext.outputMode === 'json') {
|
|
180
192
|
throw new CliError(
|