pandora-cli-skills 1.1.56 → 1.1.57

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/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.56
4
+ version: 1.1.57
5
5
  ---
6
6
 
7
7
  # Pandora CLI & Skills
@@ -14,7 +14,7 @@ const ARB_MARKET_FIELDS = [
14
14
  ];
15
15
 
16
16
  const ARB_USAGE =
17
- 'pandora arb scan [--source pandora|polymarket] [--markets <csv>] --output ndjson|json [--limit <n>] [--min-net-spread-pct <n>|--min-spread-pct <n>] [--min-tvl <usdc>] [--fee-pct-per-leg <n>] [--slippage-pct-per-leg <n>] [--amount-usdc <n>] [--combinatorial] [--max-bundle-size <n>] [--interval-ms <ms>] [--iterations <n>] [--indexer-url <url>] [--timeout-ms <ms>]';
17
+ 'pandora arb scan [--source pandora|polymarket] [--markets <csv>] --output ndjson|json [--limit <n>] [--min-net-spread-pct <n>|--min-spread-pct <n>] [--min-tvl <usdc>] [--fee-pct-per-leg <n>] [--slippage-pct-per-leg <n>] [--amount-usdc <n>] [--combinatorial] [--max-bundle-size <n>] [--similarity-threshold <0-1>] [--min-token-score <0-1>] [--max-close-diff-hours <n>] [--question-contains <text>] [--interval-ms <ms>] [--iterations <n>] [--indexer-url <url>] [--timeout-ms <ms>]';
18
18
 
19
19
  function requireDep(deps, name) {
20
20
  if (!deps || typeof deps[name] !== 'function') {
@@ -340,6 +340,10 @@ function parseArbScanFlags(args, deps) {
340
340
  amountUsdc: 100,
341
341
  combinatorial: false,
342
342
  maxBundleSize: 4,
343
+ similarityThreshold: 0.35,
344
+ minTokenScore: 0.12,
345
+ maxCloseDiffHours: 24,
346
+ questionContains: null,
343
347
  intervalMs: 5_000,
344
348
  iterations: null,
345
349
  };
@@ -406,6 +410,29 @@ function parseArbScanFlags(args, deps) {
406
410
  i += 1;
407
411
  continue;
408
412
  }
413
+ if (token === '--similarity-threshold') {
414
+ options.similarityThreshold = parseNumber(requireFlagValue(rest, i, '--similarity-threshold'), '--similarity-threshold');
415
+ i += 1;
416
+ continue;
417
+ }
418
+ if (token === '--min-token-score') {
419
+ options.minTokenScore = parseNumber(requireFlagValue(rest, i, '--min-token-score'), '--min-token-score');
420
+ i += 1;
421
+ continue;
422
+ }
423
+ if (token === '--max-close-diff-hours') {
424
+ options.maxCloseDiffHours = parsePositiveNumber(
425
+ requireFlagValue(rest, i, '--max-close-diff-hours'),
426
+ '--max-close-diff-hours',
427
+ );
428
+ i += 1;
429
+ continue;
430
+ }
431
+ if (token === '--question-contains') {
432
+ options.questionContains = String(requireFlagValue(rest, i, '--question-contains')).trim();
433
+ i += 1;
434
+ continue;
435
+ }
409
436
  if (token === '--interval-ms') {
410
437
  options.intervalMs = parsePositiveInteger(requireFlagValue(rest, i, '--interval-ms'), '--interval-ms');
411
438
  i += 1;
@@ -443,6 +470,12 @@ function parseArbScanFlags(args, deps) {
443
470
  if (options.slippagePctPerLeg < 0) {
444
471
  throw new CliError('INVALID_FLAG_VALUE', '--slippage-pct-per-leg must be >= 0.');
445
472
  }
473
+ if (options.similarityThreshold < 0 || options.similarityThreshold > 1) {
474
+ throw new CliError('INVALID_FLAG_VALUE', '--similarity-threshold must be between 0 and 1.');
475
+ }
476
+ if (options.minTokenScore < 0 || options.minTokenScore > 1) {
477
+ throw new CliError('INVALID_FLAG_VALUE', '--min-token-score must be between 0 and 1.');
478
+ }
446
479
 
447
480
  if (!Number.isInteger(options.maxBundleSize) || options.maxBundleSize < 3) {
448
481
  throw new CliError('INVALID_FLAG_VALUE', '--max-bundle-size must be an integer >= 3.');
@@ -559,13 +592,13 @@ function createRunArbCommand(deps) {
559
592
  limit: Math.max(options.limit * 4, 100),
560
593
  minSpreadPct: options.minNetSpreadPct,
561
594
  minLiquidityUsd: options.minTvlUsdc,
562
- maxCloseDiffHours: 24,
563
- similarityThreshold: 0.86,
564
- minTokenScore: 0.12,
595
+ maxCloseDiffHours: options.maxCloseDiffHours,
596
+ similarityThreshold: options.similarityThreshold,
597
+ minTokenScore: options.minTokenScore,
565
598
  crossVenueOnly: true,
566
599
  withRules: false,
567
600
  includeSimilarity: false,
568
- questionContains: null,
601
+ questionContains: options.questionContains,
569
602
  });
570
603
  pairwiseOpportunities = buildCrossVenueArbOpportunities(crossVenuePayload, options);
571
604
  combinatorialOpportunities = [];
@@ -653,6 +686,10 @@ function createRunArbCommand(deps) {
653
686
  amountUsdc: options.amountUsdc,
654
687
  combinatorial: options.combinatorial,
655
688
  maxBundleSize: options.maxBundleSize,
689
+ similarityThreshold: options.similarityThreshold,
690
+ minTokenScore: options.minTokenScore,
691
+ maxCloseDiffHours: options.maxCloseDiffHours,
692
+ questionContains: options.questionContains,
656
693
  },
657
694
  opportunities: iterationSnapshots.flatMap((row) => row.opportunities),
658
695
  snapshots: iterationSnapshots,
package/cli/pandora.cjs CHANGED
@@ -3602,7 +3602,8 @@ async function filterHedgeableMarkets({ indexerUrl, timeoutMs, options, items })
3602
3602
  minSpreadPct: 0,
3603
3603
  minLiquidityUsd: 0,
3604
3604
  maxCloseDiffHours: 24,
3605
- similarityThreshold: 0.86,
3605
+ similarityThreshold: 0.35,
3606
+ minTokenScore: 0.12,
3606
3607
  crossVenueOnly: true,
3607
3608
  withRules: false,
3608
3609
  includeSimilarity: false,
@@ -3628,13 +3629,17 @@ async function filterHedgeableMarkets({ indexerUrl, timeoutMs, options, items })
3628
3629
  if (!hasPolymarket) continue;
3629
3630
  for (const leg of legs) {
3630
3631
  if (leg && leg.venue === 'pandora' && leg.marketId) {
3631
- matchedPandoraIds.add(String(leg.marketId));
3632
+ const key = normalizeLookupKey(leg.marketId);
3633
+ if (key) matchedPandoraIds.add(key);
3632
3634
  }
3633
3635
  }
3634
3636
  }
3635
3637
 
3636
3638
  return {
3637
- items: normalizedItems.filter((item) => matchedPandoraIds.has(String(item && item.id))),
3639
+ items: normalizedItems.filter((item) => {
3640
+ const key = normalizeLookupKey(item && item.id);
3641
+ return key ? matchedPandoraIds.has(key) : false;
3642
+ }),
3638
3643
  unfilteredCount: normalizedItems.length,
3639
3644
  diagnostics: [],
3640
3645
  };
@@ -3745,18 +3750,28 @@ function buildAmmQuoteEstimateFromReserves(liquidity, side, amountUsdc) {
3745
3750
  let spotPrice = null;
3746
3751
  let impliedProbability = null;
3747
3752
  if (isYes) {
3753
+ // Binary AMM execution path is mint+swap:
3754
+ // 1) deposit collateral => mint amount YES + amount NO
3755
+ // 2) swap minted NO into pool for extra YES along x*y=k
3748
3756
  const nextReserveNo = reserveNo + amountUsdc;
3749
3757
  if (!Number.isFinite(nextReserveNo) || nextReserveNo <= 0) return null;
3750
3758
  const nextReserveYes = kValue / nextReserveNo;
3751
- estimatedShares = reserveYes - nextReserveYes;
3752
- spotPrice = reserveNo / reserveYes;
3759
+ const swapOutputYes = reserveYes - nextReserveYes;
3760
+ if (!Number.isFinite(swapOutputYes) || swapOutputYes < 0) return null;
3761
+ estimatedShares = amountUsdc + swapOutputYes;
3762
+ spotPrice = reserveNo / (reserveYes + reserveNo);
3753
3763
  impliedProbability = reserveNo / (reserveYes + reserveNo);
3754
3764
  } else {
3765
+ // Binary AMM execution path is mint+swap:
3766
+ // 1) deposit collateral => mint amount YES + amount NO
3767
+ // 2) swap minted YES into pool for extra NO along x*y=k
3755
3768
  const nextReserveYes = reserveYes + amountUsdc;
3756
3769
  if (!Number.isFinite(nextReserveYes) || nextReserveYes <= 0) return null;
3757
3770
  const nextReserveNo = kValue / nextReserveYes;
3758
- estimatedShares = reserveNo - nextReserveNo;
3759
- spotPrice = reserveYes / reserveNo;
3771
+ const swapOutputNo = reserveNo - nextReserveNo;
3772
+ if (!Number.isFinite(swapOutputNo) || swapOutputNo < 0) return null;
3773
+ estimatedShares = amountUsdc + swapOutputNo;
3774
+ spotPrice = reserveYes / (reserveYes + reserveNo);
3760
3775
  impliedProbability = reserveYes / (reserveYes + reserveNo);
3761
3776
  }
3762
3777
 
@@ -4864,9 +4879,100 @@ function computePositionMarkValue(yesBalance, noBalance, odds) {
4864
4879
  return round(yesAmount * yesProbability + noAmount * noProbability, 6);
4865
4880
  }
4866
4881
 
4867
- async function enrichPortfolioPositions(indexerUrl, positions, timeoutMs) {
4882
+ function parseTokenAmountMaybeRaw(value) {
4883
+ const numeric = toOptionalNumber(value);
4884
+ if (!Number.isFinite(numeric)) return null;
4885
+
4886
+ const rawText = typeof value === 'string' ? value.trim() : '';
4887
+ if (rawText.includes('.')) {
4888
+ return round(numeric, 6);
4889
+ }
4890
+
4891
+ if (Number.isInteger(numeric) && Math.abs(numeric) >= 1_000_000) {
4892
+ return round(numeric / 1_000_000, 6);
4893
+ }
4894
+ return round(numeric, 6);
4895
+ }
4896
+
4897
+ function normalizeTradeSide(value) {
4898
+ const normalized = String(value || '').trim().toLowerCase();
4899
+ if (['yes', 'y', 'true', '1'].includes(normalized)) return 'yes';
4900
+ if (['no', 'n', 'false', '0'].includes(normalized)) return 'no';
4901
+ return null;
4902
+ }
4903
+
4904
+ async function fetchPortfolioTradeBalanceMap(indexerUrl, options, timeoutMs) {
4905
+ const where = { trader: options.wallet };
4906
+ if (options.chainId !== null) {
4907
+ where.chainId = options.chainId;
4908
+ }
4909
+ const query = buildGraphqlListQuery(
4910
+ 'tradess',
4911
+ 'tradesFilter',
4912
+ ['id', 'marketAddress', 'side', 'tradeType', 'tokenAmount', 'tokenAmountOut'],
4913
+ );
4914
+ const variables = {
4915
+ where,
4916
+ orderBy: 'timestamp',
4917
+ orderDirection: 'desc',
4918
+ before: null,
4919
+ after: null,
4920
+ // Keep this bounded: larger values can trigger indexer INTERNAL_SERVER_ERROR.
4921
+ limit: Math.min(Math.max((Number(options.limit) || 100) * 5, 100), 500),
4922
+ };
4923
+
4924
+ let page;
4925
+ try {
4926
+ const data = await graphqlRequest(indexerUrl, query, variables, timeoutMs);
4927
+ page = normalizePageResult(data.tradess);
4928
+ } catch {
4929
+ return { balancesByMarket: new Map(), diagnostics: [] };
4930
+ }
4931
+
4932
+ const balancesByMarket = new Map();
4933
+ for (const trade of page.items || []) {
4934
+ const marketKey = normalizeLookupKey(trade && trade.marketAddress);
4935
+ if (!marketKey) continue;
4936
+ const side = normalizeTradeSide(trade && trade.side);
4937
+ if (!side) continue;
4938
+
4939
+ const tradeType = String(trade && trade.tradeType ? trade.tradeType : '').toLowerCase();
4940
+ const tokenAmount = parseTokenAmountMaybeRaw(trade && trade.tokenAmount);
4941
+ const tokenAmountOut = parseTokenAmountMaybeRaw(trade && trade.tokenAmountOut);
4942
+
4943
+ let delta = null;
4944
+ if (
4945
+ tradeType.includes('sell') ||
4946
+ tradeType.includes('remove') ||
4947
+ tradeType.includes('burn') ||
4948
+ tradeType.includes('redeem')
4949
+ ) {
4950
+ delta = tokenAmount !== null ? -tokenAmount : tokenAmountOut !== null ? -tokenAmountOut : null;
4951
+ } else {
4952
+ delta = tokenAmountOut !== null ? tokenAmountOut : tokenAmount;
4953
+ }
4954
+ if (!Number.isFinite(delta)) continue;
4955
+
4956
+ const entry = balancesByMarket.get(marketKey) || { yesBalance: 0, noBalance: 0 };
4957
+ if (side === 'yes') {
4958
+ entry.yesBalance = round(entry.yesBalance + delta, 6);
4959
+ } else {
4960
+ entry.noBalance = round(entry.noBalance + delta, 6);
4961
+ }
4962
+ balancesByMarket.set(marketKey, entry);
4963
+ }
4964
+
4965
+ const diagnostics = [];
4966
+ if (page && page.pageInfo && page.pageInfo.hasNextPage) {
4967
+ diagnostics.push('Portfolio token balances use capped trade history; increase --limit for deeper reconstruction.');
4968
+ }
4969
+
4970
+ return { balancesByMarket, diagnostics };
4971
+ }
4972
+
4973
+ async function enrichPortfolioPositions(indexerUrl, positions, options, timeoutMs) {
4868
4974
  if (!Array.isArray(positions) || !positions.length) {
4869
- return [];
4975
+ return { items: [], diagnostics: [] };
4870
4976
  }
4871
4977
 
4872
4978
  const uniqueMarketAddresses = Array.from(
@@ -4896,7 +5002,10 @@ async function enrichPortfolioPositions(indexerUrl, positions, timeoutMs) {
4896
5002
  pollsByKey = new Map();
4897
5003
  }
4898
5004
 
4899
- return positions.map((position) => {
5005
+ const tradeBalances = await fetchPortfolioTradeBalanceMap(indexerUrl, options, timeoutMs);
5006
+ const tradeBalanceByMarket = tradeBalances.balancesByMarket;
5007
+
5008
+ const items = positions.map((position) => {
4900
5009
  const marketKey = normalizeLookupKey(position && position.marketAddress);
4901
5010
  const market = marketKey ? marketsByAddress.get(marketKey) : null;
4902
5011
  const liquidity = market ? buildMarketLiquidityMetrics(market) : buildMarketLiquidityMetrics({});
@@ -4912,8 +5021,17 @@ async function enrichPortfolioPositions(indexerUrl, positions, timeoutMs) {
4912
5021
  ? firstMappedValue(pollsByKey, [market.pollAddress, market.pollId, market.poll && market.poll.id])
4913
5022
  : null;
4914
5023
 
4915
- const yesBalance = pickFiniteNumber(position && position.yesTokenAmount, position && position.yesBalance);
4916
- const noBalance = pickFiniteNumber(position && position.noTokenAmount, position && position.noBalance);
5024
+ const tradeBalance = marketKey ? tradeBalanceByMarket.get(marketKey) : null;
5025
+ const yesBalance = pickFiniteNumber(
5026
+ position && position.yesTokenAmount,
5027
+ position && position.yesBalance,
5028
+ tradeBalance && tradeBalance.yesBalance,
5029
+ );
5030
+ const noBalance = pickFiniteNumber(
5031
+ position && position.noTokenAmount,
5032
+ position && position.noBalance,
5033
+ tradeBalance && tradeBalance.noBalance,
5034
+ );
4917
5035
  const markValueUsdc = computePositionMarkValue(yesBalance, noBalance, odds);
4918
5036
 
4919
5037
  return {
@@ -4935,6 +5053,11 @@ async function enrichPortfolioPositions(indexerUrl, positions, timeoutMs) {
4935
5053
  markValueUsdc,
4936
5054
  };
4937
5055
  });
5056
+
5057
+ return {
5058
+ items,
5059
+ diagnostics: tradeBalances.diagnostics,
5060
+ };
4938
5061
  }
4939
5062
 
4940
5063
  async function fetchPortfolioLiquidityEvents(indexerUrl, options, timeoutMs) {
@@ -5001,7 +5124,13 @@ async function collectPortfolioSnapshot(indexerUrl, options, timeoutMs) {
5001
5124
  }
5002
5125
 
5003
5126
  const positions = Array.isArray(positionsPage.items) ? positionsPage.items : [];
5004
- const enrichedPositions = await enrichPortfolioPositions(indexerUrl, positions, timeoutMs);
5127
+ const enrichedPositionResult = await enrichPortfolioPositions(indexerUrl, positions, options, timeoutMs);
5128
+ const enrichedPositions = Array.isArray(enrichedPositionResult && enrichedPositionResult.items)
5129
+ ? enrichedPositionResult.items
5130
+ : [];
5131
+ const positionDiagnostics = Array.isArray(enrichedPositionResult && enrichedPositionResult.diagnostics)
5132
+ ? enrichedPositionResult.diagnostics
5133
+ : [];
5005
5134
  const liquidityEvents = Array.isArray(liquidityPage.items) ? liquidityPage.items : [];
5006
5135
  const claimEvents = Array.isArray(claimPage.items) ? claimPage.items : [];
5007
5136
  const lpPositions = Array.isArray(lpPayload && lpPayload.items) ? lpPayload.items : [];
@@ -5018,6 +5147,7 @@ async function collectPortfolioSnapshot(indexerUrl, options, timeoutMs) {
5018
5147
  },
5019
5148
  diagnostics: {
5020
5149
  lp: lpDiagnostics,
5150
+ positions: positionDiagnostics,
5021
5151
  },
5022
5152
  };
5023
5153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pandora-cli-skills",
3
- "version": "1.1.56",
3
+ "version": "1.1.57",
4
4
  "description": "Pandora CLI & Skills",
5
5
  "main": "cli/pandora.cjs",
6
6
  "bin": {
@@ -70,6 +70,8 @@ test('arb scan combinatorial flags parse with strict defaults', () => {
70
70
  assert.equal(parsed.combinatorial, false);
71
71
  assert.equal(parsed.slippagePctPerLeg, 0);
72
72
  assert.equal(parsed.maxBundleSize, 4);
73
+ assert.equal(parsed.similarityThreshold, 0.35);
74
+ assert.equal(parsed.minTokenScore, 0.12);
73
75
  });
74
76
 
75
77
  test('arb scan combinatorial mode enforces minimum market count', () => {
@@ -79,6 +81,38 @@ test('arb scan combinatorial mode enforces minimum market count', () => {
79
81
  );
80
82
  });
81
83
 
84
+ test('arb scan accepts cross-venue matching controls', () => {
85
+ const parsed = parseFlags([
86
+ 'scan',
87
+ '--source',
88
+ 'polymarket',
89
+ '--similarity-threshold',
90
+ '0.42',
91
+ '--min-token-score',
92
+ '0.2',
93
+ '--max-close-diff-hours',
94
+ '6',
95
+ '--question-contains',
96
+ 'bitcoin',
97
+ ]);
98
+ assert.equal(parsed.source, 'polymarket');
99
+ assert.equal(parsed.similarityThreshold, 0.42);
100
+ assert.equal(parsed.minTokenScore, 0.2);
101
+ assert.equal(parsed.maxCloseDiffHours, 6);
102
+ assert.equal(parsed.questionContains, 'bitcoin');
103
+ });
104
+
105
+ test('arb scan rejects out-of-range similarity controls', () => {
106
+ assert.throws(
107
+ () => parseFlags(['scan', '--source', 'polymarket', '--similarity-threshold', '1.2']),
108
+ (err) => err && err.code === 'INVALID_FLAG_VALUE',
109
+ );
110
+ assert.throws(
111
+ () => parseFlags(['scan', '--source', 'polymarket', '--min-token-score', '-0.1']),
112
+ (err) => err && err.code === 'INVALID_FLAG_VALUE',
113
+ );
114
+ });
115
+
82
116
  test('buildCombinatorialArbOpportunities applies fee + slippage to net edge', () => {
83
117
  const opportunities = buildCombinatorialArbOpportunities({
84
118
  marketSnapshots: [