Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,6 @@ npm run smoke:production -- https://YOUR-API-HOST https://YOUR-SITE.vercel.app

When basket legs request an `intervalMin`, both marginals must come from priced markets for that exact interval. Missing or unpriced requested windows return `insufficient_data`; aggregate sentiment and other intervals cannot substitute. If those marginals exist but correlation covers a different interval, the result is `independence_only` with a null adjusted probability. An independent estimate is not evidence that the two events are independent.

When several pools share that interval, the basket selects the correlation snapshot's pool when it is still priced. If a snapshot pool is no longer present among the priced markets, the basket reports the independent estimate and withholds the correlation adjustment. Matching interval alone does not establish matching event identity.

Run the deterministic engine regression suite with `npm ci && npm test`. Live API and production smoke scripts remain separate because they depend on external market data.
16 changes: 12 additions & 4 deletions src/signals/basket.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ async function resolveDependencies(deps) {
function selectWindow(sentiment, correlation, side, preferredWindowMin = null) {
if (!Array.isArray(sentiment.windows) || !sentiment.windows.length) return null;

const pool = side === "A" ? correlation?.marketPoolA : correlation?.marketPoolB;

if (Number.isFinite(preferredWindowMin)) {
const requested = sentiment.windows.find((window) => window.intervalMin === preferredWindowMin && Number.isFinite(window.impliedProbabilityRaw));
const matching = sentiment.windows.filter((window) => window.intervalMin === preferredWindowMin && Number.isFinite(window.impliedProbabilityRaw));
const requested = matching.find((window) => pool && window.pool?.toLowerCase() === pool.toLowerCase()) || matching[0];
return requested || null;
}

const pool = side === "A" ? correlation?.marketPoolA : correlation?.marketPoolB;
if (pool) {
const exactPool = sentiment.windows.find((window) => window.pool?.toLowerCase() === pool.toLowerCase());
if (exactPool) return exactPool;
Expand Down Expand Up @@ -97,12 +99,18 @@ export async function priceBasket({ legs }, deps = {}) {

const correlationWindowMismatch = preferredMarketWindowMin !== null
&& correlation.marketWindowMin !== preferredMarketWindowMin;
if (correlation.status !== "ok" || correlation.correlation === null || correlationWindowMismatch) {
const correlationPoolMismatch = [
[correlation.marketPoolA, marginalA.window?.pool],
[correlation.marketPoolB, marginalB.window?.pool],
].some(([correlationPool, pricedPool]) => correlationPool && pricedPool && correlationPool.toLowerCase() !== pricedPool.toLowerCase());
if (correlation.status !== "ok" || correlation.correlation === null || correlationWindowMismatch || correlationPoolMismatch) {
return {
status: "independence_only",
note: correlationWindowMismatch
? "Requested market prices are usable, but correlation does not cover the requested event window"
: "DreamDEX prices are usable, but current candle history is too asynchronous/sparse for a defensible correlation adjustment",
: correlationPoolMismatch
? "Market prices are usable, but correlation was measured from a different market pool"
: "DreamDEX prices are usable, but current candle history is too asynchronous/sparse for a defensible correlation adjustment",
...base,
adjustedProbability: null,
pricingDifference: null,
Expand Down
25 changes: 25 additions & 0 deletions test/basket-live-fallback.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,28 @@ test('explicit window withholds adjustment from a different correlation window',
assert.deepEqual(result.legs.map(leg => leg.intervalMin), [60, 60]);
assert.ok(Math.abs(result.independentProbability - 0.0371) < 1e-12);
});

test('explicit window never applies correlation from different pools at the same interval', async () => {
const result = await priceBasket({ legs: [
{ asset: 'BTC', intervalMin: 60 }, { asset: 'ETH', intervalMin: 60 },
] }, {
getAssetSentiment: async asset => asset === 'BTC' ? {
...btc, windows: [{ ...btc.windows[2], pool: '0xbtc-current', impliedProbabilityRaw: 0.8 }, { ...btc.windows[2], pool: '0xbtc-old', impliedProbabilityRaw: 0.1 }],
} : eth,
calculateAssetCorrelation: async () => ({ status: 'ok', correlation: 0.8, qualityScore: 1, confidence: 'high', marketWindowMin: 60, marketPoolA: '0xbtc-old', marketPoolB: '0xeth60' }),
});
assert.equal(result.legs[0].pool, '0xbtc-old');
assert.equal(result.legs[0].probability, 0.1);
assert.equal(result.status, 'ok');
});

test('correlation is withheld if its pool is absent from the priced sentiment snapshot', async () => {
const result = await priceBasket({ legs: [
{ asset: 'BTC', intervalMin: 60 }, { asset: 'ETH', intervalMin: 60 },
] }, {
getAssetSentiment: async asset => asset === 'BTC' ? btc : eth,
calculateAssetCorrelation: async () => ({ status: 'ok', correlation: 0.8, qualityScore: 1, confidence: 'high', marketWindowMin: 60, marketPoolA: '0xbtc-old', marketPoolB: '0xeth60' }),
});
assert.equal(result.status, 'independence_only');
assert.equal(result.adjustedProbability, null);
});
Loading