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 +1 -1
- package/cli/lib/arb_command_service.cjs +42 -5
- package/cli/pandora.cjs +148 -14
- package/package.json +1 -1
- package/tests/unit/combinatorial_arb.test.cjs +34 -0
package/SKILL.md
CHANGED
|
@@ -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:
|
|
563
|
-
similarityThreshold:
|
|
564
|
-
minTokenScore:
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
3752
|
-
|
|
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
|
-
|
|
3759
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4916
|
-
const
|
|
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
|
|
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
|
@@ -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: [
|