import test from 'node:test'; import assert from 'node:assert/strict'; import { evaluateTradeOpportunity } from '../src/core/strategy.mjs'; function makeConfig() { const tradingBtc = { assetId: 'nep141:btc.omft.near', symbol: 'BTC', decimals: 8, chain: 'btc:mainnet', }; const tradingEure = { assetId: 'nep141:eure.omft.near', symbol: 'EURe', decimals: 18, chain: 'eth:100', }; return { tradingBtc, tradingEure, activePair: `${tradingBtc.assetId}->${tradingEure.assetId}`, assetRegistry: new Map([ [tradingBtc.assetId, tradingBtc], [tradingEure.assetId, tradingEure], ]), strategyGrossThresholdPct: 2, strategyMaxNotionalEure: 5, strategyPriceMaxAgeMs: 30_000, strategyInventoryMaxAgeMs: 30_000, }; } function makePriceEvent(overrides = {}) { return { ingested_at: new Date('2026-04-02T10:00:00.000Z').toISOString(), payload: { price_id: 'price-1', pair: 'nep141:btc.omft.near->nep141:eure.omft.near', eur_per_btc: '100000.00000000', eure_per_btc: '100000.00000000', btc_per_eur: '0.000010000000', btc_per_eure: '0.000010000000', ...overrides, }, }; } function makeInventoryEvent(overrides = {}) { return { ingested_at: new Date('2026-04-02T10:00:00.000Z').toISOString(), payload: { inventory_id: 'inventory-1', spendable: { 'nep141:btc.omft.near': '1000000', 'nep141:eure.omft.near': '10000000000000000000', }, pending_inbound: { 'nep141:btc.omft.near': '0', 'nep141:eure.omft.near': '0', }, ...overrides, }, }; } test('strategy emits actionable exact-in BTC -> EURe command when armed and inventory is fresh', () => { const config = makeConfig(); const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-1', pair: config.activePair, asset_in: config.tradingBtc.assetId, asset_out: config.tradingEure.assetId, request_kind: 'exact_in', amount_in: '5000', min_deadline_ms: '60000', }, }, priceEvent: makePriceEvent(), inventoryEvent: makeInventoryEvent(), config, armed: true, now: Date.parse('2026-04-02T10:00:05.000Z'), }); assert.equal(result.decision.decision, 'actionable'); assert.equal(result.decision.decision_at, '2026-04-02T10:00:05.000Z'); assert.equal(result.decision.decision_reason, 'actionable'); assert.ok(result.command); assert.equal(result.command.decision_at, '2026-04-02T10:00:05.000Z'); assert.equal(result.command.direction, 'btc_to_eure'); assert.equal(result.command.quote_output.amount_out, '4900000000000000000'); }); test('strategy blocks when pending deposit exists but credited inventory is insufficient', () => { const config = makeConfig(); const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-2', pair: config.activePair, asset_in: config.tradingBtc.assetId, asset_out: config.tradingEure.assetId, request_kind: 'exact_in', amount_in: '5000', min_deadline_ms: '60000', }, }, priceEvent: makePriceEvent(), inventoryEvent: makeInventoryEvent({ spendable: { 'nep141:btc.omft.near': '1000000', 'nep141:eure.omft.near': '1000000000000000000', }, pending_inbound: { 'nep141:btc.omft.near': '0', 'nep141:eure.omft.near': '5000000000000000000', }, }), config, armed: true, now: Date.parse('2026-04-02T10:00:05.000Z'), }); assert.equal(result.decision.decision, 'rejected'); assert.equal(result.decision.decision_reason, 'pending_deposit_not_credited'); assert.equal(result.command, undefined); }); test('strategy blocks stale prices before command emission', () => { const config = makeConfig(); const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-3', pair: config.activePair, asset_in: config.tradingEure.assetId, asset_out: config.tradingBtc.assetId, request_kind: 'exact_out', amount_out: '10000', min_deadline_ms: '60000', }, }, priceEvent: makePriceEvent(), inventoryEvent: makeInventoryEvent(), config, armed: true, now: Date.parse('2026-04-02T10:00:45.000Z'), }); assert.equal(result.decision.decision_reason, 'stale_reference_price'); assert.equal(result.command, undefined); }); test('strategy emits actionable exact-in BTC -> USDC command from DB price route', () => { const config = makeBtcUsdcDbConfig(); const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-usdc-1', pair: config.activePair, asset_in: config.tradingBtc.assetId, asset_out: config.tradingUsdc.assetId, request_kind: 'exact_in', amount_in: '10000', min_deadline_ms: '60000', }, }, priceEvent: makeBtcUsdcPriceEvent(), inventoryEvent: makeBtcUsdcInventoryEvent(), config, armed: true, now: Date.parse('2026-04-02T10:00:05.000Z'), }); assert.equal(result.decision.decision, 'actionable'); assert.equal(result.decision.direction, 'base_to_quote'); assert.equal(result.decision.edge_bps, '49'); assert.equal(result.decision.price_route_id, 'btc-usdc:v1'); assert.equal(result.decision.notional, '8.000000'); assert.equal(result.decision.notional_symbol, 'USDC'); assert.equal(result.decision.eure_notional, null); assert.equal(result.command.direction, 'base_to_quote'); assert.equal(result.command.decision_at, '2026-04-02T10:00:05.000Z'); assert.equal(result.command.max_notional_eure, null); assert.equal(result.command.quote_output.amount_out, '7960800'); assert.equal(result.command.notional_symbol, 'USDC'); assert.equal(result.command.expected_inventory_delta_units[config.tradingBtc.assetId], '10000'); assert.equal(result.command.expected_inventory_delta_units[config.tradingUsdc.assetId], '-7960800'); assert.equal(result.command.asset_out_decimals, 6); }); test('strategy uses BTC inventory, not USDC inventory, for USDC -> BTC maker responses', () => { const config = makeBtcUsdcDbConfig({ includeReversePair: true }); const reversePair = `${config.tradingUsdc.assetId}->${config.tradingBtc.assetId}`; const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-usdc-to-btc-inventory-side', pair: reversePair, asset_in: config.tradingUsdc.assetId, asset_out: config.tradingBtc.assetId, request_kind: 'exact_in', amount_in: '8000000', min_deadline_ms: '60000', }, }, priceEvent: makeBtcUsdcPriceEvent(), inventoryEvent: makeBtcUsdcInventoryEvent({ spendable: { [config.tradingBtc.assetId]: '1000000', [config.tradingUsdc.assetId]: '0', }, }), config, armed: true, now: Date.parse('2026-04-02T10:00:05.000Z'), }); assert.equal(result.decision.decision, 'actionable'); assert.equal(result.decision.direction, 'quote_to_base'); assert.equal(result.decision.inventory_asset, config.tradingBtc.assetId); assert.equal(result.decision.inventory_available, '1000000'); assert.equal(result.command.expected_inventory_delta_units[config.tradingBtc.assetId], '-9951'); assert.equal(result.command.expected_inventory_delta_units[config.tradingUsdc.assetId], '8000000'); }); test('strategy subtracts pending outbound from maker inventory before approving USDC -> BTC responses', () => { const config = makeBtcUsdcDbConfig({ includeReversePair: true }); const reversePair = `${config.tradingUsdc.assetId}->${config.tradingBtc.assetId}`; const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-usdc-to-btc-pending-outbound', pair: reversePair, asset_in: config.tradingUsdc.assetId, asset_out: config.tradingBtc.assetId, request_kind: 'exact_in', amount_in: '8000000', min_deadline_ms: '60000', }, }, priceEvent: makeBtcUsdcPriceEvent(), inventoryEvent: makeBtcUsdcInventoryEvent({ spendable: { [config.tradingBtc.assetId]: '9951', [config.tradingUsdc.assetId]: '0', }, pending_outbound: { [config.tradingBtc.assetId]: '1', [config.tradingUsdc.assetId]: '0', }, }), config, armed: true, now: Date.parse('2026-04-02T10:00:05.000Z'), }); assert.equal(result.decision.decision, 'rejected'); assert.equal(result.decision.decision_reason, 'pending_outbound_reserved'); assert.equal(result.decision.inventory_required, '9951'); assert.equal(result.decision.inventory_spendable, '9951'); assert.equal(result.decision.inventory_pending_outbound, '1'); assert.equal(result.decision.inventory_available, '9950'); assert.equal(result.command, undefined); }); test('strategy rejects dust exact-out BTC -> USDC below configured min notional after integer rounding', () => { const config = makeBtcUsdcDbConfig({ strategyConfigOverrides: { edgeBps: 20, minNotional: '1', maxNotional: '400', }, }); const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-usdc-dust-exact-out', pair: config.activePair, asset_in: config.tradingBtc.assetId, asset_out: config.tradingUsdc.assetId, request_kind: 'exact_out', amount_out: '2016', min_deadline_ms: '60000', }, }, priceEvent: makeBtcUsdcPriceEvent(), inventoryEvent: makeBtcUsdcInventoryEvent(), config, armed: true, now: Date.parse('2026-04-02T10:00:05.000Z'), }); assert.equal(result.decision.decision, 'rejected'); assert.equal(result.decision.decision_reason, 'min_notional_not_met'); assert.equal(result.decision.edge_bps, '20'); assert.equal(result.decision.threshold_pct, '0.2'); assert.equal(result.decision.notional, '0.002016'); assert.equal(result.decision.min_notional, '1'); assert.equal(result.decision.proposed_amount_in, '3'); assert.ok(Number(result.decision.gross_edge_pct) >= Number(result.decision.threshold_pct)); assert.equal(result.command, undefined); }); test('strategy emits exact-out BTC -> USDC command at min notional only when rounded edge clears threshold', () => { const config = makeBtcUsdcDbConfig({ strategyConfigOverrides: { edgeBps: 20, minNotional: '1', maxNotional: '400', }, }); const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-usdc-min-exact-out', pair: config.activePair, asset_in: config.tradingBtc.assetId, asset_out: config.tradingUsdc.assetId, request_kind: 'exact_out', amount_out: '1000000', min_deadline_ms: '60000', }, }, priceEvent: makeBtcUsdcPriceEvent(), inventoryEvent: makeBtcUsdcInventoryEvent(), config, armed: true, now: Date.parse('2026-04-02T10:00:05.000Z'), }); assert.equal(result.decision.decision, 'actionable'); assert.equal(result.decision.decision_reason, 'actionable'); assert.equal(result.decision.notional, '1.000000'); assert.equal(result.decision.min_notional, '1'); assert.equal(result.command.quote_output.amount_in, '1253'); assert.ok(Number(result.decision.gross_edge_pct) >= Number(result.decision.threshold_pct)); }); test('strategy blocks BTC -> USDC when route-specific reference price is stale', () => { const config = makeBtcUsdcDbConfig(); const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-usdc-stale', pair: config.activePair, asset_in: config.tradingBtc.assetId, asset_out: config.tradingUsdc.assetId, request_kind: 'exact_in', amount_in: '10000', }, }, priceEvent: makeBtcUsdcPriceEvent(), inventoryEvent: makeBtcUsdcInventoryEvent(), config, armed: true, now: Date.parse('2026-04-02T10:00:45.000Z'), }); assert.equal(result.decision.decision_reason, 'stale_reference_price'); assert.equal(result.command, undefined); }); test('strategy blocks BTC -> USDC when price event lacks USDC route fields', () => { const config = makeBtcUsdcDbConfig(); const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-usdc-missing-price', pair: config.activePair, asset_in: config.tradingBtc.assetId, asset_out: config.tradingUsdc.assetId, request_kind: 'exact_in', amount_in: '10000', }, }, priceEvent: makePriceEvent(), inventoryEvent: makeBtcUsdcInventoryEvent(), config, armed: true, now: Date.parse('2026-04-02T10:00:05.000Z'), }); assert.equal(result.decision.decision_reason, 'reference_price_missing'); assert.equal(result.command, undefined); }); test('maker response age policy skips stale BTC -> USDC quotes without emitting a relay command', () => { const config = makeBtcUsdcDbConfig({ strategyConfigOverrides: { makerMaxQuoteAgeEnabled: true, makerMaxQuoteAgeMs: 100, makerLatencyPolicyReason: 'test stale response protection', }, }); const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-usdc-too-old', pair: config.activePair, asset_in: config.tradingBtc.assetId, asset_out: config.tradingUsdc.assetId, request_kind: 'exact_in', amount_in: '10000', maker_timing: { quote_received_at: '2026-04-02T10:00:04.800Z', }, }, }, priceEvent: makeBtcUsdcPriceEvent(), inventoryEvent: makeBtcUsdcInventoryEvent(), config, armed: true, strategyReceivedAt: '2026-04-02T10:00:05.000Z', now: Date.parse('2026-04-02T10:00:05.000Z'), }); assert.equal(result.decision.decision, 'blocked'); assert.equal(result.decision.decision_reason, 'maker_quote_too_old'); assert.equal(result.decision.quote_age_at_decision_ms, 200); assert.equal(result.decision.response_policy.enabled, true); assert.equal(result.decision.response_policy.max_quote_age_ms, 100); assert.equal(result.command, undefined); }); test('maker response age policy skips when quote timing prerequisites are missing', () => { const config = makeBtcUsdcDbConfig({ strategyConfigOverrides: { makerMaxQuoteAgeEnabled: true, makerMaxQuoteAgeMs: 100, }, }); const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-usdc-no-timing', pair: config.activePair, asset_in: config.tradingBtc.assetId, asset_out: config.tradingUsdc.assetId, request_kind: 'exact_in', amount_in: '10000', }, }, priceEvent: makeBtcUsdcPriceEvent(), inventoryEvent: makeBtcUsdcInventoryEvent(), config, armed: true, strategyReceivedAt: '2026-04-02T10:00:05.000Z', now: Date.parse('2026-04-02T10:00:05.000Z'), }); assert.equal(result.decision.decision, 'blocked'); assert.equal(result.decision.decision_reason, 'maker_quote_age_unavailable'); assert.equal(result.decision.response_policy.valid, true); assert.equal(result.command, undefined); }); test('maker response age policy skips invalid enabled config', () => { const config = makeBtcUsdcDbConfig({ strategyConfigOverrides: { makerMaxQuoteAgeEnabled: true, makerMaxQuoteAgeMs: null, }, }); const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-usdc-invalid-policy', pair: config.activePair, asset_in: config.tradingBtc.assetId, asset_out: config.tradingUsdc.assetId, request_kind: 'exact_in', amount_in: '10000', maker_timing: { quote_received_at: '2026-04-02T10:00:04.950Z', }, }, }, priceEvent: makeBtcUsdcPriceEvent(), inventoryEvent: makeBtcUsdcInventoryEvent(), config, armed: true, strategyReceivedAt: '2026-04-02T10:00:05.000Z', now: Date.parse('2026-04-02T10:00:05.000Z'), }); assert.equal(result.decision.decision, 'blocked'); assert.equal(result.decision.decision_reason, 'maker_quote_response_policy_invalid'); assert.equal(result.decision.response_policy.valid, false); assert.equal(result.command, undefined); }); test('disabled maker response age policy preserves current BTC -> USDC actionability', () => { const config = makeBtcUsdcDbConfig({ strategyConfigOverrides: { makerMaxQuoteAgeEnabled: false, makerMaxQuoteAgeMs: null, }, }); const result = evaluateTradeOpportunity({ demandEvent: { payload: { quote_id: 'quote-usdc-policy-disabled', pair: config.activePair, asset_in: config.tradingBtc.assetId, asset_out: config.tradingUsdc.assetId, request_kind: 'exact_in', amount_in: '10000', maker_timing: { quote_received_at: '2026-04-02T09:59:59.000Z', }, }, }, priceEvent: makeBtcUsdcPriceEvent(), inventoryEvent: makeBtcUsdcInventoryEvent(), config, armed: true, strategyReceivedAt: '2026-04-02T10:00:05.000Z', now: Date.parse('2026-04-02T10:00:05.000Z'), }); assert.equal(result.decision.decision, 'actionable'); assert.equal(result.decision.response_policy.enabled, false); assert.ok(result.command); }); function makeBtcUsdcDbConfig({ strategyConfigOverrides = {}, includeReversePair = false } = {}) { const tradingBtc = { assetId: 'nep141:nbtc.bridge.near', symbol: 'BTC', decimals: 8, chain: 'btc:mainnet', }; const tradingUsdc = { assetId: 'nep141:gnosis-0x2a22f9c3b484c3629090feed35f17ff8f88f76f0.omft.near', symbol: 'USDC', decimals: 6, chain: 'gnosis', }; const activePair = `${tradingBtc.assetId}->${tradingUsdc.assetId}`; const reversePair = `${tradingUsdc.assetId}->${tradingBtc.assetId}`; const strategyConfig = { configId: `${activePair}:v1`, version: 1, edgeBps: 49, maxNotional: '150', priceMaxAgeMs: 30_000, inventoryMaxAgeMs: 30_000, minNotional: '0', makerMaxQuoteAgeEnabled: false, makerMaxQuoteAgeMs: null, makerLatencyPolicyReason: null, ...strategyConfigOverrides, }; const priceRoute = { routeId: 'btc-usdc:v1', source: 'btc_usdc_reference', baseAssetId: tradingBtc.assetId, quoteAssetId: tradingUsdc.assetId, }; const pair = { pairId: activePair, key: activePair, assetIn: tradingBtc, assetOut: tradingUsdc, asset_in: tradingBtc.assetId, asset_out: tradingUsdc.assetId, enabled: true, observeEnabled: true, makerEnabled: true, takerEnabled: false, canTrade: true, blockReason: null, strategyConfig, priceRoute, }; const reverseStrategyConfig = { ...strategyConfig, configId: `${reversePair}:v1`, }; const reversePairConfig = { ...pair, pairId: reversePair, key: reversePair, assetIn: tradingUsdc, assetOut: tradingBtc, asset_in: tradingUsdc.assetId, asset_out: tradingBtc.assetId, strategyConfig: reverseStrategyConfig, }; const pairEntries = [[activePair, pair]]; if (includeReversePair) pairEntries.push([reversePair, reversePairConfig]); return { tradingBtc, tradingUsdc, activePair, ok: true, tradingConfigLoaded: true, requireDbTradingConfig: true, assetRegistry: new Map([ [tradingBtc.assetId, tradingBtc], [tradingUsdc.assetId, tradingUsdc], ]), pairByKey: new Map(pairEntries), strategyGrossThresholdPct: 0.49, strategyMaxNotionalEure: 150, strategyPriceMaxAgeMs: 30_000, strategyInventoryMaxAgeMs: 30_000, }; } function makeBtcUsdcPriceEvent(overrides = {}) { return { ingested_at: new Date('2026-04-02T10:00:00.000Z').toISOString(), payload: { price_id: 'price-usdc-1', pair: 'nep141:nbtc.bridge.near->nep141:gnosis-0x2a22f9c3b484c3629090feed35f17ff8f88f76f0.omft.near', price_route_id: 'btc-usdc:v1', eur_per_btc: '75000.00000000', eure_per_btc: '75000.00000000', btc_per_eur: '0.000013333333', btc_per_eure: '0.000013333333', usd_per_btc: '80000.00000000', usdc_per_btc: '80000.00000000', btc_per_usd: '0.000012500000', btc_per_usdc: '0.000012500000', ...overrides, }, }; } function makeBtcUsdcInventoryEvent(overrides = {}) { return { ingested_at: new Date('2026-04-02T10:00:00.000Z').toISOString(), payload: { inventory_id: 'inventory-usdc-1', spendable: { 'nep141:nbtc.bridge.near': '1000000', 'nep141:gnosis-0x2a22f9c3b484c3629090feed35f17ff8f88f76f0.omft.near': '1000000000', }, pending_inbound: { 'nep141:nbtc.bridge.near': '0', 'nep141:gnosis-0x2a22f9c3b484c3629090feed35f17ff8f88f76f0.omft.near': '0', }, pending_outbound: { 'nep141:nbtc.bridge.near': '0', 'nep141:gnosis-0x2a22f9c3b484c3629090feed35f17ff8f88f76f0.omft.near': '0', }, ...overrides, }, }; }