Proof: targeted pair-native strategy, preflight, outcome, dashboard, and ops tests pass; full npm test passes 237/237; operator dashboard production bundle builds; ops watcher Python test passes. Assumptions: DB asset, pair, strategy config, and price route rows remain canonical; legacy EURe fields stay only for old-row/API compatibility; local shell has no Kubernetes context for direct live namespace recheck. Still fake: venue-native terminal fill ids and realized fee/PnL attribution remain unavailable; live deployment verification must happen through the repo workflow because manual cluster repair is out of scope.
This commit is contained in:
parent
729d2ade0e
commit
fdeb1287b4
11 changed files with 137 additions and 13 deletions
|
|
@ -22,6 +22,11 @@ ASSETS = {
|
||||||
symbol="EURe",
|
symbol="EURe",
|
||||||
decimals=18,
|
decimals=18,
|
||||||
),
|
),
|
||||||
|
"nep141:usdc.omft.near": watch_live_mm.AssetMeta(
|
||||||
|
asset_id="nep141:usdc.omft.near",
|
||||||
|
symbol="USDC",
|
||||||
|
decimals=6,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -99,6 +104,7 @@ class WatchLiveMmTests(unittest.TestCase):
|
||||||
"spendable": {
|
"spendable": {
|
||||||
"nep141:btc.omft.near": "137014",
|
"nep141:btc.omft.near": "137014",
|
||||||
"nep141:eure": "38999978799978799978",
|
"nep141:eure": "38999978799978799978",
|
||||||
|
"nep141:usdc.omft.near": "1234567",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -132,6 +138,38 @@ class WatchLiveMmTests(unittest.TestCase):
|
||||||
self.assertEqual(state["mark_to_market_pnl_eure"], "0.497413887978799978")
|
self.assertEqual(state["mark_to_market_pnl_eure"], "0.497413887978799978")
|
||||||
self.assertEqual(state["btc_spendable"], "0.00137014 BTC")
|
self.assertEqual(state["btc_spendable"], "0.00137014 BTC")
|
||||||
self.assertEqual(state["eure_spendable"], "38.9999788 EURe")
|
self.assertEqual(state["eure_spendable"], "38.9999788 EURe")
|
||||||
|
self.assertIn("1.234567 USDC", state["spendable_assets"])
|
||||||
|
|
||||||
|
def test_current_state_fingerprint_does_not_require_eure_asset(self) -> None:
|
||||||
|
assets = {
|
||||||
|
key: value
|
||||||
|
for key, value in ASSETS.items()
|
||||||
|
if value.symbol != "EURe"
|
||||||
|
}
|
||||||
|
responses = {
|
||||||
|
"near-intents-ingest": {"ingest": {"published_count": 1}},
|
||||||
|
"inventory-sync": {
|
||||||
|
"last_snapshot": {
|
||||||
|
"spendable": {
|
||||||
|
"nep141:btc.omft.near": "100000",
|
||||||
|
"nep141:usdc.omft.near": "5000000",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"history-writer": {"latest_portfolio_metrics": {}},
|
||||||
|
"strategy-engine": {"armed": False, "latest_decision": {}},
|
||||||
|
"trade-executor": {"armed": False, "signer_registered": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
watch_live_mm,
|
||||||
|
"fetch_service_state",
|
||||||
|
side_effect=lambda *, namespace, deployment: responses[deployment],
|
||||||
|
):
|
||||||
|
state = watch_live_mm.current_state_fingerprint(namespace="unrip", assets=assets)
|
||||||
|
|
||||||
|
self.assertEqual(state["eure_spendable"], "untracked")
|
||||||
|
self.assertIn("5 USDC", state["spendable_assets"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@ def current_state_fingerprint(*, namespace: str, assets: dict[str, AssetMeta]) -
|
||||||
spendable = snapshot.get("spendable") or {}
|
spendable = snapshot.get("spendable") or {}
|
||||||
latest_decision = strategy.get("latest_decision") or {}
|
latest_decision = strategy.get("latest_decision") or {}
|
||||||
latest_metrics = history.get("latest_portfolio_metrics") or {}
|
latest_metrics = history.get("latest_portfolio_metrics") or {}
|
||||||
|
spendable_assets = format_spendable_assets(spendable=spendable, assets=assets)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ts": now_string(),
|
"ts": now_string(),
|
||||||
|
|
@ -227,8 +228,9 @@ def current_state_fingerprint(*, namespace: str, assets: dict[str, AssetMeta]) -
|
||||||
"strategy_armed": str(bool(strategy.get("armed"))).lower(),
|
"strategy_armed": str(bool(strategy.get("armed"))).lower(),
|
||||||
"executor_armed": str(bool(executor.get("armed"))).lower(),
|
"executor_armed": str(bool(executor.get("armed"))).lower(),
|
||||||
"signer_registered": str(executor.get("signer_registered")),
|
"signer_registered": str(executor.get("signer_registered")),
|
||||||
"btc_spendable": format_amount(spendable.get(find_symbol_asset_id(assets, "BTC"), "0"), assets.get(find_symbol_asset_id(assets, "BTC"))),
|
"spendable_assets": ", ".join(spendable_assets) if spendable_assets else "none",
|
||||||
"eure_spendable": format_amount(spendable.get(find_symbol_asset_id(assets, "EURe"), "0"), assets.get(find_symbol_asset_id(assets, "EURe"))),
|
"btc_spendable": format_symbol_amount(spendable=spendable, assets=assets, symbol="BTC"),
|
||||||
|
"eure_spendable": format_symbol_amount(spendable=spendable, assets=assets, symbol="EURe"),
|
||||||
"latest_reason": str(latest_decision.get("decision_reason") or "none"),
|
"latest_reason": str(latest_decision.get("decision_reason") or "none"),
|
||||||
"latest_quote_id": str(latest_decision.get("quote_id") or "none"),
|
"latest_quote_id": str(latest_decision.get("quote_id") or "none"),
|
||||||
"trade_pnl_eure": str(latest_metrics.get("trade_pnl_eure") or "-"),
|
"trade_pnl_eure": str(latest_metrics.get("trade_pnl_eure") or "-"),
|
||||||
|
|
@ -293,6 +295,7 @@ def print_snapshot(*, namespace: str, assets: dict[str, AssetMeta]) -> None:
|
||||||
f" signer_registered={state['signer_registered']}"
|
f" signer_registered={state['signer_registered']}"
|
||||||
f" btc={state['btc_spendable']}"
|
f" btc={state['btc_spendable']}"
|
||||||
f" eure={state['eure_spendable']}"
|
f" eure={state['eure_spendable']}"
|
||||||
|
f" balances=[{state['spendable_assets']}]"
|
||||||
f" trade_pnl_eure={state['trade_pnl_eure']}"
|
f" trade_pnl_eure={state['trade_pnl_eure']}"
|
||||||
f" mtm_pnl_eure={state['mark_to_market_pnl_eure']}"
|
f" mtm_pnl_eure={state['mark_to_market_pnl_eure']}"
|
||||||
f" latest_reason={state['latest_reason']}"
|
f" latest_reason={state['latest_reason']}"
|
||||||
|
|
@ -308,6 +311,7 @@ def print_state_line(state: dict[str, str]) -> None:
|
||||||
f" signer_registered={state['signer_registered']}"
|
f" signer_registered={state['signer_registered']}"
|
||||||
f" btc={state['btc_spendable']}"
|
f" btc={state['btc_spendable']}"
|
||||||
f" eure={state['eure_spendable']}"
|
f" eure={state['eure_spendable']}"
|
||||||
|
f" balances=[{state['spendable_assets']}]"
|
||||||
f" trade_pnl_eure={state['trade_pnl_eure']}"
|
f" trade_pnl_eure={state['trade_pnl_eure']}"
|
||||||
f" mtm_pnl_eure={state['mark_to_market_pnl_eure']}"
|
f" mtm_pnl_eure={state['mark_to_market_pnl_eure']}"
|
||||||
f" last_matching_quote_at={state['last_matching_quote_at']}"
|
f" last_matching_quote_at={state['last_matching_quote_at']}"
|
||||||
|
|
@ -404,6 +408,22 @@ def find_symbol_asset_id(assets: dict[str, AssetMeta], symbol: str) -> str:
|
||||||
raise KeyError(f"missing asset for symbol {symbol}")
|
raise KeyError(f"missing asset for symbol {symbol}")
|
||||||
|
|
||||||
|
|
||||||
|
def format_symbol_amount(*, spendable: dict[str, str], assets: dict[str, AssetMeta], symbol: str) -> str:
|
||||||
|
for asset_id, asset in assets.items():
|
||||||
|
if asset.symbol == symbol:
|
||||||
|
return format_amount(spendable.get(asset_id, "0"), asset)
|
||||||
|
return "untracked"
|
||||||
|
|
||||||
|
|
||||||
|
def format_spendable_assets(*, spendable: dict[str, str], assets: dict[str, AssetMeta]) -> list[str]:
|
||||||
|
rows: list[str] = []
|
||||||
|
for asset_id, asset in sorted(assets.items(), key=lambda entry: (entry[1].symbol, entry[0])):
|
||||||
|
rows.append(format_amount(spendable.get(asset_id, "0"), asset))
|
||||||
|
for asset_id in sorted(set(spendable) - set(assets)):
|
||||||
|
rows.append(f"{spendable.get(asset_id, '0')} {asset_id}")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def format_amount(raw_amount: str | None, asset: AssetMeta | None, *, max_places: int = 8) -> str:
|
def format_amount(raw_amount: str | None, asset: AssetMeta | None, *, max_places: int = 8) -> str:
|
||||||
if raw_amount in (None, ""):
|
if raw_amount in (None, ""):
|
||||||
return "-"
|
return "-"
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ async function handleDemand(event) {
|
||||||
source: 'strategy-engine',
|
source: 'strategy-engine',
|
||||||
venue: 'near-intents',
|
venue: 'near-intents',
|
||||||
eventType: 'execute_trade',
|
eventType: 'execute_trade',
|
||||||
observedAt: event.observed_at,
|
observedAt: evaluation.command.decision_at || event.observed_at || event.ingested_at,
|
||||||
payload: evaluation.command,
|
payload: evaluation.command,
|
||||||
});
|
});
|
||||||
await producer.sendJson(config.kafkaTopicCmdExecuteTrade, commandEvent, { key: evaluation.command.execution_key });
|
await producer.sendJson(config.kafkaTopicCmdExecuteTrade, commandEvent, { key: evaluation.command.execution_key });
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,11 @@ export function createIntentRequestController({
|
||||||
const destinationAsset = requestPair.destinationAsset;
|
const destinationAsset = requestPair.destinationAsset;
|
||||||
const sourceAmount = String(
|
const sourceAmount = String(
|
||||||
body.source_amount
|
body.source_amount
|
||||||
|| body.amount
|
?? body.amount
|
||||||
|| body.amount_eure
|
?? body.amount_eure
|
||||||
|| requestPair.requestDefaultNotional
|
?? requestPair.requestDefaultNotional
|
||||||
|| config.intentRequestDefaultAmountEure
|
?? config.intentRequestDefaultAmountEure
|
||||||
|| '5',
|
?? '5',
|
||||||
);
|
);
|
||||||
const legacyAmountEure = body.amount_eure == null ? null : String(body.amount_eure);
|
const legacyAmountEure = body.amount_eure == null ? null : String(body.amount_eure);
|
||||||
const slippageBps = Number(body.slippage_bps ?? requestPair.slippageBps ?? config.intentRequestDefaultSlippageBps ?? 200);
|
const slippageBps = Number(body.slippage_bps ?? requestPair.slippageBps ?? config.intentRequestDefaultSlippageBps ?? 200);
|
||||||
|
|
@ -95,7 +95,12 @@ export function createIntentRequestController({
|
||||||
sourceAsset.decimals,
|
sourceAsset.decimals,
|
||||||
{ field: 'intent_request_max_notional' },
|
{ field: 'intent_request_max_notional' },
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
sourceAmountUnits = parseDecimalToUnits(sourceAmount, sourceAsset.decimals, { field: 'source_amount' });
|
sourceAmountUnits = parseDecimalToUnits(sourceAmount, sourceAsset.decimals, { field: 'source_amount' });
|
||||||
|
} catch (error) {
|
||||||
|
blockedBeforeQuote = true;
|
||||||
|
throw codedError('invalid_source_amount', error.message || 'Source amount is invalid.');
|
||||||
|
}
|
||||||
if (maxAmountUnits != null && BigInt(sourceAmountUnits) > BigInt(maxAmountUnits)) {
|
if (maxAmountUnits != null && BigInt(sourceAmountUnits) > BigInt(maxAmountUnits)) {
|
||||||
blockedBeforeQuote = true;
|
blockedBeforeQuote = true;
|
||||||
throw codedError(
|
throw codedError(
|
||||||
|
|
@ -198,7 +203,10 @@ export function createIntentRequestController({
|
||||||
throw codedError('solver_quote_unanswered', 'The relay returned no solver quotes for this request.');
|
throw codedError('solver_quote_unanswered', 'The relay returned no solver quotes for this request.');
|
||||||
}
|
}
|
||||||
if (!selectedQuote) {
|
if (!selectedQuote) {
|
||||||
throw codedError('quote_below_min_receive', 'Solver quotes were below the explicit minimum BTC receive amount.');
|
throw codedError(
|
||||||
|
'quote_below_min_receive',
|
||||||
|
`Solver quotes were below the explicit minimum ${destinationAsset.symbol || 'destination'} receive amount.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = 'draft';
|
state = 'draft';
|
||||||
|
|
|
||||||
|
|
@ -1001,6 +1001,8 @@ function normalizeIntentRequestForUi({ config, request }) {
|
||||||
const destinationAsset = config.assetRegistry.get(request.destination_asset_id) || config.tradingBtc;
|
const destinationAsset = config.assetRegistry.get(request.destination_asset_id) || config.tradingBtc;
|
||||||
return {
|
return {
|
||||||
...request,
|
...request,
|
||||||
|
source_symbol: request.source_symbol || sourceAsset?.symbol || 'Source',
|
||||||
|
destination_symbol: request.destination_symbol || destinationAsset?.symbol || 'Destination',
|
||||||
source_amount: formatUnits(request.source_amount_units || '0', sourceAsset?.decimals || 0),
|
source_amount: formatUnits(request.source_amount_units || '0', sourceAsset?.decimals || 0),
|
||||||
expected_destination_amount: formatUnits(
|
expected_destination_amount: formatUnits(
|
||||||
request.expected_destination_amount_units || '0',
|
request.expected_destination_amount_units || '0',
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,10 @@ export function evaluateTradeOpportunity({
|
||||||
const effectiveMaxNotional = pairRuntime.maxNotional ?? maxNotional;
|
const effectiveMaxNotional = pairRuntime.maxNotional ?? maxNotional;
|
||||||
const legacyEureNotional = isLegacyEureNotional({ pairRuntime, config });
|
const legacyEureNotional = isLegacyEureNotional({ pairRuntime, config });
|
||||||
const decisionId = crypto.randomUUID();
|
const decisionId = crypto.randomUUID();
|
||||||
|
const decisionAt = new Date(now).toISOString();
|
||||||
const baseDecision = {
|
const baseDecision = {
|
||||||
decision_id: decisionId,
|
decision_id: decisionId,
|
||||||
|
decision_at: decisionAt,
|
||||||
quote_id: payload.quote_id,
|
quote_id: payload.quote_id,
|
||||||
pair: pairKey(payload.asset_in, payload.asset_out),
|
pair: pairKey(payload.asset_in, payload.asset_out),
|
||||||
pair_id: pairRuntime.pair?.pairId || null,
|
pair_id: pairRuntime.pair?.pairId || null,
|
||||||
|
|
@ -143,11 +145,13 @@ export function evaluateTradeOpportunity({
|
||||||
pair_config_id: decision.pair_config_id,
|
pair_config_id: decision.pair_config_id,
|
||||||
pair_config_version: decision.pair_config_version,
|
pair_config_version: decision.pair_config_version,
|
||||||
edge_bps: decision.edge_bps,
|
edge_bps: decision.edge_bps,
|
||||||
|
decision_at: decision.decision_at,
|
||||||
max_notional: decision.max_notional,
|
max_notional: decision.max_notional,
|
||||||
min_notional: decision.min_notional,
|
min_notional: decision.min_notional,
|
||||||
max_notional_eure: decision.max_notional_eure,
|
max_notional_eure: decision.max_notional_eure,
|
||||||
price_route_id: decision.price_route_id,
|
price_route_id: decision.price_route_id,
|
||||||
reference_price_id: buildResult.details.price_id || null,
|
reference_price_id: buildResult.details.price_id || null,
|
||||||
|
direction: decision.direction,
|
||||||
notional: decision.notional,
|
notional: decision.notional,
|
||||||
notional_asset_id: decision.notional_asset_id,
|
notional_asset_id: decision.notional_asset_id,
|
||||||
notional_symbol: decision.notional_symbol,
|
notional_symbol: decision.notional_symbol,
|
||||||
|
|
|
||||||
|
|
@ -382,6 +382,7 @@ function IntentRequestForm({ summary, onControl }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function IntentRequestLifecycle({ item }) {
|
function IntentRequestLifecycle({ item }) {
|
||||||
|
const destinationSymbol = item.destination_symbol || 'destination';
|
||||||
return (
|
return (
|
||||||
<div className="lifecycle-detail-panel">
|
<div className="lifecycle-detail-panel">
|
||||||
<div className="lifecycle-stage-grid">
|
<div className="lifecycle-stage-grid">
|
||||||
|
|
@ -400,7 +401,11 @@ function IntentRequestLifecycle({ item }) {
|
||||||
<div className="stage-status">{`${item.solver_quote_count || 0} quote(s)`}</div>
|
<div className="stage-status">{`${item.solver_quote_count || 0} quote(s)`}</div>
|
||||||
<div className="stage-body">
|
<div className="stage-body">
|
||||||
<IdentifierLine label="Quote hash" value={item.quote_hash} />
|
<IdentifierLine label="Quote hash" value={item.quote_hash} />
|
||||||
<div className="status-subtle">{item.quoted_destination_amount ? `Quoted ${item.quoted_destination_amount} BTC` : 'No usable quote stored'}</div>
|
<div className="status-subtle">
|
||||||
|
{item.quoted_destination_amount
|
||||||
|
? `Quoted ${item.quoted_destination_amount} ${destinationSymbol}`
|
||||||
|
: 'No usable quote stored'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="lifecycle-stage-card">
|
<div className="lifecycle-stage-card">
|
||||||
|
|
@ -470,14 +475,16 @@ function IntentRequestsTable({ items, executorArmed, onControl }) {
|
||||||
const rowKey = item.request_id || item.idempotency_key || String(index);
|
const rowKey = item.request_id || item.idempotency_key || String(index);
|
||||||
const isExpanded = expanded.has(rowKey);
|
const isExpanded = expanded.has(rowKey);
|
||||||
const canSubmit = item.live_submit_capable && executorArmed === true;
|
const canSubmit = item.live_submit_capable && executorArmed === true;
|
||||||
|
const sourceSymbol = item.source_symbol || 'source';
|
||||||
|
const destinationSymbol = item.destination_symbol || 'destination';
|
||||||
return (
|
return (
|
||||||
<Fragment key={rowKey}>
|
<Fragment key={rowKey}>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{formatTimestamp(item.resolved_at || item.submitted_at || item.created_at)}</td>
|
<td>{formatTimestamp(item.resolved_at || item.submitted_at || item.created_at)}</td>
|
||||||
<td><IdentifierLine label="Request" value={item.request_id} /></td>
|
<td><IdentifierLine label="Request" value={item.request_id} /></td>
|
||||||
<td>
|
<td>
|
||||||
<div className="mono">{`${item.source_amount} ${item.source_symbol || 'EURe'}`}</div>
|
<div className="mono">{`${item.source_amount} ${sourceSymbol}`}</div>
|
||||||
<div className="status-subtle">{`Min ${item.min_destination_amount} ${item.destination_symbol || 'BTC'}`}</div>
|
<div className="status-subtle">{`Min ${item.min_destination_amount} ${destinationSymbol}`}</div>
|
||||||
<div className="status-subtle">{`${item.slippage_bps ?? 'n/a'} bps slippage`}</div>
|
<div className="status-subtle">{`${item.slippage_bps ?? 'n/a'} bps slippage`}</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -452,6 +452,34 @@ test('nBTC/USDC preflight blocks insufficient source inventory with source-speci
|
||||||
assert.equal(relay.quoteCalls, 0);
|
assert.equal(relay.quoteCalls, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('pair-native preflight treats explicit zero source_amount as invalid instead of using default', async () => {
|
||||||
|
const relay = buildRelay();
|
||||||
|
const pair = buildPair({
|
||||||
|
sourceAsset: USDC,
|
||||||
|
destinationAsset: BTC,
|
||||||
|
source: 'btc_usdc_reference',
|
||||||
|
routeId: 'btc-usdc-route',
|
||||||
|
});
|
||||||
|
const { controller } = buildController({
|
||||||
|
relay,
|
||||||
|
getTradingConfig: async () => ({
|
||||||
|
ok: true,
|
||||||
|
tradingEure: EURE,
|
||||||
|
tradingBtc: BTC,
|
||||||
|
pairByKey: new Map([[pair.key, pair]]),
|
||||||
|
pairById: new Map([[pair.pairId, pair]]),
|
||||||
|
defaultTakerPair: pair,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const preflight = await controller.preflight({ pair_id: pair.pairId, source_amount: 0 });
|
||||||
|
|
||||||
|
assert.equal(preflight.state, 'blocked');
|
||||||
|
assert.equal(preflight.reason_code, 'invalid_source_amount');
|
||||||
|
assert.equal(preflight.source_amount, '0');
|
||||||
|
assert.equal(relay.quoteCalls, 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('insufficient spendable EURe blocks before solver quote or signing', async () => {
|
test('insufficient spendable EURe blocks before solver quote or signing', async () => {
|
||||||
const store = buildStore({ inventoryUnits: '0' });
|
const store = buildStore({ inventoryUnits: '0' });
|
||||||
const relay = buildRelay();
|
const relay = buildRelay();
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,12 @@ test('request UI uses pair-native source amount fields and copy', () => {
|
||||||
assert.match(fundsSource, /name="source_amount"/);
|
assert.match(fundsSource, /name="source_amount"/);
|
||||||
assert.match(fundsSource, /pair_id: form\.pair_id/);
|
assert.match(fundsSource, /pair_id: form\.pair_id/);
|
||||||
assert.match(fundsSource, /Pair request creation/);
|
assert.match(fundsSource, /Pair request creation/);
|
||||||
|
assert.match(fundsSource, /Quoted \$\{item\.quoted_destination_amount\} \$\{destinationSymbol\}/);
|
||||||
assert.doesNotMatch(fundsSource, /EURe to BTC request creation/);
|
assert.doesNotMatch(fundsSource, /EURe to BTC request creation/);
|
||||||
assert.doesNotMatch(fundsSource, /EURe-to-BTC/);
|
assert.doesNotMatch(fundsSource, /EURe-to-BTC/);
|
||||||
|
assert.doesNotMatch(fundsSource, /Quoted \$\{item\.quoted_destination_amount\} BTC/);
|
||||||
|
assert.doesNotMatch(fundsSource, /source_symbol \|\| 'EURe'/);
|
||||||
|
assert.doesNotMatch(fundsSource, /destination_symbol \|\| 'BTC'/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('funds balance rows expose unvalued valuation reasons', () => {
|
test('funds balance rows expose unvalued valuation reasons', () => {
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,10 @@ test('strategy duplicate quote tracking is bounded and state-safe', () => {
|
||||||
assert.match(source, /seenQuotes\.getState\(\)/);
|
assert.match(source, /seenQuotes\.getState\(\)/);
|
||||||
assert.doesNotMatch(source, /seen_quotes:\s*\{\}/);
|
assert.doesNotMatch(source, /seen_quotes:\s*\{\}/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('strategy execute commands use decision timestamp as durable observed time', () => {
|
||||||
|
assert.match(
|
||||||
|
source,
|
||||||
|
/observedAt:\s*evaluation\.command\.decision_at\s*\|\|\s*event\.observed_at\s*\|\|\s*event\.ingested_at/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,11 @@ test('strategy emits actionable exact-in BTC -> EURe command when armed and inve
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(result.decision.decision, 'actionable');
|
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.equal(result.decision.decision_reason, 'actionable');
|
||||||
assert.ok(result.command);
|
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');
|
assert.equal(result.command.quote_output.amount_out, '4900000000000000000');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -180,6 +183,9 @@ test('strategy emits actionable exact-in BTC -> USDC command from DB price route
|
||||||
assert.equal(result.decision.notional, '8.000000');
|
assert.equal(result.decision.notional, '8.000000');
|
||||||
assert.equal(result.decision.notional_symbol, 'USDC');
|
assert.equal(result.decision.notional_symbol, 'USDC');
|
||||||
assert.equal(result.decision.eure_notional, null);
|
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.quote_output.amount_out, '7960800');
|
||||||
assert.equal(result.command.notional_symbol, 'USDC');
|
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.tradingBtc.assetId], '10000');
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue