pandora-cli-skills 1.1.56 → 1.1.58

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.58
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
@@ -3211,7 +3211,11 @@ function maybeNormalizeReserveUnits(item, reserveYes, reserveNo) {
3211
3211
  const currentTvl = toOptionalNumber(item && item.currentTvl);
3212
3212
  let scale = 1;
3213
3213
 
3214
- if (Number.isFinite(currentTvl) && currentTvl > 0) {
3214
+ // Most indexer reserve fields are raw token units (USDC 6 decimals).
3215
+ // Normalize integer reserve payloads to human USDC units.
3216
+ if (Number.isInteger(reserveYes) && Number.isInteger(reserveNo) && Math.max(reserveYes, reserveNo) >= 1_000_000) {
3217
+ scale = 1_000_000;
3218
+ } else if (Number.isFinite(currentTvl) && currentTvl > 0) {
3215
3219
  const ratio = total / currentTvl;
3216
3220
  // Indexer payloads sometimes expose reserves in 1e6 units while TVL is already in USDC.
3217
3221
  if (Number.isFinite(ratio) && ratio > 500_000 && ratio < 2_500_000) {
@@ -3602,7 +3606,8 @@ async function filterHedgeableMarkets({ indexerUrl, timeoutMs, options, items })
3602
3606
  minSpreadPct: 0,
3603
3607
  minLiquidityUsd: 0,
3604
3608
  maxCloseDiffHours: 24,
3605
- similarityThreshold: 0.86,
3609
+ similarityThreshold: 0.35,
3610
+ minTokenScore: 0.12,
3606
3611
  crossVenueOnly: true,
3607
3612
  withRules: false,
3608
3613
  includeSimilarity: false,
@@ -3628,13 +3633,17 @@ async function filterHedgeableMarkets({ indexerUrl, timeoutMs, options, items })
3628
3633
  if (!hasPolymarket) continue;
3629
3634
  for (const leg of legs) {
3630
3635
  if (leg && leg.venue === 'pandora' && leg.marketId) {
3631
- matchedPandoraIds.add(String(leg.marketId));
3636
+ const key = normalizeLookupKey(leg.marketId);
3637
+ if (key) matchedPandoraIds.add(key);
3632
3638
  }
3633
3639
  }
3634
3640
  }
3635
3641
 
3636
3642
  return {
3637
- items: normalizedItems.filter((item) => matchedPandoraIds.has(String(item && item.id))),
3643
+ items: normalizedItems.filter((item) => {
3644
+ const key = normalizeLookupKey(item && item.id);
3645
+ return key ? matchedPandoraIds.has(key) : false;
3646
+ }),
3638
3647
  unfilteredCount: normalizedItems.length,
3639
3648
  diagnostics: [],
3640
3649
  };
@@ -3745,18 +3754,28 @@ function buildAmmQuoteEstimateFromReserves(liquidity, side, amountUsdc) {
3745
3754
  let spotPrice = null;
3746
3755
  let impliedProbability = null;
3747
3756
  if (isYes) {
3757
+ // Binary AMM execution path is mint+swap:
3758
+ // 1) deposit collateral => mint amount YES + amount NO
3759
+ // 2) swap minted NO into pool for extra YES along x*y=k
3748
3760
  const nextReserveNo = reserveNo + amountUsdc;
3749
3761
  if (!Number.isFinite(nextReserveNo) || nextReserveNo <= 0) return null;
3750
3762
  const nextReserveYes = kValue / nextReserveNo;
3751
- estimatedShares = reserveYes - nextReserveYes;
3752
- spotPrice = reserveNo / reserveYes;
3763
+ const swapOutputYes = reserveYes - nextReserveYes;
3764
+ if (!Number.isFinite(swapOutputYes) || swapOutputYes < 0) return null;
3765
+ estimatedShares = amountUsdc + swapOutputYes;
3766
+ spotPrice = reserveNo / (reserveYes + reserveNo);
3753
3767
  impliedProbability = reserveNo / (reserveYes + reserveNo);
3754
3768
  } else {
3769
+ // Binary AMM execution path is mint+swap:
3770
+ // 1) deposit collateral => mint amount YES + amount NO
3771
+ // 2) swap minted YES into pool for extra NO along x*y=k
3755
3772
  const nextReserveYes = reserveYes + amountUsdc;
3756
3773
  if (!Number.isFinite(nextReserveYes) || nextReserveYes <= 0) return null;
3757
3774
  const nextReserveNo = kValue / nextReserveYes;
3758
- estimatedShares = reserveNo - nextReserveNo;
3759
- spotPrice = reserveYes / reserveNo;
3775
+ const swapOutputNo = reserveNo - nextReserveNo;
3776
+ if (!Number.isFinite(swapOutputNo) || swapOutputNo < 0) return null;
3777
+ estimatedShares = amountUsdc + swapOutputNo;
3778
+ spotPrice = reserveYes / (reserveYes + reserveNo);
3760
3779
  impliedProbability = reserveYes / (reserveYes + reserveNo);
3761
3780
  }
3762
3781
 
@@ -4864,9 +4883,100 @@ function computePositionMarkValue(yesBalance, noBalance, odds) {
4864
4883
  return round(yesAmount * yesProbability + noAmount * noProbability, 6);
4865
4884
  }
4866
4885
 
4867
- async function enrichPortfolioPositions(indexerUrl, positions, timeoutMs) {
4886
+ function parseTokenAmountMaybeRaw(value) {
4887
+ const numeric = toOptionalNumber(value);
4888
+ if (!Number.isFinite(numeric)) return null;
4889
+
4890
+ const rawText = typeof value === 'string' ? value.trim() : '';
4891
+ if (rawText.includes('.')) {
4892
+ return round(numeric, 6);
4893
+ }
4894
+
4895
+ if (Number.isInteger(numeric) && Math.abs(numeric) >= 1_000_000) {
4896
+ return round(numeric / 1_000_000, 6);
4897
+ }
4898
+ return round(numeric, 6);
4899
+ }
4900
+
4901
+ function normalizeTradeSide(value) {
4902
+ const normalized = String(value || '').trim().toLowerCase();
4903
+ if (['yes', 'y', 'true', '1'].includes(normalized)) return 'yes';
4904
+ if (['no', 'n', 'false', '0'].includes(normalized)) return 'no';
4905
+ return null;
4906
+ }
4907
+
4908
+ async function fetchPortfolioTradeBalanceMap(indexerUrl, options, timeoutMs) {
4909
+ const where = { trader: options.wallet };
4910
+ if (options.chainId !== null) {
4911
+ where.chainId = options.chainId;
4912
+ }
4913
+ const query = buildGraphqlListQuery(
4914
+ 'tradess',
4915
+ 'tradesFilter',
4916
+ ['id', 'marketAddress', 'side', 'tradeType', 'tokenAmount', 'tokenAmountOut'],
4917
+ );
4918
+ const variables = {
4919
+ where,
4920
+ orderBy: 'timestamp',
4921
+ orderDirection: 'desc',
4922
+ before: null,
4923
+ after: null,
4924
+ // Keep this bounded: larger values can trigger indexer INTERNAL_SERVER_ERROR.
4925
+ limit: Math.min(Math.max((Number(options.limit) || 100) * 5, 100), 500),
4926
+ };
4927
+
4928
+ let page;
4929
+ try {
4930
+ const data = await graphqlRequest(indexerUrl, query, variables, timeoutMs);
4931
+ page = normalizePageResult(data.tradess);
4932
+ } catch {
4933
+ return { balancesByMarket: new Map(), diagnostics: [] };
4934
+ }
4935
+
4936
+ const balancesByMarket = new Map();
4937
+ for (const trade of page.items || []) {
4938
+ const marketKey = normalizeLookupKey(trade && trade.marketAddress);
4939
+ if (!marketKey) continue;
4940
+ const side = normalizeTradeSide(trade && trade.side);
4941
+ if (!side) continue;
4942
+
4943
+ const tradeType = String(trade && trade.tradeType ? trade.tradeType : '').toLowerCase();
4944
+ const tokenAmount = parseTokenAmountMaybeRaw(trade && trade.tokenAmount);
4945
+ const tokenAmountOut = parseTokenAmountMaybeRaw(trade && trade.tokenAmountOut);
4946
+
4947
+ let delta = null;
4948
+ if (
4949
+ tradeType.includes('sell') ||
4950
+ tradeType.includes('remove') ||
4951
+ tradeType.includes('burn') ||
4952
+ tradeType.includes('redeem')
4953
+ ) {
4954
+ delta = tokenAmount !== null ? -tokenAmount : tokenAmountOut !== null ? -tokenAmountOut : null;
4955
+ } else {
4956
+ delta = tokenAmountOut !== null ? tokenAmountOut : tokenAmount;
4957
+ }
4958
+ if (!Number.isFinite(delta)) continue;
4959
+
4960
+ const entry = balancesByMarket.get(marketKey) || { yesBalance: 0, noBalance: 0 };
4961
+ if (side === 'yes') {
4962
+ entry.yesBalance = round(entry.yesBalance + delta, 6);
4963
+ } else {
4964
+ entry.noBalance = round(entry.noBalance + delta, 6);
4965
+ }
4966
+ balancesByMarket.set(marketKey, entry);
4967
+ }
4968
+
4969
+ const diagnostics = [];
4970
+ if (page && page.pageInfo && page.pageInfo.hasNextPage) {
4971
+ diagnostics.push('Portfolio token balances use capped trade history; increase --limit for deeper reconstruction.');
4972
+ }
4973
+
4974
+ return { balancesByMarket, diagnostics };
4975
+ }
4976
+
4977
+ async function enrichPortfolioPositions(indexerUrl, positions, options, timeoutMs) {
4868
4978
  if (!Array.isArray(positions) || !positions.length) {
4869
- return [];
4979
+ return { items: [], diagnostics: [] };
4870
4980
  }
4871
4981
 
4872
4982
  const uniqueMarketAddresses = Array.from(
@@ -4896,7 +5006,10 @@ async function enrichPortfolioPositions(indexerUrl, positions, timeoutMs) {
4896
5006
  pollsByKey = new Map();
4897
5007
  }
4898
5008
 
4899
- return positions.map((position) => {
5009
+ const tradeBalances = await fetchPortfolioTradeBalanceMap(indexerUrl, options, timeoutMs);
5010
+ const tradeBalanceByMarket = tradeBalances.balancesByMarket;
5011
+
5012
+ const items = positions.map((position) => {
4900
5013
  const marketKey = normalizeLookupKey(position && position.marketAddress);
4901
5014
  const market = marketKey ? marketsByAddress.get(marketKey) : null;
4902
5015
  const liquidity = market ? buildMarketLiquidityMetrics(market) : buildMarketLiquidityMetrics({});
@@ -4912,8 +5025,17 @@ async function enrichPortfolioPositions(indexerUrl, positions, timeoutMs) {
4912
5025
  ? firstMappedValue(pollsByKey, [market.pollAddress, market.pollId, market.poll && market.poll.id])
4913
5026
  : null;
4914
5027
 
4915
- const yesBalance = pickFiniteNumber(position && position.yesTokenAmount, position && position.yesBalance);
4916
- const noBalance = pickFiniteNumber(position && position.noTokenAmount, position && position.noBalance);
5028
+ const tradeBalance = marketKey ? tradeBalanceByMarket.get(marketKey) : null;
5029
+ const yesBalance = pickFiniteNumber(
5030
+ position && position.yesTokenAmount,
5031
+ position && position.yesBalance,
5032
+ tradeBalance && tradeBalance.yesBalance,
5033
+ );
5034
+ const noBalance = pickFiniteNumber(
5035
+ position && position.noTokenAmount,
5036
+ position && position.noBalance,
5037
+ tradeBalance && tradeBalance.noBalance,
5038
+ );
4917
5039
  const markValueUsdc = computePositionMarkValue(yesBalance, noBalance, odds);
4918
5040
 
4919
5041
  return {
@@ -4935,6 +5057,11 @@ async function enrichPortfolioPositions(indexerUrl, positions, timeoutMs) {
4935
5057
  markValueUsdc,
4936
5058
  };
4937
5059
  });
5060
+
5061
+ return {
5062
+ items,
5063
+ diagnostics: tradeBalances.diagnostics,
5064
+ };
4938
5065
  }
4939
5066
 
4940
5067
  async function fetchPortfolioLiquidityEvents(indexerUrl, options, timeoutMs) {
@@ -5001,7 +5128,13 @@ async function collectPortfolioSnapshot(indexerUrl, options, timeoutMs) {
5001
5128
  }
5002
5129
 
5003
5130
  const positions = Array.isArray(positionsPage.items) ? positionsPage.items : [];
5004
- const enrichedPositions = await enrichPortfolioPositions(indexerUrl, positions, timeoutMs);
5131
+ const enrichedPositionResult = await enrichPortfolioPositions(indexerUrl, positions, options, timeoutMs);
5132
+ const enrichedPositions = Array.isArray(enrichedPositionResult && enrichedPositionResult.items)
5133
+ ? enrichedPositionResult.items
5134
+ : [];
5135
+ const positionDiagnostics = Array.isArray(enrichedPositionResult && enrichedPositionResult.diagnostics)
5136
+ ? enrichedPositionResult.diagnostics
5137
+ : [];
5005
5138
  const liquidityEvents = Array.isArray(liquidityPage.items) ? liquidityPage.items : [];
5006
5139
  const claimEvents = Array.isArray(claimPage.items) ? claimPage.items : [];
5007
5140
  const lpPositions = Array.isArray(lpPayload && lpPayload.items) ? lpPayload.items : [];
@@ -5018,6 +5151,7 @@ async function collectPortfolioSnapshot(indexerUrl, options, timeoutMs) {
5018
5151
  },
5019
5152
  diagnostics: {
5020
5153
  lp: lpDiagnostics,
5154
+ positions: positionDiagnostics,
5021
5155
  },
5022
5156
  };
5023
5157
  }
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.58",
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: [