Almaatla commited on
Commit
e1bf013
·
verified ·
1 Parent(s): 4cf70f7

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +380 -447
index.html CHANGED
@@ -1,467 +1,400 @@
1
- <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Market Simulator v2025</title>
7
-
8
- <!-- Tailwind CSS -->
9
- <script src="https://cdn.tailwindcss.com"></script>
10
-
11
- <!-- React Dependencies -->
12
- <script crossorigin src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"></script>
13
- <script crossorigin src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
14
-
15
- <!-- Recharts Dependencies -->
16
- <script src="https://unpkg.com/prop-types/prop-types.min.js"></script>
17
- <script src="https://unpkg.com/recharts@1.8.5/umd/Recharts.min.js"></script>
18
-
19
- <!-- Lucide Icons -->
20
- <script src="https://unpkg.com/lucide@0.294.0/dist/umd/lucide.min.js"></script>
21
-
22
- <!-- Babel -->
23
- <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
24
-
25
  <style>
26
- body { background-color: #030712; color: #f3f4f6; overflow: hidden; margin: 0; }
27
- ::-webkit-scrollbar { width: 6px; height: 6px; }
28
- ::-webkit-scrollbar-track { background: #1f2937; }
29
- ::-webkit-scrollbar-thumb { background: #4b5563; border-radius: 3px; }
30
- ::-webkit-scrollbar-thumb:hover { background: #6b7280; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  </style>
32
  </head>
33
 
34
  <body>
35
- <div id="root"></div>
36
-
37
- <script type="text/babel">
38
- // --- SAFE GLOBALS EXTRACTION ---
39
- const { useState, useEffect, useReducer, createContext, useContext, useRef, useMemo } = React;
40
- const Recharts = window.Recharts;
41
- const { ComposedChart, Line, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, ReferenceLine, ReferenceDot } = Recharts;
42
-
43
- // Icons rendered via lucide.createElement(name)
44
- const Lucide = window.lucide;
45
-
46
- const Icon = ({ name, size = 16, className = "" }) => {
47
- if (!Lucide || !Lucide.createElement || !Lucide.icons) return <span />;
48
-
49
- // lucide icons keys are PascalCase in the bundle (e.g., "Activity", "MousePointer")
50
- const iconNode = Lucide.icons[name];
51
- if (!iconNode) return <span />;
52
-
53
- const el = Lucide.createElement(iconNode, {
54
- width: size,
55
- height: size,
56
- class: className
57
- });
58
-
59
- return <span dangerouslySetInnerHTML={{ __html: el.outerHTML }} />;
60
- };
61
-
62
- // Use PascalCase names here:
63
- const Activity = (p) => <Icon name="Activity" {...p} />;
64
- const Trophy = (p) => <Icon name="Trophy" {...p} />;
65
- const MousePointer = (p) => <Icon name="MousePointer" {...p} />;
66
- const Pencil = (p) => <Icon name="Pencil" {...p} />;
67
- const Minus = (p) => <Icon name="Minus" {...p} />;
68
- const Trash2 = (p) => <Icon name="Trash2" {...p} />;
69
-
70
-
71
- // --- CONSTANTS ---
72
- const INITIAL_CASH = 10000;
73
- const COMMISSION = 5;
74
-
75
- // --- GAME STATE REDUCER ---
76
- const createLog = (day, message, type = "INFO") => ({
77
- id: Math.random().toString(36).substr(2, 9),
78
- day,
79
- message,
80
- type,
81
- timestamp: Date.now()
82
- });
83
-
84
- const initialState = {
85
- market: [],
86
- currentDay: 0,
87
- orders: [],
88
- position: { shares: 0, avgCost: 0 },
89
- account: { cash: INITIAL_CASH, equity: INITIAL_CASH, realizedPnL: 0 },
90
- drawings: [],
91
- logs: [],
92
- leaderboard: [],
93
- connected: false, // CHANGED: used for UI + websocket state
94
- wsStatus: "DISCONNECTED" // CHANGED: DISCONNECTED | CONNECTING | CONNECTED | ERROR
95
- };
96
-
97
- const gameReducer = (state, action) => {
98
- switch (action.type) {
99
- case "INIT_MARKET":
100
- return {
101
- ...state,
102
- market: action.payload.market || [],
103
- connected: true,
104
- wsStatus: "CONNECTED", // CHANGED
105
- logs: [...state.logs, createLog(0, "Connected to Market Server")]
106
- };
107
-
108
- case "WS_STATUS": // CHANGED
109
- return { ...state, wsStatus: action.payload, connected: action.payload === "CONNECTED" };
110
-
111
- case "TICK": {
112
- const dayIdx = action.payload.day;
113
- if (!state.market?.[dayIdx]) return state;
114
- const currentPrice = state.market[dayIdx].close;
115
- const newEquity = state.account.cash + state.position.shares * currentPrice;
116
-
117
- return {
118
- ...state,
119
- currentDay: dayIdx,
120
- leaderboard: action.payload.leaderboard || [],
121
- account: { ...state.account, equity: newEquity }
122
- };
123
- }
124
-
125
- case "PLACE_ORDER": {
126
- const { side, shares } = action.payload;
127
- const price = state.market[state.currentDay]?.close;
128
- if (!price || shares <= 0) return state;
129
-
130
- const cost = shares * price;
131
- let { cash, realizedPnL } = state.account;
132
- let { shares: posShares, avgCost } = state.position;
133
-
134
- if (side === "BUY") {
135
- if (cash < cost + COMMISSION) return state;
136
- cash -= (cost + COMMISSION);
137
- const totalVal = posShares * avgCost + cost;
138
- posShares += shares;
139
- avgCost = posShares > 0 ? totalVal / posShares : 0;
140
- } else {
141
- if (posShares < shares) return state;
142
- const revenue = cost - COMMISSION;
143
- const profit = (price - avgCost) * shares;
144
- cash += revenue;
145
- posShares -= shares;
146
- realizedPnL += profit;
147
- if (posShares === 0) avgCost = 0;
148
- }
149
-
150
- const newEquity = cash + posShares * price;
151
-
152
- return {
153
- ...state,
154
- orders: [
155
- ...state.orders,
156
- {
157
- id: Date.now(),
158
- side,
159
- shares,
160
- execDay: state.currentDay,
161
- price,
162
- status: "FILLED"
163
- }
164
- ],
165
- account: { cash, equity: newEquity, realizedPnL },
166
- position: { shares: posShares, avgCost },
167
- logs: [...state.logs, createLog(state.currentDay, `${side} ${shares} @ ${price.toFixed(2)}`, "TRADE")]
168
- };
169
- }
170
-
171
- case "ADD_DRAWING":
172
- return { ...state, drawings: [...state.drawings, action.payload] };
173
-
174
- case "REMOVE_DRAWING":
175
- return { ...state, drawings: state.drawings.filter(d => d.id !== action.payload) };
176
-
177
- default:
178
- return state;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  };
181
 
182
- // --- CONTEXT ---
183
- const GameContext = createContext(null);
184
-
185
- const GameProvider = ({ children }) => {
186
- const [state, dispatch] = useReducer(gameReducer, initialState);
187
- const socketRef = useRef(null);
188
- const clientId = useRef("user_" + Math.random().toString(36).substr(2, 5));
189
- const stateRef = useRef(state);
190
- useEffect(() => { stateRef.current = state; }, [state]);
191
-
192
- useEffect(() => {
193
- // --- FIX: correct ws/wss URL formatting for HF Spaces ---
194
- const isLocalhost =
195
- window.location.hostname === "localhost" ||
196
- window.location.hostname === "127.0.0.1";
197
-
198
- const protocol = isLocalhost ? "ws" : "wss";
199
- const host = window.location.host;
200
-
201
- // CHANGED: must include :// and correct path: /ws/{client_id}
202
- const wsUrl = `${protocol}://${host}/ws/${clientId.current}`;
203
- console.log("Attempting WS Connection:", wsUrl);
204
-
205
- dispatch({ type: "WS_STATUS", payload: "CONNECTING" }); // CHANGED
206
-
207
- const ws = new WebSocket(wsUrl);
208
- socketRef.current = ws;
209
-
210
- ws.onopen = () => {
211
- console.log("WS Connected");
212
- // Backend sends INIT immediately after connect; keep UI in CONNECTING until INIT
213
- // dispatch({ type: "WS_STATUS", payload: "CONNECTED" });
214
- };
215
-
216
- ws.onerror = (e) => {
217
- console.error("WS Error", e);
218
- dispatch({ type: "WS_STATUS", payload: "ERROR" }); // CHANGED
219
- };
220
-
221
- ws.onclose = () => {
222
- dispatch({ type: "WS_STATUS", payload: "DISCONNECTED" }); // CHANGED
223
- };
224
-
225
- ws.onmessage = (event) => {
226
- try {
227
- const msg = JSON.parse(event.data);
228
-
229
- // CHANGED: exact message types expected from backend
230
- if (msg.type === "INIT") {
231
- dispatch({ type: "INIT_MARKET", payload: msg.payload });
232
- } else if (msg.type === "TICK") {
233
- dispatch({ type: "TICK", payload: msg.payload });
234
- }
235
- } catch (e) {
236
- console.error("Parse Error", e);
237
- }
238
- };
239
-
240
- // Heartbeat: report equity/roi to server
241
- const hb = setInterval(() => {
242
- if (ws.readyState === WebSocket.OPEN) {
243
- const eq = stateRef.current.account.equity;
244
- const roi = ((eq - INITIAL_CASH) / INITIAL_CASH) * 100;
245
-
246
- ws.send(JSON.stringify({
247
- type: "UPDATE_EQUITY", // must match backend
248
- payload: {
249
- name: clientId.current,
250
- equity: eq,
251
- roi: roi
252
- }
253
- }));
254
- }
255
- }, 1000);
256
-
257
- return () => {
258
- clearInterval(hb);
259
- try { ws.close(); } catch (_) {}
260
- };
261
- }, []);
262
-
263
- return (
264
- <GameContext.Provider value={{ state, dispatch }}>
265
- {children}
266
- </GameContext.Provider>
267
- );
268
- };
269
 
270
- // --- COMPONENTS ---
271
- const ChartArea = () => {
272
- const { state, dispatch } = useContext(GameContext);
273
- const { market, currentDay, drawings, orders } = state;
274
- const [mode, setMode] = useState("NONE");
275
- const [tempPoint, setTempPoint] = useState(null);
276
 
277
- if (!ComposedChart) return <div className="p-10 text-red-500">Error: Recharts library failed to load.</div>;
278
- if (!market || market.length === 0) {
279
- return <div className="flex h-full items-center justify-center text-blue-400 animate-pulse">Connecting to Server...</div>;
 
280
  }
281
 
282
- const visibleData = market.slice(Math.max(0, currentDay - 60), currentDay + 1);
283
-
284
- const handleChartClick = (e) => {
285
- if (!e || !e.activePayload) return;
286
- const p = { x: e.activePayload[0].payload.i, y: e.activePayload[0].payload.close };
287
-
288
- if (mode === "HORIZ") {
289
- dispatch({ type: "ADD_DRAWING", payload: { id: Date.now(), type: "HORIZ", points: [p], color: "#d8b4fe" } });
290
- setMode("NONE");
291
- } else if (mode === "TREND") {
292
- if (!tempPoint) setTempPoint(p);
293
- else {
294
- dispatch({ type: "ADD_DRAWING", payload: { id: Date.now(), type: "TREND", points: [tempPoint, p], color: "#38bdf8" } });
295
- setTempPoint(null);
296
- setMode("NONE");
297
- }
298
- }
299
- };
300
-
301
- return (
302
- <div className="relative w-full h-full bg-gray-900 border border-gray-800 rounded overflow-hidden group">
303
- <div className="absolute top-2 left-2 z-20 flex gap-1 bg-black/60 backdrop-blur p-1 rounded opacity-0 group-hover:opacity-100 transition-opacity">
304
- <button onClick={() => setMode("NONE")} className={`p-1 rounded ${mode === "NONE" ? "bg-blue-600" : "hover:bg-gray-700"}`}><MousePointer size={14} /></button>
305
- <button onClick={() => setMode("TREND")} className={`p-1 rounded ${mode === "TREND" ? "bg-blue-600" : "hover:bg-gray-700"}`}><Pencil size={14} /></button>
306
- <button onClick={() => setMode("HORIZ")} className={`p-1 rounded ${mode === "HORIZ" ? "bg-blue-600" : "hover:bg-gray-700"}`}><Minus size={14} /></button>
307
- <button onClick={() => drawings.forEach(d => dispatch({ type: "REMOVE_DRAWING", payload: d.id }))} className="p-1 rounded hover:bg-red-900/50 text-red-400"><Trash2 size={14} /></button>
308
- </div>
309
-
310
- <ResponsiveContainer width="100%" height="100%">
311
- <ComposedChart data={visibleData} onClick={handleChartClick} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
312
- <XAxis dataKey="i" hide domain={["dataMin", "dataMax"]} />
313
- <YAxis orientation="right" domain={["auto", "auto"]} stroke="#4b5563" tick={{ fontSize: 10 }} />
314
- <Tooltip
315
- contentStyle={{ backgroundColor: "#111827", borderColor: "#374151" }}
316
- itemStyle={{ color: "#fff" }}
317
- labelStyle={{ display: "none" }}
318
- formatter={(v) => (typeof v === "number" ? v.toFixed(2) : v)}
319
- isAnimationActive={false}
320
- />
321
- <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" vertical={false} />
322
- <Bar dataKey="close" barSize={3} fill="#3b82f6" isAnimationActive={false} />
323
-
324
- {drawings.map(d => {
325
- if (d.type === "HORIZ") return <ReferenceLine key={d.id} y={d.points[0].y} stroke={d.color} />;
326
- if (d.type === "TREND") return <ReferenceLine key={d.id} segment={[
327
- { x: d.points[0].x, y: d.points[0].y },
328
- { x: d.points[1].x, y: d.points[1].y }
329
- ]} stroke={d.color} />;
330
- return null;
331
- })}
332
-
333
- {orders
334
- .filter(o => o.execDay >= (visibleData[0]?.i ?? 0))
335
- .map(o => (
336
- <ReferenceDot
337
- key={o.id}
338
- x={o.execDay}
339
- y={o.price}
340
- r={4}
341
- fill={o.side === "BUY" ? "#22c55e" : "#ef4444"}
342
- stroke="none"
343
- />
344
- ))}
345
- </ComposedChart>
346
- </ResponsiveContainer>
347
- </div>
348
- );
349
- };
350
 
351
- const Dashboard = () => {
352
- const { state, dispatch } = useContext(GameContext);
353
- const [size, setSize] = useState(100);
354
- const price = state.market[state.currentDay]?.close || 0;
355
-
356
- return (
357
- <div className="w-64 bg-gray-900 border-l border-gray-800 flex flex-col">
358
- <div className="p-4 border-b border-gray-800">
359
- <div className="text-2xl font-bold text-white mb-4 text-center tracking-wider">{price.toFixed(2)}</div>
360
-
361
- <div className="flex gap-2 mb-4">
362
- <button
363
- onClick={() => dispatch({ type: "PLACE_ORDER", payload: { side: "BUY", shares: parseInt(size) } })}
364
- className="flex-1 bg-green-600 hover:bg-green-500 text-white font-bold py-3 rounded transition"
365
- >
366
- BUY
367
- </button>
368
- <button
369
- onClick={() => dispatch({ type: "PLACE_ORDER", payload: { side: "SELL", shares: parseInt(size) } })}
370
- className="flex-1 bg-red-600 hover:bg-red-500 text-white font-bold py-3 rounded transition"
371
- >
372
- SELL
373
- </button>
374
- </div>
375
-
376
- <div className="mb-4">
377
- <label className="text-xs text-gray-500 font-bold uppercase block mb-1">Quantity</label>
378
- <input
379
- type="number"
380
- value={size}
381
- onChange={(e) => setSize(e.target.value)}
382
- className="w-full bg-gray-800 border border-gray-700 text-white p-2 rounded text-center font-mono"
383
- />
384
- </div>
385
-
386
- <div className="grid grid-cols-2 gap-2 text-xs">
387
- <div className="text-gray-500">Cash</div><div className="text-right text-gray-300">{state.account.cash.toFixed(0)}</div>
388
- <div className="text-gray-500">Equity</div><div className="text-right text-blue-400 font-bold">{state.account.equity.toFixed(0)}</div>
389
- </div>
390
- </div>
391
-
392
- <div className="flex-1 flex flex-col min-h-0">
393
- <div className="p-2 bg-gray-800/50 text-xs font-bold text-gray-400 uppercase flex items-center gap-2">
394
- <Trophy size={12} className="text-yellow-500" />
395
- Live Rankings
396
- </div>
397
-
398
- <div className="flex-1 overflow-y-auto">
399
- {(state.leaderboard || [])
400
- .slice()
401
- .sort((a, b) => b.equity - a.equity)
402
- .map((p, i) => (
403
- <div key={i} className="p-2 border-b border-gray-800 flex justify-between items-center text-xs">
404
- <span className="text-gray-300 truncate w-24">{p.name}</span>
405
- <div className="text-right">
406
- <div className="text-white font-mono">{(p.equity / 1000).toFixed(1)}k</div>
407
- <div className={p.roi >= 0 ? "text-green-500" : "text-red-500"}>{p.roi.toFixed(1)}%</div>
408
- </div>
409
- </div>
410
- ))}
411
- </div>
412
- </div>
413
- </div>
414
- );
415
- };
416
 
417
- const App = () => {
418
- const { state } = useContext(GameContext);
419
-
420
- const dotColor =
421
- state.wsStatus === "CONNECTED" ? "bg-green-500" :
422
- state.wsStatus === "CONNECTING" ? "bg-yellow-500" :
423
- state.wsStatus === "ERROR" ? "bg-red-500" :
424
- "bg-gray-500";
425
-
426
- const label =
427
- state.wsStatus === "CONNECTED" ? "Connected" :
428
- state.wsStatus === "CONNECTING" ? "Connecting" :
429
- state.wsStatus === "ERROR" ? "Error" :
430
- "Disconnected";
431
-
432
- return (
433
- <div className="flex flex-col h-screen text-gray-100 font-sans">
434
- <header className="h-10 bg-gray-950 border-b border-gray-800 flex items-center px-4 justify-between">
435
- <div className="flex items-center gap-2 font-bold text-blue-500">
436
- <Activity size={16} />
437
- MarketSim
438
- </div>
439
-
440
- {/* CHANGED: reflect actual WS status */}
441
- <div className="text-xs flex items-center gap-2">
442
- <span className={`w-2 h-2 ${dotColor} rounded-full`} />
443
- <span className="text-gray-300">{label}</span>
444
- </div>
445
- </header>
446
-
447
- <div className="flex-1 flex overflow-hidden">
448
- <div className="flex-1 p-1 bg-gray-950 flex flex-col">
449
- <ChartArea />
450
- </div>
451
- <Dashboard />
452
- </div>
453
- </div>
454
- );
455
  };
456
 
457
- const Root = () => (
458
- <GameProvider>
459
- <App />
460
- </GameProvider>
461
- );
462
-
463
- const root = ReactDOM.createRoot(document.getElementById("root"));
464
- root.render(<Root />);
465
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  </body>
467
  </html>
 
1
+ <!doctype html>
2
  <html lang="en">
3
  <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>MPTrading (Simple)</title>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  <style>
8
+ :root{
9
+ --bg:#0b1020; --panel:#111a33; --panel2:#0f1730; --text:#e7eaf3;
10
+ --muted:#aab2d5; --line:#233055; --green:#22c55e; --red:#ef4444; --blue:#60a5fa;
11
+ }
12
+ *{ box-sizing:border-box; }
13
+ body{ margin:0; font-family:system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background:var(--bg); color:var(--text); }
14
+ header{ height:52px; display:flex; align-items:center; justify-content:space-between; padding:0 14px; border-bottom:1px solid var(--line); background:rgba(0,0,0,0.15); }
15
+ header .title{ font-weight:700; letter-spacing:0.3px; }
16
+ header .status{ font-size:12px; color:var(--muted); display:flex; align-items:center; gap:8px; }
17
+ .dot{ width:10px; height:10px; border-radius:999px; background:#6b7280; }
18
+ .dot.connected{ background:var(--green); }
19
+ .dot.connecting{ background:#f59e0b; }
20
+ .dot.error{ background:var(--red); }
21
+
22
+ main{ display:grid; grid-template-columns: 1.2fr 1fr; gap:12px; padding:12px; height: calc(100vh - 52px); }
23
+ .card{ background:var(--panel); border:1px solid var(--line); border-radius:10px; overflow:hidden; display:flex; flex-direction:column; min-height:0; }
24
+ .card h2{ margin:0; padding:10px 12px; font-size:13px; color:var(--muted); text-transform:uppercase; letter-spacing:0.08em; border-bottom:1px solid var(--line); background:var(--panel2); }
25
+ .card .content{ padding:12px; overflow:auto; min-height:0; }
26
+
27
+ .grid2{ display:grid; grid-template-columns: 1fr 1fr; gap:10px; }
28
+ .kpi{ padding:10px; border:1px solid var(--line); border-radius:10px; background:rgba(255,255,255,0.02); }
29
+ .kpi .label{ color:var(--muted); font-size:12px; }
30
+ .kpi .value{ font-size:20px; font-weight:700; margin-top:4px; }
31
+ .kpi .sub{ color:var(--muted); font-size:12px; margin-top:4px; }
32
+ .value.green{ color:var(--green); }
33
+ .value.red{ color:var(--red); }
34
+
35
+ .row{ display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
36
+ input, button{
37
+ font:inherit; border-radius:10px; border:1px solid var(--line);
38
+ background:#0c1430; color:var(--text); padding:10px 12px;
39
+ }
40
+ input{ width:120px; }
41
+ button{ cursor:pointer; }
42
+ button.buy{ border-color:rgba(34,197,94,0.5); }
43
+ button.sell{ border-color:rgba(239,68,68,0.5); }
44
+ button.buy:hover{ background:rgba(34,197,94,0.12); }
45
+ button.sell:hover{ background:rgba(239,68,68,0.12); }
46
+ button.secondary:hover{ background:rgba(96,165,250,0.12); }
47
+
48
+ table{ width:100%; border-collapse:collapse; font-size:13px; }
49
+ th, td{ text-align:left; padding:8px 6px; border-bottom:1px solid rgba(35,48,85,0.6); }
50
+ th{ color:var(--muted); font-weight:600; }
51
+ td.mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
52
+ .pill{ display:inline-block; padding:2px 8px; border-radius:999px; font-size:12px; border:1px solid var(--line); }
53
+ .pill.buy{ color:var(--green); border-color:rgba(34,197,94,0.5); }
54
+ .pill.sell{ color:var(--red); border-color:rgba(239,68,68,0.5); }
55
+ .muted{ color:var(--muted); }
56
+ .small{ font-size:12px; color:var(--muted); }
57
+ .right{ text-align:right; }
58
  </style>
59
  </head>
60
 
61
  <body>
62
+ <header>
63
+ <div class="title">MPTrading (Simple)</div>
64
+ <div class="status">
65
+ <span id="dot" class="dot"></span>
66
+ <span id="statusText">Disconnected</span>
67
+ <span class="small" id="clientIdText"></span>
68
+ </div>
69
+ </header>
70
+
71
+ <main>
72
+ <!-- Left: Trading / KPIs -->
73
+ <section class="card">
74
+ <h2>Account & Trading</h2>
75
+ <div class="content">
76
+ <div class="grid2">
77
+ <div class="kpi">
78
+ <div class="label">Day / Price</div>
79
+ <div class="value" id="kpiPrice">--</div>
80
+ <div class="sub"><span class="muted">Day:</span> <span id="kpiDay">--</span></div>
81
+ </div>
82
+ <div class="kpi">
83
+ <div class="label">Equity</div>
84
+ <div class="value" id="kpiEquity">--</div>
85
+ <div class="sub"><span class="muted">ROI:</span> <span id="kpiRoi">--</span></div>
86
+ </div>
87
+ <div class="kpi">
88
+ <div class="label">Cash</div>
89
+ <div class="value" id="kpiCash">--</div>
90
+ <div class="sub muted">Commission: 5 per trade</div>
91
+ </div>
92
+ <div class="kpi">
93
+ <div class="label">Position</div>
94
+ <div class="value" id="kpiPos">--</div>
95
+ <div class="sub"><span class="muted">Avg cost:</span> <span id="kpiAvg">--</span></div>
96
+ </div>
97
+ </div>
98
+
99
+ <div style="height:12px"></div>
100
+
101
+ <div class="row">
102
+ <label class="small">Qty</label>
103
+ <input id="qty" type="number" value="100" min="1" step="1"/>
104
+ <button class="buy" id="buyBtn">Buy</button>
105
+ <button class="sell" id="sellBtn">Sell</button>
106
+ <button class="secondary" id="resetBtn" title="Reset local portfolio only">Reset local</button>
107
+ </div>
108
+
109
+ <div style="height:12px"></div>
110
+
111
+ <div class="small muted">
112
+ Notes: This portfolio is computed locally in the browser and reported to the server each second via <code>UPDATE_EQUITY</code>.
113
+ </div>
114
+ </div>
115
+ </section>
116
+
117
+ <!-- Right: Orders / Leaderboard -->
118
+ <section class="card">
119
+ <h2>Orders & Leaderboard</h2>
120
+ <div class="content" style="display:grid; grid-template-rows: 1fr 1fr; gap:12px; min-height:0;">
121
+ <div class="card" style="margin:0; min-height:0;">
122
+ <h2 style="border-bottom:1px solid var(--line);">Current orders</h2>
123
+ <div class="content" style="min-height:0;">
124
+ <table>
125
+ <thead>
126
+ <tr>
127
+ <th>Side</th>
128
+ <th class="right">Qty</th>
129
+ <th class="right">Price</th>
130
+ <th class="right">Day</th>
131
+ </tr>
132
+ </thead>
133
+ <tbody id="ordersBody">
134
+ <tr><td colspan="4" class="muted">No orders yet.</td></tr>
135
+ </tbody>
136
+ </table>
137
+ </div>
138
+ </div>
139
+
140
+ <div class="card" style="margin:0; min-height:0;">
141
+ <h2 style="border-bottom:1px solid var(--line);">Leaderboard</h2>
142
+ <div class="content" style="min-height:0;">
143
+ <table>
144
+ <thead>
145
+ <tr>
146
+ <th>Name</th>
147
+ <th class="right">Equity</th>
148
+ <th class="right">ROI %</th>
149
+ </tr>
150
+ </thead>
151
+ <tbody id="lbBody">
152
+ <tr><td colspan="3" class="muted">Waiting for ticks…</td></tr>
153
+ </tbody>
154
+ </table>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </section>
159
+ </main>
160
+
161
+ <script>
162
+ // ---- Protocol: INIT / TICK / UPDATE_EQUITY (from your FastAPI backend) ----
163
+ // WebSocket endpoint: /ws/{client_id}
164
+ // INIT payload: { market: [{i, close}, ...], startDay }
165
+ // TICK payload: { day, leaderboard: [{name, equity, roi}, ...] }
166
+ // Client sends UPDATE_EQUITY payload: { name, equity, roi }
167
+
168
+ const INITIAL_CASH = 10000;
169
+ const COMMISSION = 5;
170
+
171
+ const state = {
172
+ ws: null,
173
+ wsStatus: "DISCONNECTED",
174
+ clientId: "user_" + Math.random().toString(36).slice(2, 7),
175
+
176
+ market: [],
177
+ day: 0,
178
+ price: 0,
179
+
180
+ cash: INITIAL_CASH,
181
+ shares: 0,
182
+ avgCost: 0,
183
+
184
+ equity: INITIAL_CASH,
185
+ roi: 0,
186
+
187
+ orders: [],
188
+ leaderboard: []
189
+ };
190
+
191
+ // ---- UI helpers ----
192
+ const $ = (id) => document.getElementById(id);
193
+
194
+ function fmtMoney(x) {
195
+ if (!Number.isFinite(x)) return "--";
196
+ return x.toLocaleString(undefined, { maximumFractionDigits: 0 });
197
+ }
198
+ function fmtPrice(x) {
199
+ if (!Number.isFinite(x)) return "--";
200
+ return x.toFixed(2);
201
+ }
202
+ function fmtPct(x) {
203
+ if (!Number.isFinite(x)) return "--";
204
+ return x.toFixed(2) + "%";
205
+ }
206
+
207
+ function setStatus(status) {
208
+ state.wsStatus = status;
209
+ const dot = $("dot");
210
+ const txt = $("statusText");
211
+
212
+ dot.className = "dot";
213
+ if (status === "CONNECTED") dot.classList.add("connected");
214
+ else if (status === "CONNECTING") dot.classList.add("connecting");
215
+ else if (status === "ERROR") dot.classList.add("error");
216
+
217
+ txt.textContent = status[0] + status.slice(1).toLowerCase();
218
+ $("clientIdText").textContent = "· " + state.clientId;
219
+ }
220
+
221
+ function recalcEquity() {
222
+ state.equity = state.cash + state.shares * state.price;
223
+ state.roi = ((state.equity - INITIAL_CASH) / INITIAL_CASH) * 100;
224
+ }
225
+
226
+ function render() {
227
+ $("kpiDay").textContent = state.day;
228
+ $("kpiPrice").textContent = fmtPrice(state.price);
229
+ $("kpiCash").textContent = fmtMoney(state.cash);
230
+ $("kpiEquity").textContent = fmtMoney(state.equity);
231
+
232
+ const roiEl = $("kpiRoi");
233
+ roiEl.textContent = fmtPct(state.roi);
234
+ roiEl.className = (state.roi >= 0) ? "value green" : "value red";
235
+
236
+ $("kpiPos").textContent = state.shares.toLocaleString();
237
+ $("kpiAvg").textContent = fmtPrice(state.avgCost);
238
+
239
+ // Orders
240
+ const ob = $("ordersBody");
241
+ ob.innerHTML = "";
242
+ if (!state.orders.length) {
243
+ const tr = document.createElement("tr");
244
+ tr.innerHTML = `<td colspan="4" class="muted">No orders yet.</td>`;
245
+ ob.appendChild(tr);
246
+ } else {
247
+ for (const o of state.orders.slice().reverse().slice(0, 50)) {
248
+ const tr = document.createElement("tr");
249
+ tr.innerHTML = `
250
+ <td><span class="pill ${o.side === "BUY" ? "buy" : "sell"}">${o.side}</span></td>
251
+ <td class="right mono">${o.qty}</td>
252
+ <td class="right mono">${fmtPrice(o.price)}</td>
253
+ <td class="right mono">${o.day}</td>
254
+ `;
255
+ ob.appendChild(tr);
256
  }
257
+ }
258
+
259
+ // Leaderboard
260
+ const lb = $("lbBody");
261
+ lb.innerHTML = "";
262
+ if (!state.leaderboard || !state.leaderboard.length) {
263
+ const tr = document.createElement("tr");
264
+ tr.innerHTML = `<td colspan="3" class="muted">Waiting for ticks…</td>`;
265
+ lb.appendChild(tr);
266
+ } else {
267
+ for (const p of state.leaderboard.slice(0, 50)) {
268
+ const tr = document.createElement("tr");
269
+ const roiClass = p.roi >= 0 ? "style='color:var(--green)'" : "style='color:var(--red)'";
270
+ tr.innerHTML = `
271
+ <td>${escapeHtml(p.name)}</td>
272
+ <td class="right mono">${fmtMoney(p.equity)}</td>
273
+ <td class="right mono" ${roiClass}>${fmtPct(p.roi)}</td>
274
+ `;
275
+ lb.appendChild(tr);
276
+ }
277
+ }
278
+ }
279
+
280
+ function escapeHtml(s) {
281
+ return String(s).replace(/[&<>"']/g, (c) => ({
282
+ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;"
283
+ }[c]));
284
+ }
285
+
286
+ // ---- Trading actions (local only) ----
287
+ function placeOrder(side, qty) {
288
+ qty = Math.floor(Number(qty));
289
+ if (!Number.isFinite(qty) || qty <= 0) return;
290
+ if (!Number.isFinite(state.price) || state.price <= 0) return;
291
+
292
+ const cost = qty * state.price;
293
+
294
+ if (side === "BUY") {
295
+ if (state.cash < cost + COMMISSION) return;
296
+ // Update avg cost
297
+ const totalVal = state.shares * state.avgCost + cost;
298
+ state.shares += qty;
299
+ state.avgCost = state.shares > 0 ? (totalVal / state.shares) : 0;
300
+ state.cash -= (cost + COMMISSION);
301
+ } else {
302
+ if (state.shares < qty) return;
303
+ state.shares -= qty;
304
+ state.cash += (cost - COMMISSION);
305
+ if (state.shares === 0) state.avgCost = 0;
306
+ }
307
+
308
+ state.orders.push({ side, qty, price: state.price, day: state.day, ts: Date.now() });
309
+
310
+ recalcEquity();
311
+ render();
312
+ }
313
+
314
+ function resetLocal() {
315
+ state.cash = INITIAL_CASH;
316
+ state.shares = 0;
317
+ state.avgCost = 0;
318
+ state.orders = [];
319
+ recalcEquity();
320
+ render();
321
+ }
322
+
323
+ // ---- WebSocket ----
324
+ function connectWS() {
325
+ const isLocalhost = (location.hostname === "localhost" || location.hostname === "127.0.0.1");
326
+ const protocol = isLocalhost ? "ws" : "wss";
327
+ const wsUrl = `${protocol}://${location.host}/ws/${state.clientId}`;
328
+
329
+ setStatus("CONNECTING");
330
+
331
+ const ws = new WebSocket(wsUrl);
332
+ state.ws = ws;
333
+
334
+ ws.onopen = () => {
335
+ // Server sends INIT immediately; status will flip to CONNECTED once INIT arrives.
336
+ // Keep it CONNECTING here.
337
  };
338
 
339
+ ws.onmessage = (ev) => {
340
+ let msg;
341
+ try { msg = JSON.parse(ev.data); } catch { return; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
 
343
+ if (msg.type === "INIT") {
344
+ state.market = Array.isArray(msg.payload?.market) ? msg.payload.market : [];
345
+ // day stays at 0 until first tick, but initialize price if available
346
+ state.day = Number(msg.payload?.startDay ?? 0);
347
+ state.price = Number(state.market?.[state.day]?.close ?? 0);
 
348
 
349
+ recalcEquity();
350
+ setStatus("CONNECTED");
351
+ render();
352
+ return;
353
  }
354
 
355
+ if (msg.type === "TICK") {
356
+ const d = Number(msg.payload?.day);
357
+ if (Number.isFinite(d)) state.day = d;
358
+ const px = Number(state.market?.[state.day]?.close ?? state.price);
359
+ if (Number.isFinite(px)) state.price = px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
 
361
+ state.leaderboard = Array.isArray(msg.payload?.leaderboard) ? msg.payload.leaderboard : state.leaderboard;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
+ recalcEquity();
364
+ render();
365
+ return;
366
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  };
368
 
369
+ ws.onerror = () => setStatus("ERROR");
370
+ ws.onclose = () => setStatus("DISCONNECTED");
371
+ }
372
+
373
+ // Report equity every second (if connected)
374
+ setInterval(() => {
375
+ if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return;
376
+
377
+ // send local equity to server leaderboard
378
+ const payload = {
379
+ type: "UPDATE_EQUITY",
380
+ payload: {
381
+ name: state.clientId,
382
+ equity: state.equity,
383
+ roi: state.roi
384
+ }
385
+ };
386
+ state.ws.send(JSON.stringify(payload));
387
+ }, 1000);
388
+
389
+ // ---- Bind UI ----
390
+ $("buyBtn").addEventListener("click", () => placeOrder("BUY", $("qty").value));
391
+ $("sellBtn").addEventListener("click", () => placeOrder("SELL", $("qty").value));
392
+ $("resetBtn").addEventListener("click", resetLocal);
393
+
394
+ // Start
395
+ setStatus("DISCONNECTED");
396
+ render();
397
+ connectWS();
398
+ </script>
399
  </body>
400
  </html>