1183 lines
60 KiB
JavaScript
1183 lines
60 KiB
JavaScript
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 <canvas ref={ref} style={{ width, height, display: 'block' }} />;
|
||
}
|
||
|
||
/* ── 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 (
|
||
<div style={{
|
||
overflow: 'hidden', whiteSpace: 'nowrap',
|
||
background: 'rgba(10,16,28,0.95)', borderBottom: '1px solid var(--border)',
|
||
padding: '5px 0', fontFamily: 'var(--font-mono)', fontSize: '0.65rem',
|
||
}}>
|
||
<div style={{ display: 'inline-flex', transform: `translateX(${offset}px)`, willChange: 'transform' }}>
|
||
{dup.map((t, i) => (
|
||
<span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: '5px', padding: '0 14px', borderRight: '1px solid var(--border)' }}>
|
||
<span style={{ color: 'var(--fg-bright)', fontWeight: 600 }}>{t.symbol}</span>
|
||
<span style={{ color: 'var(--fg-dim)' }}>₢{t.price.toLocaleString()}</span>
|
||
<span style={{ color: t.change >= 0 ? 'var(--green)' : 'var(--red)', fontWeight: 600 }}>
|
||
{t.change >= 0 ? '▲' : '▼'} {Math.abs(t.changePct).toFixed(1)}%
|
||
</span>
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── Category Tabs ── */
|
||
function CategoryTabs({ active, onChange }) {
|
||
return (
|
||
<div style={{
|
||
display: 'flex', gap: '2px', padding: 'var(--sp-2) 0',
|
||
borderBottom: '1px solid var(--border)', marginBottom: 'var(--sp-4)',
|
||
overflowX: 'auto',
|
||
}}>
|
||
{CATEGORIES.map(cat => (
|
||
<button key={cat.id} onClick={() => onChange(cat.id)} style={{
|
||
padding: '6px 14px', border: 'none', borderRadius: 'var(--radius-md)',
|
||
background: active === cat.id ? 'var(--accent-bg)' : 'transparent',
|
||
color: active === cat.id ? 'var(--accent)' : 'var(--muted)',
|
||
border: active === cat.id ? '1px solid var(--accent-border)' : '1px solid transparent',
|
||
fontSize: '0.72rem', fontFamily: 'var(--font-mono)', cursor: 'pointer',
|
||
transition: 'all var(--transition-fast)', whiteSpace: 'nowrap',
|
||
fontWeight: active === cat.id ? 600 : 400,
|
||
letterSpacing: '0.03em',
|
||
}}>
|
||
{cat.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── Contract Board — main table ── */
|
||
function ContractBoard({ commodities, selected, onSelect, category }) {
|
||
const filtered = category === 'all' ? commodities : commodities.filter(c => c.category === category);
|
||
return (
|
||
<div style={{
|
||
background: 'var(--surface)', border: '1px solid var(--border)',
|
||
borderRadius: 'var(--radius-lg)', overflow: 'hidden',
|
||
}}>
|
||
{/* Table header */}
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: '90px 120px 80px 60px 70px 70px 80px 90px 70px',
|
||
gap: '4px', alignItems: 'center',
|
||
padding: '8px 12px', borderBottom: '1px solid var(--border)',
|
||
background: 'var(--surface-raised)',
|
||
fontFamily: 'var(--font-mono)', fontSize: '0.6rem', color: 'var(--muted)',
|
||
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||
}}>
|
||
<span>Ticker</span><span>Name</span><span>Last</span><span>Chg%</span>
|
||
<span>Bid</span><span>Ask</span><span>Volume</span><span>Open Int.</span><span />
|
||
</div>
|
||
{/* Rows */}
|
||
<div style={{ maxHeight: '380px', overflowY: 'auto', overscrollBehavior: 'contain' }}>
|
||
{filtered.map(c => {
|
||
const up = c.change >= 0;
|
||
const isSel = selected === c.symbol;
|
||
const spread = c.bestAsk - c.bestBid;
|
||
return (
|
||
<div key={c.symbol} onClick={() => 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',
|
||
}}>
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontWeight: 700, color: 'var(--accent)' }}>{c.symbol}</span>
|
||
<span style={{ color: 'var(--fg-dim)', fontSize: '0.7rem', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{c.name}</span>
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontWeight: 700, color: 'var(--fg-bright)' }}>₢{c.price.toLocaleString()}</span>
|
||
<span style={{
|
||
fontFamily: 'var(--font-mono)', fontSize: '0.65rem', fontWeight: 600,
|
||
color: up ? 'var(--green)' : 'var(--red)',
|
||
background: up ? 'var(--green-bg)' : 'var(--red-bg)',
|
||
padding: '1px 5px', borderRadius: 'var(--radius-pill)', display: 'inline-block',
|
||
}}>
|
||
{up ? '+' : ''}{c.changePct.toFixed(1)}%
|
||
</span>
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--green)' }}>₢{c.bestBid}</span>
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--red)' }}>₢{c.bestAsk}</span>
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--fg-dim)' }}>{(c.volume / 1000).toFixed(1)}K</span>
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--cyan)' }}>{(c.openInterest / 1000).toFixed(1)}K</span>
|
||
<span><Sparkline data={c.history} width={60} height={18} color={up ? 'var(--green)' : 'var(--red)'} /></span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── 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 (
|
||
<div style={{
|
||
background: 'var(--surface)', border: '1px solid var(--border)',
|
||
borderRadius: 'var(--radius-lg)', overflow: 'hidden',
|
||
}}>
|
||
<div style={{
|
||
padding: '10px 14px', borderBottom: '1px solid var(--border)',
|
||
background: 'var(--surface-raised)',
|
||
fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)',
|
||
textTransform: 'uppercase', letterSpacing: '0.06em',
|
||
}}>
|
||
Contract Specification
|
||
</div>
|
||
<div style={{ padding: 'var(--sp-3) var(--sp-4)', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px 16px' }}>
|
||
{[
|
||
['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) => (
|
||
<React.Fragment key={i}>
|
||
<span style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>{label}</span>
|
||
<span style={{ fontSize: '0.7rem', color, fontFamily: 'var(--font-mono)', fontWeight: 500, textAlign: 'right' }}>{value}</span>
|
||
</React.Fragment>
|
||
))}
|
||
</div>
|
||
{/* Supply/Demand bar */}
|
||
<div style={{ padding: '0 var(--sp-4) var(--sp-3)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.6rem', color: 'var(--muted)', marginBottom: '3px', fontFamily: 'var(--font-mono)' }}>
|
||
<span>SUPPLY</span>
|
||
<span style={{ color: commodity.demand > commodity.supply ? 'var(--red)' : 'var(--green)' }}>
|
||
{commodity.demand > commodity.supply ? 'DEFICIT' : 'SURPLUS'}: {Math.abs(commodity.supply - commodity.demand).toLocaleString()}
|
||
</span>
|
||
<span>DEMAND</span>
|
||
</div>
|
||
<div style={{ display: 'flex', height: '6px', borderRadius: '3px', overflow: 'hidden', background: 'var(--surface-raised)' }}>
|
||
<div style={{ width: `${(commodity.supply / Math.max(commodity.supply, commodity.demand)) * 100}%`, background: 'var(--green-dim)', borderRadius: '3px 0 0 3px' }} />
|
||
<div style={{ width: `${(commodity.demand / Math.max(commodity.supply, commodity.demand)) * 100}%`, background: 'var(--red-dim)', borderRadius: '0 3px 3px 0', opacity: 0.7 }} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── 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 <canvas ref={ref} style={{ width: '100%', height: '100px', display: 'block' }} />;
|
||
}
|
||
|
||
/* ── 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 (
|
||
<div style={{
|
||
background: 'var(--surface)', border: '1px solid var(--border)',
|
||
borderRadius: 'var(--radius-lg)', overflow: 'hidden',
|
||
}}>
|
||
<div style={{
|
||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||
padding: '8px 12px', borderBottom: '1px solid var(--border)',
|
||
background: 'var(--surface-raised)',
|
||
}}>
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
|
||
Market Depth
|
||
</span>
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--accent)' }}>
|
||
Spread: ₢{spread} ({spreadPct.toFixed(2)}%)
|
||
</span>
|
||
</div>
|
||
<div style={{
|
||
display: 'grid', gridTemplateColumns: '1fr 1fr 1fr',
|
||
fontFamily: 'var(--font-mono)', fontSize: '0.58rem', color: 'var(--muted)',
|
||
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||
padding: '5px 12px', borderBottom: '1px solid var(--border)',
|
||
}}>
|
||
<span>Price</span><span style={{ textAlign: 'center' }}>Size</span><span style={{ textAlign: 'right' }}>Cum</span>
|
||
</div>
|
||
{/* Asks (reversed) */}
|
||
<div style={{ maxHeight: '160px', overflowY: 'auto', overscrollBehavior: 'contain' }}>
|
||
{[...asks].reverse().map((a, i) => (
|
||
<div key={`a${i}`} style={{ position: 'relative', display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', padding: '2px 12px', fontFamily: 'var(--font-mono)', fontSize: '0.68rem' }}>
|
||
<div style={{ position: 'absolute', inset: 0, background: 'rgba(239,68,68,0.07)', width: `${(a.volume / maxAsk) * 100}%`, right: 0, left: 'auto' }} />
|
||
<span style={{ color: 'var(--red)', position: 'relative' }}>₢{a.price}</span>
|
||
<span style={{ color: 'var(--fg-dim)', textAlign: 'center', position: 'relative' }}>{a.volume.toLocaleString()}</span>
|
||
<span style={{ color: 'var(--muted)', textAlign: 'right', position: 'relative' }}>{a.cumulative.toLocaleString()}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{/* Mid */}
|
||
<div style={{
|
||
padding: '5px 12px', background: 'var(--surface-raised)',
|
||
borderTop: '1px solid var(--border)', borderBottom: '1px solid var(--border)',
|
||
display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 'var(--sp-2)',
|
||
}}>
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.85rem', fontWeight: 700, color: 'var(--fg-bright)' }}>
|
||
₢{((bids[0]?.price || 0 + asks[0]?.price || 0) / 2).toLocaleString()}
|
||
</span>
|
||
<span style={{ fontSize: '0.58rem', color: 'var(--muted)' }}>MID</span>
|
||
</div>
|
||
{/* Bids */}
|
||
<div style={{ maxHeight: '160px', overflowY: 'auto', overscrollBehavior: 'contain' }}>
|
||
{bids.map((b, i) => (
|
||
<div key={`b${i}`} style={{ position: 'relative', display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', padding: '2px 12px', fontFamily: 'var(--font-mono)', fontSize: '0.68rem' }}>
|
||
<div style={{ position: 'absolute', inset: 0, background: 'rgba(34,197,94,0.07)', width: `${(b.volume / maxBid) * 100}%` }} />
|
||
<span style={{ color: 'var(--green)', position: 'relative' }}>₢{b.price}</span>
|
||
<span style={{ color: 'var(--fg-dim)', textAlign: 'center', position: 'relative' }}>{b.volume.toLocaleString()}</span>
|
||
<span style={{ color: 'var(--muted)', textAlign: 'right', position: 'relative' }}>{b.cumulative.toLocaleString()}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── 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 <canvas ref={ref} style={{ width: '100%', height: '240px', display: 'block' }} />;
|
||
}
|
||
|
||
/* ── 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 (
|
||
<div style={{
|
||
background: 'var(--surface)', border: '1px solid var(--border)',
|
||
borderRadius: 'var(--radius-lg)', overflow: 'hidden',
|
||
}}>
|
||
<div style={{
|
||
padding: '8px 12px', borderBottom: '1px solid var(--border)',
|
||
background: 'var(--surface-raised)',
|
||
fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)',
|
||
textTransform: 'uppercase', letterSpacing: '0.06em',
|
||
display: 'flex', justifyContent: 'space-between',
|
||
}}>
|
||
<span>Place Order — {contract.symbol}</span>
|
||
<span>Lot: {contract.lotSize.toLocaleString()} units</span>
|
||
</div>
|
||
<div style={{ padding: 'var(--sp-3) var(--sp-4)' }}>
|
||
{/* Long / Short toggle */}
|
||
<div style={{
|
||
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '3px',
|
||
marginBottom: 'var(--sp-3)', background: 'var(--surface-raised)',
|
||
borderRadius: 'var(--radius-md)', padding: '3px',
|
||
}}>
|
||
{['long', 'short'].map(d => (
|
||
<button key={d} onClick={() => setDirection(d)} style={{
|
||
padding: '7px', border: 'none', borderRadius: 'var(--radius-md)',
|
||
background: direction === d ? (d === 'long' ? 'var(--green-bg)' : 'var(--red-bg)') : 'transparent',
|
||
color: direction === d ? (d === 'long' ? 'var(--green)' : 'var(--red)') : 'var(--muted)',
|
||
fontWeight: 600, fontSize: '0.75rem', cursor: 'pointer', fontFamily: 'var(--font-mono)',
|
||
transition: 'all var(--transition-fast)',
|
||
border: direction === d ? `1px solid ${d === 'long' ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'}` : '1px solid transparent',
|
||
}}>
|
||
{d === 'long' ? '▲ LONG' : '▼ SHORT'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Order type pills */}
|
||
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-3)' }}>
|
||
{['market', 'limit', 'stop'].map(t => (
|
||
<button key={t} onClick={() => setOrderType(t)} style={{
|
||
padding: '3px 10px', border: `1px solid ${orderType === t ? 'var(--accent-border)' : 'var(--border)'}`,
|
||
borderRadius: 'var(--radius-pill)', background: orderType === t ? 'var(--accent-bg)' : 'transparent',
|
||
color: orderType === t ? 'var(--accent)' : 'var(--muted)',
|
||
fontSize: '0.65rem', fontFamily: 'var(--font-mono)', cursor: 'pointer', transition: 'all var(--transition-fast)',
|
||
}}>
|
||
{t.toUpperCase()}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Limit/Stop price */}
|
||
{orderType !== 'market' && (
|
||
<div style={{ marginBottom: 'var(--sp-3)' }}>
|
||
<label style={{ fontSize: '0.6rem', color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '0.04em', display: 'block', marginBottom: '3px' }}>
|
||
{orderType === 'limit' ? 'Limit Price' : 'Stop Price'}
|
||
</label>
|
||
<input type="number" value={limitPrice} onChange={e => 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',
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Lots */}
|
||
<div style={{ marginBottom: 'var(--sp-3)' }}>
|
||
<label style={{ fontSize: '0.6rem', color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '0.04em', display: 'block', marginBottom: '3px' }}>
|
||
Lots (× {contract.lotSize.toLocaleString()} units)
|
||
</label>
|
||
<div style={{ display: 'flex', gap: '3px' }}>
|
||
<input type="number" value={lots} onChange={e => 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 => (
|
||
<button key={n} onClick={() => setLots(String(n))} style={{
|
||
padding: '7px 5px', border: '1px solid var(--border)',
|
||
borderRadius: 'var(--radius-md)', background: 'var(--surface-raised)',
|
||
color: 'var(--fg-dim)', fontSize: '0.6rem', fontFamily: 'var(--font-mono)',
|
||
cursor: 'pointer', transition: 'all var(--transition-fast)',
|
||
}}>{n}</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Summary */}
|
||
<div style={{
|
||
fontFamily: 'var(--font-mono)', fontSize: '0.68rem', color: 'var(--fg-dim)',
|
||
background: 'var(--bg)', borderRadius: 'var(--radius-md)', padding: 'var(--sp-3)',
|
||
marginBottom: 'var(--sp-3)',
|
||
}}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '3px' }}>
|
||
<span>Price / Unit</span><span style={{ color: 'var(--fg-bright)' }}>₢{effectivePrice.toLocaleString()}</span>
|
||
</div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '3px' }}>
|
||
<span>Total Units</span><span style={{ color: 'var(--fg-bright)' }}>{totalUnits.toLocaleString()}</span>
|
||
</div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '3px' }}>
|
||
<span>Notional Value</span><span style={{ color: 'var(--fg-bright)' }}>₢{notional.toLocaleString()}</span>
|
||
</div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '3px' }}>
|
||
<span>Margin ({contract.marginPct}%)</span><span style={{ color: 'var(--cyan)' }}>₢{marginRequired.toLocaleString()}</span>
|
||
</div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '3px' }}>
|
||
<span>Commission</span><span style={{ color: 'var(--accent)' }}>₢{commission.toLocaleString()}</span>
|
||
</div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', paddingTop: '5px', borderTop: '1px solid var(--border)', fontWeight: 700 }}>
|
||
<span>Required</span>
|
||
<span style={{ color: direction === 'long' ? 'var(--green)' : 'var(--red)' }}>
|
||
₢{(marginRequired + commission).toLocaleString()}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{error && <div style={{ fontSize: '0.7rem', color: 'var(--red)', marginBottom: 'var(--sp-2)', padding: '5px 10px', background: 'var(--red-bg)', borderRadius: 'var(--radius-md)' }}>{error}</div>}
|
||
|
||
<button onClick={handleSubmit} disabled={!lots || !canAfford} style={{
|
||
width: '100%', padding: '9px', border: 'none', borderRadius: 'var(--radius-md)',
|
||
background: direction === 'long' ? 'var(--green-dim)' : 'var(--red-dim)',
|
||
color: 'var(--fg-bright)', fontWeight: 700, fontSize: '0.8rem',
|
||
cursor: canAfford && lots ? 'pointer' : 'not-allowed',
|
||
opacity: canAfford && lots ? 1 : 0.4,
|
||
fontFamily: 'var(--font-mono)', transition: 'all var(--transition-fast)',
|
||
}}>
|
||
{direction === 'long' ? '▲ BUY' : '▼ SELL'} {numLots}× {contract.symbol}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Confirmation */}
|
||
{confirmation && (
|
||
<div style={{
|
||
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 2000,
|
||
}} onClick={() => setConfirmation(null)}>
|
||
<div style={{
|
||
background: 'var(--surface)', border: '1px solid var(--border)',
|
||
borderRadius: 'var(--radius-lg)', padding: 'var(--sp-5)', minWidth: 320,
|
||
}} onClick={e => e.stopPropagation()}>
|
||
<h3 style={{ marginBottom: 'var(--sp-4)', fontSize: '0.9rem' }}>Confirm {confirmation.orderType} Order</h3>
|
||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.78rem', lineHeight: 2 }}>
|
||
<div><span style={{ color: 'var(--muted)' }}>Direction:</span> <span style={{ color: confirmation.direction === 'long' ? 'var(--green)' : 'var(--red)', fontWeight: 700 }}>{confirmation.direction.toUpperCase()}</span></div>
|
||
<div><span style={{ color: 'var(--muted)' }}>Contract:</span> <span style={{ color: 'var(--fg-bright)' }}>{contract.symbol} ({contract.name})</span></div>
|
||
<div><span style={{ color: 'var(--muted)' }}>Lots:</span> <span style={{ color: 'var(--fg-bright)' }}>{confirmation.lots} ({confirmation.totalUnits.toLocaleString()} units)</span></div>
|
||
<div><span style={{ color: 'var(--muted)' }}>Price:</span> <span style={{ color: 'var(--fg-bright)' }}>₢{confirmation.price.toLocaleString()}/unit</span></div>
|
||
<div><span style={{ color: 'var(--muted)' }}>Notional:</span> <span style={{ color: 'var(--fg-bright)' }}>₢{confirmation.notional.toLocaleString()}</span></div>
|
||
<div><span style={{ color: 'var(--muted)' }}>Margin:</span> <span style={{ color: 'var(--cyan)' }}>₢{confirmation.marginRequired.toLocaleString()}</span></div>
|
||
<div><span style={{ color: 'var(--muted)' }}>Commission:</span> <span style={{ color: 'var(--accent)' }}>₢{confirmation.commission.toLocaleString()}</span></div>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 'var(--sp-3)', marginTop: 'var(--sp-5)' }}>
|
||
<button onClick={confirmTrade} style={{
|
||
flex: 1, padding: '9px', border: 'none', borderRadius: 'var(--radius-md)',
|
||
background: confirmation.direction === 'long' ? 'var(--green-dim)' : 'var(--red-dim)',
|
||
color: 'var(--fg-bright)', fontWeight: 700, fontSize: '0.8rem', cursor: 'pointer',
|
||
fontFamily: 'var(--font-mono)',
|
||
}}>CONFIRM</button>
|
||
<button onClick={() => setConfirmation(null)} style={{
|
||
flex: 1, padding: '9px', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)',
|
||
background: 'var(--surface-raised)', color: 'var(--fg-dim)', fontSize: '0.8rem',
|
||
cursor: 'pointer', fontFamily: 'var(--font-body)',
|
||
}}>Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── 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 (
|
||
<div style={{
|
||
background: 'var(--surface)', border: '1px solid var(--border)',
|
||
borderRadius: 'var(--radius-lg)', overflow: 'hidden',
|
||
}}>
|
||
<div style={{
|
||
padding: '8px 12px', borderBottom: '1px solid var(--border)',
|
||
background: 'var(--surface-raised)',
|
||
fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)',
|
||
textTransform: 'uppercase', letterSpacing: '0.06em',
|
||
display: 'flex', justifyContent: 'space-between',
|
||
}}>
|
||
<span>Open Positions</span>
|
||
<span style={{ color: totalUnrealized >= 0 ? 'var(--green)' : 'var(--red)' }}>
|
||
{totalUnrealized >= 0 ? '▲' : '▼'} ₢{Math.abs(totalUnrealized).toLocaleString()}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Account summary */}
|
||
<div style={{ padding: 'var(--sp-3) var(--sp-4)' }}>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 'var(--sp-2)', marginBottom: 'var(--sp-3)' }}>
|
||
<div style={{ background: 'var(--bg)', borderRadius: 'var(--radius-md)', padding: '8px 10px' }}>
|
||
<div style={{ fontSize: '0.55rem', color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: '1px' }}>Balance</div>
|
||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.85rem', fontWeight: 700, color: 'var(--fg-bright)' }}>₢{credits.toLocaleString()}</div>
|
||
</div>
|
||
<div style={{ background: 'var(--bg)', borderRadius: 'var(--radius-md)', padding: '8px 10px' }}>
|
||
<div style={{ fontSize: '0.55rem', color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: '1px' }}>Used Margin</div>
|
||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.85rem', fontWeight: 700, color: 'var(--cyan)' }}>₢{usedMargin.toLocaleString()}</div>
|
||
</div>
|
||
<div style={{ background: 'var(--bg)', borderRadius: 'var(--radius-md)', padding: '8px 10px' }}>
|
||
<div style={{ fontSize: '0.55rem', color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: '1px' }}>Exposure</div>
|
||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.85rem', fontWeight: 700, color: 'var(--accent)' }}>₢{totalPositionValue.toLocaleString()}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Margin utilization bar */}
|
||
<div style={{ marginBottom: 'var(--sp-3)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.55rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', marginBottom: '3px' }}>
|
||
<span>MARGIN UTILIZATION</span>
|
||
<span style={{ color: marginUtilization > 80 ? 'var(--red)' : marginUtilization > 50 ? 'var(--accent)' : 'var(--green)' }}>
|
||
{marginUtilization.toFixed(1)}%
|
||
</span>
|
||
</div>
|
||
<div style={{ height: '4px', background: 'var(--surface-raised)', borderRadius: '2px', overflow: 'hidden' }}>
|
||
<div style={{
|
||
height: '100%', borderRadius: '2px',
|
||
width: `${Math.min(marginUtilization, 100)}%`,
|
||
background: marginUtilization > 80 ? 'var(--red)' : marginUtilization > 50 ? 'var(--accent)' : 'var(--green)',
|
||
transition: 'width 0.3s ease',
|
||
}} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Position rows */}
|
||
{positions.length === 0 && (
|
||
<div style={{ textAlign: 'center', padding: 'var(--sp-4)', color: 'var(--muted)', fontSize: '0.75rem' }}>
|
||
No open positions
|
||
</div>
|
||
)}
|
||
{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 (
|
||
<div key={i} style={{
|
||
padding: '6px 0', borderBottom: '1px solid var(--border)', fontSize: '0.72rem',
|
||
}}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '3px' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-2)' }}>
|
||
<span style={{ fontWeight: 700, color: 'var(--fg-bright)' }}>{p.symbol}</span>
|
||
<span style={{
|
||
fontSize: '0.55rem', fontFamily: 'var(--font-mono)', fontWeight: 600,
|
||
color: p.direction === 'long' ? 'var(--green)' : 'var(--red)',
|
||
background: p.direction === 'long' ? 'var(--green-bg)' : 'var(--red-bg)',
|
||
padding: '1px 5px', borderRadius: 'var(--radius-pill)',
|
||
}}>
|
||
{p.direction === 'long' ? 'LONG' : 'SHORT'}
|
||
</span>
|
||
</div>
|
||
<span style={{
|
||
fontFamily: 'var(--font-mono)', fontWeight: 700,
|
||
color: pnl >= 0 ? 'var(--green)' : 'var(--red)',
|
||
}}>
|
||
{pnl >= 0 ? '+' : ''}₢{pnl.toLocaleString()}
|
||
</span>
|
||
</div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: 'var(--font-mono)', fontSize: '0.62rem', color: 'var(--muted)' }}>
|
||
<span>{p.lots} lot{p.lots > 1 ? 's' : ''} ({p.totalUnits.toLocaleString()} u.) @ ₢{p.avgEntry}</span>
|
||
<span>Margin: ₢{p.margin.toLocaleString()}</span>
|
||
</div>
|
||
<div style={{ marginTop: '3px', height: '3px', background: 'var(--surface-raised)', borderRadius: '2px', overflow: 'hidden' }}>
|
||
<div style={{
|
||
height: '100%', borderRadius: '2px',
|
||
width: `${Math.min(Math.abs(pnlPct), 100)}%`,
|
||
background: pnl >= 0 ? 'var(--green)' : 'var(--red)',
|
||
opacity: 0.6,
|
||
}} />
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── Trade Feed ── */
|
||
function TradeFeed({ trades }) {
|
||
return (
|
||
<div style={{
|
||
background: 'var(--surface)', border: '1px solid var(--border)',
|
||
borderRadius: 'var(--radius-lg)', overflow: 'hidden',
|
||
}}>
|
||
<div style={{
|
||
padding: '8px 12px', borderBottom: '1px solid var(--border)',
|
||
background: 'var(--surface-raised)',
|
||
fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)',
|
||
textTransform: 'uppercase', letterSpacing: '0.06em',
|
||
}}>
|
||
Market Trades
|
||
</div>
|
||
<div style={{ maxHeight: '180px', overflowY: 'auto', overscrollBehavior: 'contain' }}>
|
||
{trades.map((t, i) => (
|
||
<div key={i} style={{
|
||
display: 'grid', gridTemplateColumns: '42px 1fr 70px 50px',
|
||
padding: '4px 12px', fontFamily: 'var(--font-mono)', fontSize: '0.65rem',
|
||
borderBottom: '1px solid var(--border)',
|
||
background: i < 2 ? (t.side === 'long' ? 'rgba(34,197,94,0.03)' : 'rgba(239,68,68,0.03)') : 'transparent',
|
||
}}>
|
||
<span style={{ color: 'var(--muted)' }}>{t.time}</span>
|
||
<span style={{ color: 'var(--fg-dim)' }}>{t.lots}× {t.symbol}</span>
|
||
<span style={{ color: 'var(--fg-bright)', textAlign: 'right' }}>₢{t.price.toLocaleString()}</span>
|
||
<span style={{ color: t.side === 'long' ? 'var(--green)' : 'var(--red)', textAlign: 'right', fontWeight: 600, fontSize: '0.58rem' }}>
|
||
{t.side === 'long' ? 'BID' : 'ASK'}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────
|
||
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 (
|
||
<div className="content-inner" style={{ maxWidth: '1260px' }}>
|
||
<a href="#overview" style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '0.75rem', fontFamily: 'var(--font-mono)', color: 'var(--muted)', textDecoration: 'none', marginBottom: 'var(--sp-2)', transition: 'color 0.15s' }} onMouseEnter={e => e.currentTarget.style.color='var(--fg-bright)'} onMouseLeave={e => e.currentTarget.style.color='var(--muted)'}>← Back to Docs</a>
|
||
{/* Ticker */}
|
||
<TickerTape items={tickerItems} />
|
||
|
||
{/* Header bar */}
|
||
<div style={{
|
||
display: 'flex', alignItems: 'center', gap: 'var(--sp-3)',
|
||
padding: 'var(--sp-3) 0', fontFamily: 'var(--font-mono)', fontSize: '0.7rem',
|
||
borderBottom: '1px solid var(--border)', marginBottom: 'var(--sp-3)',
|
||
flexWrap: 'wrap',
|
||
}}>
|
||
<span style={{ fontWeight: 700, color: 'var(--fg-bright)', fontSize: '0.85rem', letterSpacing: '0.03em' }}>COMMODITIES EXCHANGE</span>
|
||
<div style={{ width: 1, height: 14, background: 'var(--border)' }} />
|
||
<span style={{ color: 'var(--muted)' }}>SESSION</span>
|
||
<span style={{ color: 'var(--green)', fontWeight: 600 }}>OPEN</span>
|
||
<div style={{ width: 1, height: 14, background: 'var(--border)' }} />
|
||
<span style={{ color: 'var(--green)' }}>{advancers} ▲</span>
|
||
<span style={{ color: 'var(--red)' }}>{decliners} ▼</span>
|
||
<div style={{ width: 1, height: 14, background: 'var(--border)' }} />
|
||
<span style={{ color: 'var(--muted)' }}>TOTAL VOL</span>
|
||
<span style={{ color: 'var(--fg-bright)' }}>{(totalVolume / 1e6).toFixed(2)}M</span>
|
||
<div style={{ width: 1, height: 14, background: 'var(--border)' }} />
|
||
<span style={{ color: 'var(--muted)' }}>OPEN INT.</span>
|
||
<span style={{ color: 'var(--cyan)' }}>{(totalOI / 1e6).toFixed(1)}M</span>
|
||
<div style={{ width: 1, height: 14, background: 'var(--border)' }} />
|
||
<span style={{ color: 'var(--muted)' }}>ACCOUNT (₢ = ISK)</span>
|
||
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>₢{credits.toLocaleString()}</span>
|
||
</div>
|
||
|
||
{/* Category tabs */}
|
||
<CategoryTabs active={category} onChange={setCategory} />
|
||
|
||
{/* Notifications */}
|
||
<div style={{ position: 'fixed', top: 'var(--sp-4)', right: 'var(--sp-4)', zIndex: 1000, display: 'flex', flexDirection: 'column', gap: 'var(--sp-2)' }}>
|
||
{notifications.map(n => (
|
||
<div key={n.id} style={{
|
||
background: 'var(--surface)', border: `1px solid ${n.color}40`,
|
||
borderRadius: 'var(--radius-md)', padding: 'var(--sp-2) var(--sp-4)',
|
||
fontSize: '0.72rem', color: n.color, fontFamily: 'var(--font-mono)',
|
||
boxShadow: 'var(--shadow-md)',
|
||
}}>
|
||
{n.msg}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Main grid: Left (board + chart + feed) | Right (specs + depth + order + positions) */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 380px', gap: 'var(--sp-4)', alignItems: 'start' }}>
|
||
|
||
{/* Left column */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sp-4)' }}>
|
||
{/* Contract board */}
|
||
<ContractBoard commodities={commodities} selected={selectedSymbol} onSelect={setSelectedSymbol} category={category} />
|
||
|
||
{/* Price chart */}
|
||
<div style={{
|
||
background: 'var(--surface)', border: '1px solid var(--border)',
|
||
borderRadius: 'var(--radius-lg)', overflow: 'hidden',
|
||
}}>
|
||
<div style={{
|
||
padding: '8px 14px', borderBottom: '1px solid var(--border)',
|
||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 'var(--sp-2)' }}>
|
||
<span style={{ fontWeight: 700, color: 'var(--accent)', fontSize: '0.9rem', fontFamily: 'var(--font-mono)' }}>{selected.symbol}</span>
|
||
<span style={{ fontSize: '0.7rem', color: 'var(--fg-dim)' }}>{selected.name}</span>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 'var(--sp-3)' }}>
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)' }}>
|
||
O:₢{selected.open} H:₢{selected.high} L:₢{selected.low}
|
||
</span>
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '1.1rem', fontWeight: 700, color: 'var(--fg-bright)' }}>
|
||
₢{selected.price.toLocaleString()}
|
||
</span>
|
||
<span style={{
|
||
fontFamily: 'var(--font-mono)', fontSize: '0.7rem', fontWeight: 600,
|
||
color: selected.change >= 0 ? 'var(--green)' : 'var(--red)',
|
||
}}>
|
||
{selected.change >= 0 ? '+' : ''}{selected.change.toFixed(2)} ({selected.changePct >= 0 ? '+' : ''}{selected.changePct.toFixed(2)}%)
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div style={{ padding: 'var(--sp-3)' }}>
|
||
<PriceChart data={chartData} symbol={selectedSymbol} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Trade feed */}
|
||
<TradeFeed trades={tradeFeed} />
|
||
</div>
|
||
|
||
{/* Right column */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sp-4)', position: 'sticky', top: 'var(--sp-4)' }}>
|
||
{/* Contract spec */}
|
||
<ContractSpec contract={selectedContract} commodity={selected} />
|
||
|
||
{/* Depth chart */}
|
||
<div style={{
|
||
background: 'var(--surface)', border: '1px solid var(--border)',
|
||
borderRadius: 'var(--radius-lg)', overflow: 'hidden',
|
||
padding: 'var(--sp-3)',
|
||
}}>
|
||
<div style={{
|
||
fontFamily: 'var(--font-mono)', fontSize: '0.6rem', color: 'var(--muted)',
|
||
textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 'var(--sp-2)',
|
||
}}>
|
||
Depth of Market
|
||
</div>
|
||
<DepthChart bids={orderBook.bids} asks={orderBook.asks} midPrice={midPrice} />
|
||
</div>
|
||
|
||
{/* Order book */}
|
||
<OrderBook
|
||
bids={orderBook.bids} asks={orderBook.asks}
|
||
spread={spread} spreadPct={spreadPct}
|
||
/>
|
||
|
||
{/* Order form */}
|
||
<OrderForm
|
||
contract={selectedContract}
|
||
commodity={selected}
|
||
credits={credits}
|
||
positions={positions}
|
||
onTrade={handleTrade}
|
||
/>
|
||
|
||
{/* Positions */}
|
||
<PositionsPanel
|
||
positions={positions}
|
||
commodities={commodities}
|
||
credits={credits}
|
||
usedMargin={usedMargin}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
window.GDD.MarketDemo = MarketDemo;
|