| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export const COMMODITY_FEED = 'market:commodities-bootstrap:v1'; |
| const DAY_MS = 24 * 60 * 60 * 1000; |
| |
| |
| const COMMODITY_HORIZON_MS = 4 * DAY_MS; |
| |
| |
| const MOVE_FRACTION = 0.025; |
|
|
| const COMMODITIES = [ |
| { symbol: 'CL=F', subject: 'the WTI crude oil price', unit: 'USD/bbl' }, |
| { symbol: 'BZ=F', subject: 'the Brent crude oil price', unit: 'USD/bbl' }, |
| { symbol: 'NG=F', subject: 'the US natural gas price', unit: 'USD/MMBtu' }, |
| { symbol: 'GC=F', subject: 'the gold price', unit: 'USD/oz' }, |
| ]; |
|
|
| function findQuote(feed, symbol) { |
| const quotes = Array.isArray(feed?.quotes) ? feed.quotes : Array.isArray(feed) ? feed : null; |
| if (!quotes) return null; |
| return quotes.find((q) => q && q.symbol === symbol) || null; |
| } |
|
|
| function buildCommodityTemplate({ symbol, subject, unit }) { |
| return { |
| id: `commodity:${symbol}`, |
| feedKey: COMMODITY_FEED, |
| domain: 'market', |
|
|
| extractMetric(feed) { |
| const q = findQuote(feed, symbol); |
| const price = Number(q?.price); |
| |
| |
| if (!q || !Number.isFinite(price) || price <= 0) return null; |
| |
| |
| |
| const changePct = Number(q.change); |
| return { subject, symbol, value: price, changePct: Number.isFinite(changePct) ? changePct : null, unit }; |
| }, |
|
|
| horizonPolicy({ nowMs }) { |
| return nowMs + COMMODITY_HORIZON_MS; |
| }, |
|
|
| buildResolutionSpec({ metric, deadlineMs }) { |
| const { value, changePct } = metric; |
| const magnitude = Math.abs(value) * MOVE_FRACTION; |
| if (!(magnitude > 0)) return null; |
| |
| |
| |
| if (changePct == null || changePct === 0) return null; |
| const wantUp = changePct > 0; |
| const threshold = round(wantUp ? value + magnitude : value - magnitude); |
| return { |
| kind: 'hard', |
| metricKey: `${COMMODITY_FEED}|price(symbol==${symbol})`, |
| operator: 'crosses', |
| threshold, |
| baselineValue: round(value), |
| window: 'at-deadline', |
| deadline: deadlineMs, |
| sourceFeed: COMMODITY_FEED, |
| question: buildQuestion(metric, wantUp, threshold, deadlineMs), |
| }; |
| }, |
|
|
| buildQuestion({ spec }) { |
| return spec.question; |
| }, |
|
|
| buildTitle({ metric, spec }) { |
| const dir = spec.threshold >= spec.baselineValue ? 'rise' : 'fall'; |
| return `${capitalize(metric.subject)}: ${dir} to ${spec.threshold} ${metric.unit}?`; |
| }, |
|
|
| userValueScore() { |
| return symbol === 'CL=F' || symbol === 'BZ=F' ? 0.7 : 0.6; |
| }, |
| }; |
| } |
|
|
| function buildQuestion(metric, wantUp, threshold, deadlineMs) { |
| const dir = wantUp ? 'rise to at least' : 'fall to at most'; |
| return `Will ${metric.subject} ${dir} ${threshold} ${metric.unit} by ${isoDate(deadlineMs)}?`; |
| } |
|
|
| export const COMMODITY_BET_TEMPLATES = COMMODITIES.map(buildCommodityTemplate); |
|
|
| function isoDate(ms) { |
| return new Date(ms).toISOString().slice(0, 10); |
| } |
|
|
| function capitalize(text) { |
| const s = String(text || ''); |
| return s.charAt(0).toUpperCase() + s.slice(1); |
| } |
|
|
| |
| |
| |
| function round(value) { |
| if (!Number.isFinite(value)) return value; |
| return Math.round(value * 100) / 100; |
| } |
|
|