window.GDD = window.GDD || {}; const { useState, useEffect, useRef, useCallback, useMemo } = React; /* ────────────────────────────────────────────── Commodities Exchange — Contract Market ────────────────────────────────────────────── */ /* ── Static contract catalogue ── */ const CATEGORIES = [ { id: 'all', label: 'All Contracts' }, { id: 'ore', label: 'Raw Ores' }, { id: 'mineral', label: 'Refined Minerals' }, { id: 'gas', label: 'Gas Products' }, { id: 'isotope', label: 'Isotopes' }, { id: 'exotic', label: 'Exotic Matter' }, ]; const CONTRACTS = [ /* Ores */ { symbol:'VLD', name:'Veldspar', category:'ore', lotSize:1000, tickSize:0.01, marginPct:8, basePrice:14, hub:'Jita IV – Moon 4' }, { symbol:'SCR', name:'Scordite', category:'ore', lotSize:500, tickSize:0.02, marginPct:10, basePrice:32, hub:'Jita IV – Moon 4' }, { symbol:'PYX', name:'Pyroxeres', category:'ore', lotSize:500, tickSize:0.05, marginPct:10, basePrice:45, hub:'Amarr VIII – Emperor' }, { symbol:'KRN', name:'Kernite', category:'ore', lotSize:250, tickSize:0.10, marginPct:12, basePrice:90, hub:'Amarr VIII – Emperor' }, { symbol:'OMB', name:'Omber', category:'ore', lotSize:250, tickSize:0.10, marginPct:12, basePrice:135, hub:'Rens VI – Moon 8' }, { symbol:'JSP', name:'Jaspet', category:'ore', lotSize:200, tickSize:0.10, marginPct:15, basePrice:190, hub:'Dodixie IX – Moon 20' }, { symbol:'HEM', name:'Hemorphite', category:'ore', lotSize:100, tickSize:0.50, marginPct:18, basePrice:360, hub:'Jita IV – Moon 4' }, { symbol:'ARK', name:'Arkonor', category:'ore', lotSize:50, tickSize:1.00, marginPct:20, basePrice:620, hub:'Jita IV – Moon 4' }, /* Minerals */ { symbol:'TRI', name:'Tritanium', category:'mineral', lotSize:5000, tickSize:0.01, marginPct:6, basePrice:5, hub:'Jita IV – Moon 4' }, { symbol:'PYE', name:'Pyerite', category:'mineral', lotSize:2000, tickSize:0.01, marginPct:8, basePrice:12, hub:'Jita IV – Moon 4' }, { symbol:'MLX', name:'Mexallon', category:'mineral', lotSize:1000, tickSize:0.02, marginPct:8, basePrice:35, hub:'Jita IV – Moon 4' }, { symbol:'ISO', name:'Isogen', category:'mineral', lotSize:500, tickSize:0.05, marginPct:12, basePrice:110, hub:'Amarr VIII – Emperor' }, { symbol:'NCX', name:'Nocxium', category:'mineral', lotSize:250, tickSize:0.10, marginPct:15, basePrice:380, hub:'Jita IV – Moon 4' }, { symbol:'ZDR', name:'Zydrine', category:'mineral', lotSize:100, tickSize:0.50, marginPct:18, basePrice:950, hub:'Jita IV – Moon 4' }, { symbol:'MEG', name:'Megacyte', category:'mineral', lotSize:50, tickSize:1.00, marginPct:20, basePrice:2800,hub:'Jita IV – Moon 4' }, { symbol:'MOR', name:'Morphite', category:'mineral', lotSize:10, tickSize:5.00, marginPct:25, basePrice:8500,hub:'Jita IV – Moon 4' }, /* Gas */ { symbol:'ATM', name:'Atmospheric', category:'gas', lotSize:2000, tickSize:0.01, marginPct:10, basePrice:8, hub:'Jita IV – Moon 4' }, { symbol:'NEB', name:'Nebular', category:'gas', lotSize:500, tickSize:0.05, marginPct:12, basePrice:45, hub:'Amarr VIII – Emperor' }, { symbol:'ION', name:'Ionized', category:'gas', lotSize:200, tickSize:0.10, marginPct:15, basePrice:120, hub:'Jita IV – Moon 4' }, { symbol:'FUL', name:'Fullerides', category:'gas', lotSize:100, tickSize:0.50, marginPct:18, basePrice:450, hub:'Jita IV – Moon 4' }, /* Isotopes */ { symbol:'H3', name:'Hydrogen-3', category:'isotope', lotSize:500, tickSize:0.05, marginPct:15, basePrice:85, hub:'Jita IV – Moon 4' }, { symbol:'HE4', name:'Helium-4', category:'isotope', lotSize:500, tickSize:0.05, marginPct:15, basePrice:92, hub:'Amarr VIII – Emperor' }, { symbol:'N15', name:'Nitrogen-15', category:'isotope', lotSize:500, tickSize:0.05, marginPct:15, basePrice:78, hub:'Rens VI – Moon 8' }, { symbol:'O18', name:'Oxygen-18', category:'isotope', lotSize:500, tickSize:0.05, marginPct:15, basePrice:88, hub:'Dodixie IX – Moon 20' }, /* Exotic */ { symbol:'RDB', name:'Reedstone', category:'exotic', lotSize:10, tickSize:5.00, marginPct:25, basePrice:12000,hub:'Jita IV – Moon 4' }, { symbol:'TCH', name:'Tachyon Salt', category:'exotic', lotSize:5, tickSize:10.0, marginPct:30, basePrice:45000,hub:'Jita IV – Moon 4' }, ]; /* ── Sparkline ── */ function Sparkline({ data, width = 80, height = 24, color = 'var(--green)' }) { const ref = useRef(null); useEffect(() => { const c = ref.current; if (!c || !data || data.length < 2) return; const ctx = c.getContext('2d'); const dpr = window.devicePixelRatio || 1; c.width = width * dpr; c.height = height * dpr; ctx.scale(dpr, dpr); ctx.clearRect(0, 0, width, height); const min = Math.min(...data); const max = Math.max(...data); const range = max - min || 1; const step = width / (data.length - 1); ctx.beginPath(); data.forEach((v, i) => { const x = i * step; const y = height - ((v - min) / range) * (height - 4) - 2; i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); }); ctx.strokeStyle = color; ctx.lineWidth = 1.5; ctx.lineJoin = 'round'; ctx.stroke(); const grad = ctx.createLinearGradient(0, 0, 0, height); const isGreen = color.includes('green'); grad.addColorStop(0, isGreen ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)'); grad.addColorStop(1, 'rgba(0,0,0,0)'); ctx.lineTo(width, height); ctx.lineTo(0, height); ctx.closePath(); ctx.fillStyle = grad; ctx.fill(); }, [data, width, height, color]); return ; } /* ── Ticker Tape ── */ function TickerTape({ items }) { const [offset, setOffset] = useState(0); const raf = useRef(null); useEffect(() => { const speed = 0.5; const tick = () => { setOffset(o => { const n = o - speed; return n < -3000 ? 0 : n; }); raf.current = requestAnimationFrame(tick); }; raf.current = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf.current); }, []); const dup = [...items, ...items, ...items]; return (
{dup.map((t, i) => ( {t.symbol} ₢{t.price.toLocaleString()} = 0 ? 'var(--green)' : 'var(--red)', fontWeight: 600 }}> {t.change >= 0 ? '▲' : '▼'} {Math.abs(t.changePct).toFixed(1)}% ))}
); } /* ── Category Tabs ── */ function CategoryTabs({ active, onChange }) { return (
{CATEGORIES.map(cat => ( ))}
); } /* ── Contract Board — main table ── */ function ContractBoard({ commodities, selected, onSelect, category }) { const filtered = category === 'all' ? commodities : commodities.filter(c => c.category === category); return (
{/* Table header */}
TickerNameLastChg% BidAskVolumeOpen Int.
{/* Rows */}
{filtered.map(c => { const up = c.change >= 0; const isSel = selected === c.symbol; const spread = c.bestAsk - c.bestBid; return (
onSelect(c.symbol)} style={{ display: 'grid', gridTemplateColumns: '90px 120px 80px 60px 70px 70px 80px 90px 70px', gap: '4px', alignItems: 'center', padding: '7px 12px', cursor: 'pointer', background: isSel ? 'var(--surface-hover)' : 'transparent', borderLeft: isSel ? '2px solid var(--accent)' : '2px solid transparent', borderBottom: '1px solid var(--border)', transition: 'background var(--transition-fast)', fontSize: '0.75rem', }}> {c.symbol} {c.name} ₢{c.price.toLocaleString()} {up ? '+' : ''}{c.changePct.toFixed(1)}% ₢{c.bestBid} ₢{c.bestAsk} {(c.volume / 1000).toFixed(1)}K {(c.openInterest / 1000).toFixed(1)}K
); })}
); } /* ── Contract Spec Panel ── */ function ContractSpec({ contract, commodity }) { if (!contract || !commodity) return null; const marginPerLot = Math.ceil(commodity.price * contract.lotSize * (contract.marginPct / 100)); const notionalPerLot = commodity.price * contract.lotSize; return (
Contract Specification
{[ ['Ticker', contract.symbol, 'var(--accent)'], ['Unit', contract.name, 'var(--fg-bright)'], ['Lot Size', contract.lotSize.toLocaleString() + ' units', 'var(--fg-bright)'], ['Tick Size', '₢' + contract.tickSize, 'var(--fg-bright)'], ['Margin Req.', contract.marginPct + '%', 'var(--cyan)'], ['Margin / Lot', '₢' + marginPerLot.toLocaleString(), 'var(--cyan)'], ['Notional / Lot', '₢' +notionalPerLot.toLocaleString(), 'var(--fg-dim)'], ['Delivery', contract.hub, 'var(--fg-dim)'], ['Settlement', commodity.settlement ? '₢' + commodity.settlement : '—', 'var(--fg-dim)'], ['Expiry', '30 DTE', 'var(--muted)'], ['Supply', (commodity.supply / 1000).toFixed(0) + 'K/day', 'var(--green)'], ['Demand', (commodity.demand / 1000).toFixed(0) + 'K/day', commodity.demand > commodity.supply ? 'var(--red)' : 'var(--green)'], ].map(([label, value, color], i) => ( {label} {value} ))}
{/* Supply/Demand bar */}
SUPPLY commodity.supply ? 'var(--red)' : 'var(--green)' }}> {commodity.demand > commodity.supply ? 'DEFICIT' : 'SURPLUS'}: {Math.abs(commodity.supply - commodity.demand).toLocaleString()} DEMAND
); } /* ── Depth Chart (canvas) ── */ function DepthChart({ bids, asks, midPrice }) { const ref = useRef(null); useEffect(() => { const c = ref.current; if (!c) return; const ctx = c.getContext('2d'); const dpr = window.devicePixelRatio || 1; const W = c.clientWidth; const H = c.clientHeight; c.width = W * dpr; c.height = H * dpr; ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H); if (!bids.length || !asks.length) return; const maxCum = Math.max( bids.reduce((s, b) => Math.max(s, b.cumulative), 0), asks.reduce((s, a) => Math.max(s, a.cumulative), 0) ); const priceRange = midPrice * 0.06; const pMin = midPrice - priceRange; const pMax = midPrice + priceRange; const toX = p => ((p - pMin) / (pMax - pMin)) * W; const toY = cum => H - (cum / (maxCum * 1.1)) * H; /* Grid */ ctx.strokeStyle = 'rgba(28,42,63,0.4)'; ctx.lineWidth = 0.5; for (let i = 0; i < 4; i++) { const y = (H / 3) * i; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); } /* Bids (green area, left of mid) */ ctx.beginPath(); ctx.moveTo(toX(bids[0].price), H); bids.forEach(b => ctx.lineTo(toX(b.price), toY(b.cumulative))); ctx.lineTo(toX(bids[bids.length - 1].price), H); ctx.closePath(); const bg = ctx.createLinearGradient(0, 0, 0, H); bg.addColorStop(0, 'rgba(34,197,94,0.25)'); bg.addColorStop(1, 'rgba(34,197,94,0.03)'); ctx.fillStyle = bg; ctx.fill(); /* Asks (red area, right of mid) */ ctx.beginPath(); ctx.moveTo(toX(asks[0].price), H); asks.forEach(a => ctx.lineTo(toX(a.price), toY(a.cumulative))); ctx.lineTo(toX(asks[asks.length - 1].price), H); ctx.closePath(); const ag = ctx.createLinearGradient(0, 0, 0, H); ag.addColorStop(0, 'rgba(239,68,68,0.25)'); ag.addColorStop(1, 'rgba(239,68,68,0.03)'); ctx.fillStyle = ag; ctx.fill(); /* Mid line */ const mx = toX(midPrice); ctx.setLineDash([3, 3]); ctx.beginPath(); ctx.moveTo(mx, 0); ctx.lineTo(mx, H); ctx.strokeStyle = 'var(--accent)'; ctx.lineWidth = 1; ctx.stroke(); ctx.setLineDash([]); /* Price label */ ctx.fillStyle = 'var(--accent)'; ctx.font = 'bold 9px var(--font-mono)'; ctx.textAlign = 'center'; ctx.fillText('₢' + midPrice.toFixed(0), mx, H - 4); }, [bids, asks, midPrice]); return ; } /* ── Order Book ── */ function OrderBook({ bids, asks, spread, spreadPct }) { const maxBid = Math.max(...bids.map(b => b.volume)); const maxAsk = Math.max(...asks.map(a => a.volume)); return (
Market Depth Spread: ₢{spread} ({spreadPct.toFixed(2)}%)
PriceSizeCum
{/* Asks (reversed) */}
{[...asks].reverse().map((a, i) => (
₢{a.price} {a.volume.toLocaleString()} {a.cumulative.toLocaleString()}
))}
{/* Mid */}
₢{((bids[0]?.price || 0 + asks[0]?.price || 0) / 2).toLocaleString()} MID
{/* Bids */}
{bids.map((b, i) => (
₢{b.price} {b.volume.toLocaleString()} {b.cumulative.toLocaleString()}
))}
); } /* ── Price Chart (canvas) ── */ function PriceChart({ data, symbol }) { const ref = useRef(null); useEffect(() => { const c = ref.current; if (!c || !data || data.length < 2) return; const ctx = c.getContext('2d'); const dpr = window.devicePixelRatio || 1; const W = c.clientWidth; const H = c.clientHeight; c.width = W * dpr; c.height = H * dpr; ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H); const prices = data.map(d => d.close); const highs = data.map(d => d.high); const lows = data.map(d => d.low); const volumes = data.map(d => d.volume); const allP = [...highs, ...lows]; const minP = Math.min(...allP); const maxP = Math.max(...allP); const rangeP = maxP - minP || 1; const maxV = Math.max(...volumes); const chartH = H * 0.72; const volH = H * 0.22; const barW = Math.max(2, (W / data.length) - 1); /* Grid */ ctx.strokeStyle = 'rgba(28,42,63,0.5)'; ctx.lineWidth = 0.5; for (let i = 0; i < 5; i++) { const y = (chartH / 4) * i; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); ctx.fillStyle = 'var(--muted)'; ctx.font = '8px var(--font-mono)'; ctx.fillText('₢' + (maxP - (rangeP / 4) * i).toFixed(0), W - 42, y + 9); } /* Volume bars */ data.forEach((d, i) => { const x = (i / data.length) * W; const vh = (d.volume / maxV) * volH; ctx.fillStyle = d.close >= d.open ? 'rgba(34,197,94,0.18)' : 'rgba(239,68,68,0.18)'; ctx.fillRect(x, H - vh, barW, vh); }); /* Price line */ ctx.beginPath(); data.forEach((d, i) => { const x = (i / (data.length - 1)) * W; const y = chartH - ((d.close - minP) / rangeP) * (chartH - 10) - 5; i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); }); const lastC = data[data.length - 1].close; const firstC = data[0].close; const lineColor = lastC >= firstC ? '#22c55e' : '#ef4444'; ctx.strokeStyle = lineColor; ctx.lineWidth = 1.8; ctx.lineJoin = 'round'; ctx.stroke(); /* Area fill */ const lastX = W; ctx.lineTo(lastX, chartH); ctx.lineTo(0, chartH); ctx.closePath(); const grad = ctx.createLinearGradient(0, 0, 0, chartH); grad.addColorStop(0, lastC >= firstC ? 'rgba(34,197,94,0.10)' : 'rgba(239,68,68,0.10)'); grad.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = grad; ctx.fill(); /* Current price line */ const curY = chartH - ((lastC - minP) / rangeP) * (chartH - 10) - 5; ctx.setLineDash([4, 4]); ctx.beginPath(); ctx.moveTo(0, curY); ctx.lineTo(W, curY); ctx.strokeStyle = lineColor; ctx.lineWidth = 1; ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = lineColor; ctx.beginPath(); ctx.roundRect(W - 55, curY - 8, 52, 16, 3); ctx.fill(); ctx.fillStyle = '#080c14'; ctx.font = 'bold 8px var(--font-mono)'; ctx.fillText('₢' + lastC.toFixed(0), W - 52, curY + 3); }, [data, symbol]); return ; } /* ── Order Form (commodities-style: lots, margin, long/short) ── */ function OrderForm({ contract, commodity, credits, positions, onTrade }) { const [direction, setDirection] = useState('long'); // long or short const [orderType, setOrderType] = useState('market'); // market, limit, stop const [lots, setLots] = useState(''); const [limitPrice, setLimitPrice] = useState(''); const [error, setError] = useState(''); const [confirmation, setConfirmation] = useState(null); if (!contract || !commodity) return null; const effectivePrice = orderType === 'market' ? commodity.price : (parseFloat(limitPrice) || commodity.price); const numLots = parseInt(lots) || 0; const totalUnits = numLots * contract.lotSize; const notional = totalUnits * effectivePrice; const marginRequired = Math.ceil(notional * (contract.marginPct / 100)); const commission = Math.ceil(notional * 0.015); const pos = positions.find(p => p.symbol === contract.symbol); const existingDirection = pos ? pos.direction : null; const canAfford = marginRequired + commission <= credits; const handleSubmit = () => { if (numLots <= 0) { setError('Enter lot quantity'); return; } if (orderType !== 'market' && !limitPrice) { setError('Set limit/stop price'); return; } if (!canAfford) { setError('Insufficient margin + commission'); return; } setError(''); setConfirmation({ direction, orderType, lots: numLots, price: effectivePrice, notional, marginRequired, commission, totalUnits }); }; const confirmTrade = () => { onTrade(direction, contract.symbol, effectivePrice, numLots, totalUnits, marginRequired, commission); setConfirmation(null); setLots(''); setLimitPrice(''); setError(''); }; return (
Place Order — {contract.symbol} Lot: {contract.lotSize.toLocaleString()} units
{/* Long / Short toggle */}
{['long', 'short'].map(d => ( ))}
{/* Order type pills */}
{['market', 'limit', 'stop'].map(t => ( ))}
{/* Limit/Stop price */} {orderType !== 'market' && (
setLimitPrice(e.target.value)} placeholder={`Spot: ₢${commodity.price}`} style={{ width: '100%', padding: '7px 10px', background: 'var(--bg)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', color: 'var(--fg)', fontFamily: 'var(--font-mono)', fontSize: '0.8rem', outline: 'none', boxSizing: 'border-box', }} />
)} {/* Lots */}
setLots(e.target.value)} placeholder="0" min="1" style={{ flex: 1, padding: '7px 10px', background: 'var(--bg)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', color: 'var(--fg)', fontFamily: 'var(--font-mono)', fontSize: '0.8rem', outline: 'none', }} /> {[1, 5, 10, 25].map(n => ( ))}
{/* Summary */}
Price / Unit₢{effectivePrice.toLocaleString()}
Total Units{totalUnits.toLocaleString()}
Notional Value₢{notional.toLocaleString()}
Margin ({contract.marginPct}%)₢{marginRequired.toLocaleString()}
Commission₢{commission.toLocaleString()}
Required ₢{(marginRequired + commission).toLocaleString()}
{error &&
{error}
}
{/* Confirmation */} {confirmation && (
setConfirmation(null)}>
e.stopPropagation()}>

Confirm {confirmation.orderType} Order

Direction: {confirmation.direction.toUpperCase()}
Contract: {contract.symbol} ({contract.name})
Lots: {confirmation.lots} ({confirmation.totalUnits.toLocaleString()} units)
Price: ₢{confirmation.price.toLocaleString()}/unit
Notional: ₢{confirmation.notional.toLocaleString()}
Margin: ₢{confirmation.marginRequired.toLocaleString()}
Commission: ₢{confirmation.commission.toLocaleString()}
)}
); } /* ── Positions Panel (margin account + open positions) ── */ function PositionsPanel({ positions, commodities, credits, usedMargin }) { const totalUnrealized = positions.reduce((sum, p) => { const c = commodities.find(c => c.symbol === p.symbol); if (!c) return sum; const currentValue = c.price * p.totalUnits; const entryValue = p.avgEntry * p.totalUnits; const diff = p.direction === 'long' ? currentValue - entryValue : entryValue - currentValue; return sum + diff; }, 0); const totalPositionValue = positions.reduce((sum, p) => { const c = commodities.find(c => c.symbol === p.symbol); return sum + (c ? c.price * p.totalUnits : 0); }, 0); const marginUtilization = credits > 0 ? ((usedMargin / credits) * 100) : 0; return (
Open Positions = 0 ? 'var(--green)' : 'var(--red)' }}> {totalUnrealized >= 0 ? '▲' : '▼'} ₢{Math.abs(totalUnrealized).toLocaleString()}
{/* Account summary */}
Balance
₢{credits.toLocaleString()}
Used Margin
₢{usedMargin.toLocaleString()}
Exposure
₢{totalPositionValue.toLocaleString()}
{/* Margin utilization bar */}
MARGIN UTILIZATION 80 ? 'var(--red)' : marginUtilization > 50 ? 'var(--accent)' : 'var(--green)' }}> {marginUtilization.toFixed(1)}%
80 ? 'var(--red)' : marginUtilization > 50 ? 'var(--accent)' : 'var(--green)', transition: 'width 0.3s ease', }} />
{/* Position rows */} {positions.length === 0 && (
No open positions
)} {positions.map((p, i) => { const c = commodities.find(c => c.symbol === p.symbol); if (!c) return null; const currentVal = c.price * p.totalUnits; const entryVal = p.avgEntry * p.totalUnits; const pnl = p.direction === 'long' ? currentVal - entryVal : entryVal - currentVal; const pnlPct = entryVal > 0 ? (pnl / p.margin) * 100 : 0; const contract = CONTRACTS.find(ct => ct.symbol === p.symbol); return (
{p.symbol} {p.direction === 'long' ? 'LONG' : 'SHORT'}
= 0 ? 'var(--green)' : 'var(--red)', }}> {pnl >= 0 ? '+' : ''}₢{pnl.toLocaleString()}
{p.lots} lot{p.lots > 1 ? 's' : ''} ({p.totalUnits.toLocaleString()} u.) @ ₢{p.avgEntry} Margin: ₢{p.margin.toLocaleString()}
= 0 ? 'var(--green)' : 'var(--red)', opacity: 0.6, }} />
); })}
); } /* ── Trade Feed ── */ function TradeFeed({ trades }) { return (
Market Trades
{trades.map((t, i) => (
{t.time} {t.lots}× {t.symbol} ₢{t.price.toLocaleString()} {t.side === 'long' ? 'BID' : 'ASK'}
))}
); } /* ────────────────────────────────────────────── Main MarketDemo component ────────────────────────────────────────────── */ function MarketDemo() { const [credits, setCredits] = useState(250000); const [selectedSymbol, setSelectedSymbol] = useState('VLD'); const [category, setCategory] = useState('all'); const [notifications, setNotifications] = useState([]); /* Generate live commodity data */ const [commodities, setCommodities] = useState(() => { return CONTRACTS.map(ct => { const history = []; let p = ct.basePrice; for (let i = 0; i < 60; i++) { p = Math.max(ct.basePrice * 0.7, Math.min(ct.basePrice * 1.3, p + (Math.random() - 0.48) * ct.basePrice * 0.04)); history.push(Math.round(p * 100) / 100); } const price = history[history.length - 1]; const prev = history[history.length - 2]; const settlement = history[Math.floor(history.length / 2)]; const bestBid = Math.round((price - ct.basePrice * 0.005) * 100) / 100; const bestAsk = Math.round((price + ct.basePrice * 0.005) * 100) / 100; return { ...ct, price, prevPrice: prev, change: price - prev, changePct: ((price - prev) / prev) * 100, history, settlement, bestBid, bestAsk, volume: Math.floor(Math.random() * 80000 + 5000), openInterest: Math.floor(Math.random() * 200000 + 10000), open: history[0], high: Math.max(...history), low: Math.min(...history), supply: Math.floor(Math.random() * 50000 + 10000), demand: Math.floor(Math.random() * 60000 + 8000), }; }); }); /* Open positions */ const [positions, setPositions] = useState([ { symbol: 'VLD', direction: 'long', lots: 5, totalUnits: 5000, avgEntry: 13.20, margin: 5280 }, { symbol: 'TRI', direction: 'long', lots: 3, totalUnits: 15000, avgEntry: 4.80, margin: 2160 }, { symbol: 'ARK', direction: 'short', lots: 2, totalUnits: 100, avgEntry: 635, margin: 25400 }, ]); /* Order book for selected */ const selected = commodities.find(c => c.symbol === selectedSymbol) || commodities[0]; const selectedContract = CONTRACTS.find(c => c.symbol === selectedSymbol) || CONTRACTS[0]; const orderBook = useMemo(() => { const p = selected.price; const bids = []; const asks = []; let bidCum = 0; let askCum = 0; for (let i = 0; i < 10; i++) { const bv = Math.floor(Math.random() * 12000 + 500); bidCum += bv; bids.push({ price: Math.max(0.01, Math.round((p - (i + 1) * selectedContract.tickSize * 3) * 100) / 100), volume: bv, cumulative: bidCum }); const av = Math.floor(Math.random() * 12000 + 500); askCum += av; asks.push({ price: Math.round((p + (i + 1) * selectedContract.tickSize * 3) * 100) / 100, volume: av, cumulative: askCum }); } return { bids, asks }; }, [selected.price, selectedSymbol, selectedContract.tickSize]); const usedMargin = positions.reduce((s, p) => s + p.margin, 0); /* Price chart data */ const chartData = useMemo(() => { return selected.history.map((c, i) => { const noise = selected.price * 0.01; return { open: selected.history[Math.max(0, i - 1)] || c, high: c + Math.random() * noise * 2, low: Math.max(0.01, c - Math.random() * noise * 2), close: c, volume: Math.floor(Math.random() * 5000 + 500), }; }); }, [selected]); /* Trade feed */ const [tradeFeed, setTradeFeed] = useState(() => { const feed = []; const syms = CONTRACTS.map(c => c.symbol); for (let i = 0; i < 20; i++) { const sym = syms[Math.floor(Math.random() * syms.length)]; const ct = CONTRACTS.find(c => c.symbol === sym); const c = commodities.find(c => c.symbol === sym); feed.push({ time: `${14}:${String(22 + i).padStart(2, '0')}`, symbol: sym, lots: Math.floor(Math.random() * 10 + 1), price: c ? Math.round(c.price * 100) / 100 : ct.basePrice, side: Math.random() > 0.5 ? 'long' : 'short', }); } return feed.reverse(); }); /* Live price ticks */ useEffect(() => { const interval = setInterval(() => { setCommodities(prev => prev.map(c => { const ct = CONTRACTS.find(x => x.symbol === c.symbol); const delta = (Math.random() - 0.48) * c.price * 0.006; const newPrice = Math.max(0.01, Math.round((c.price + delta) * 100) / 100); const history = [...c.history.slice(1), newPrice]; const change = newPrice - c.prevPrice; const spread = ct.basePrice * 0.005; return { ...c, price: newPrice, history, volume: c.volume + Math.floor(Math.random() * 300), change, changePct: c.prevPrice > 0 ? (change / c.prevPrice) * 100 : 0, high: Math.max(c.high, newPrice), low: Math.min(c.low, newPrice), bestBid: Math.round((newPrice - spread) * 100) / 100, bestAsk: Math.round((newPrice + spread) * 100) / 100, openInterest: c.openInterest + Math.floor(Math.random() * 200 - 80), supply: c.supply + Math.floor(Math.random() * 200 - 100), demand: c.demand + Math.floor(Math.random() * 200 - 80), }; })); }, 2500); return () => clearInterval(interval); }, []); /* New trades in feed */ useEffect(() => { const interval = setInterval(() => { const syms = CONTRACTS.map(c => c.symbol); const sym = syms[Math.floor(Math.random() * syms.length)]; const c = commodities.find(c => c.symbol === sym); const ct = CONTRACTS.find(c => c.symbol === sym); const now = new Date(); setTradeFeed(prev => [{ time: `${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}`, symbol: sym, lots: Math.floor(Math.random() * 15 + 1), price: c ? c.price : ct.basePrice, side: Math.random() > 0.5 ? 'long' : 'short', }, ...prev].slice(0, 40)); }, 3000); return () => clearInterval(interval); }, [commodities]); const addNotif = (msg, color) => { const id = Date.now(); setNotifications(prev => [...prev, { id, msg, color }]); setTimeout(() => setNotifications(prev => prev.filter(n => n.id !== id)), 3500); }; const handleTrade = (direction, symbol, price, lots, totalUnits, marginRequired, commission) => { /* Deduct commission + margin from credits */ setCredits(prev => prev - commission - marginRequired); setPositions(prev => { const existing = prev.find(p => p.symbol === symbol && p.direction === direction); if (existing) { /* Merge into existing position — weighted average entry */ const newUnits = existing.totalUnits + totalUnits; const newAvg = Math.round(((existing.avgEntry * existing.totalUnits) + (price * totalUnits)) / newUnits * 100) / 100; const newMargin = existing.margin + marginRequired; return prev.map(p => p.symbol === symbol && p.direction === direction ? { ...p, lots: p.lots + lots, totalUnits: newUnits, avgEntry: newAvg, margin: newMargin } : p); } return [...prev, { symbol, direction, lots, totalUnits, avgEntry: price, margin: marginRequired }]; }); addNotif( `${direction === 'long' ? 'LONG' : 'SHORT'} ${lots}× ${symbol} (${totalUnits.toLocaleString()} u.) @ ₢${price}`, direction === 'long' ? 'var(--green)' : 'var(--red)' ); const now = new Date(); setTradeFeed(prev => [{ time: `${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}`, symbol, lots, price, side: direction, }, ...prev].slice(0, 40)); }; /* Market stats */ const totalVolume = commodities.reduce((s, c) => s + c.volume, 0); const totalOI = commodities.reduce((s, c) => s + c.openInterest, 0); const advancers = commodities.filter(c => c.change >= 0).length; const decliners = commodities.length - advancers; const tickerItems = commodities.slice(0, 12).map(c => ({ symbol: c.symbol, price: c.price, change: c.change, changePct: c.changePct, })); const spread = selectedContract ? Math.round((selected.bestAsk - selected.bestBid) * 100) / 100 : 0; const spreadPct = selected.bestBid > 0 ? (spread / selected.bestBid) * 100 : 0; const midPrice = Math.round(((selected.bestBid + selected.bestAsk) / 2) * 100) / 100; return (
e.currentTarget.style.color='var(--fg-bright)'} onMouseLeave={e => e.currentTarget.style.color='var(--muted)'}>← Back to Docs {/* Ticker */} {/* Header bar */}
COMMODITIES EXCHANGE
SESSION OPEN
{advancers} ▲ {decliners} ▼
TOTAL VOL {(totalVolume / 1e6).toFixed(2)}M
OPEN INT. {(totalOI / 1e6).toFixed(1)}M
ACCOUNT (₢ = ISK) ₢{credits.toLocaleString()}
{/* Category tabs */} {/* Notifications */}
{notifications.map(n => (
{n.msg}
))}
{/* Main grid: Left (board + chart + feed) | Right (specs + depth + order + positions) */}
{/* Left column */}
{/* Contract board */} {/* Price chart */}
{selected.symbol} {selected.name}
O:₢{selected.open} H:₢{selected.high} L:₢{selected.low} ₢{selected.price.toLocaleString()} = 0 ? 'var(--green)' : 'var(--red)', }}> {selected.change >= 0 ? '+' : ''}{selected.change.toFixed(2)} ({selected.changePct >= 0 ? '+' : ''}{selected.changePct.toFixed(2)}%)
{/* Trade feed */}
{/* Right column */}
{/* Contract spec */} {/* Depth chart */}
Depth of Market
{/* Order book */} {/* Order form */} {/* Positions */}
); } window.GDD.MarketDemo = MarketDemo;