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 +1 -1
- package/cli/lib/arb_command_service.cjs +42 -5
- package/cli/pandora.cjs +143 -13
- 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
|
@@ -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.
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
3752
|
-
|
|
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
|
-
|
|
3759
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4916
|
-
const
|
|
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
|
|
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
|
@@ -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: [
|