Almaatla commited on
Commit
13e4f52
·
verified ·
1 Parent(s): bef72d9

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +447 -0
index.html ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-react@0.294.0/dist/umd/lucide-react.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
+ const Lucide = window.lucideReact;
44
+ const Activity = Lucide.Activity || (() => <span />);
45
+ const Trophy = Lucide.Trophy || (() => <span />);
46
+ const MousePointer = Lucide.MousePointer || (() => <span />);
47
+ const Pencil = Lucide.Pencil || (() => <span />);
48
+ const Minus = Lucide.Minus || (() => <span />);
49
+ const Trash2 = Lucide.Trash2 || (() => <span />);
50
+
51
+ // --- CONSTANTS ---
52
+ const INITIAL_CASH = 10000;
53
+ const COMMISSION = 5;
54
+
55
+ // --- GAME STATE REDUCER ---
56
+ const createLog = (day, message, type = "INFO") => ({
57
+ id: Math.random().toString(36).substr(2, 9),
58
+ day,
59
+ message,
60
+ type,
61
+ timestamp: Date.now()
62
+ });
63
+
64
+ const initialState = {
65
+ market: [],
66
+ currentDay: 0,
67
+ orders: [],
68
+ position: { shares: 0, avgCost: 0 },
69
+ account: { cash: INITIAL_CASH, equity: INITIAL_CASH, realizedPnL: 0 },
70
+ drawings: [],
71
+ logs: [],
72
+ leaderboard: [],
73
+ connected: false, // CHANGED: used for UI + websocket state
74
+ wsStatus: "DISCONNECTED" // CHANGED: DISCONNECTED | CONNECTING | CONNECTED | ERROR
75
+ };
76
+
77
+ const gameReducer = (state, action) => {
78
+ switch (action.type) {
79
+ case "INIT_MARKET":
80
+ return {
81
+ ...state,
82
+ market: action.payload.market || [],
83
+ connected: true,
84
+ wsStatus: "CONNECTED", // CHANGED
85
+ logs: [...state.logs, createLog(0, "Connected to Market Server")]
86
+ };
87
+
88
+ case "WS_STATUS": // CHANGED
89
+ return { ...state, wsStatus: action.payload, connected: action.payload === "CONNECTED" };
90
+
91
+ case "TICK": {
92
+ const dayIdx = action.payload.day;
93
+ if (!state.market?.[dayIdx]) return state;
94
+ const currentPrice = state.market[dayIdx].close;
95
+ const newEquity = state.account.cash + state.position.shares * currentPrice;
96
+
97
+ return {
98
+ ...state,
99
+ currentDay: dayIdx,
100
+ leaderboard: action.payload.leaderboard || [],
101
+ account: { ...state.account, equity: newEquity }
102
+ };
103
+ }
104
+
105
+ case "PLACE_ORDER": {
106
+ const { side, shares } = action.payload;
107
+ const price = state.market[state.currentDay]?.close;
108
+ if (!price || shares <= 0) return state;
109
+
110
+ const cost = shares * price;
111
+ let { cash, realizedPnL } = state.account;
112
+ let { shares: posShares, avgCost } = state.position;
113
+
114
+ if (side === "BUY") {
115
+ if (cash < cost + COMMISSION) return state;
116
+ cash -= (cost + COMMISSION);
117
+ const totalVal = posShares * avgCost + cost;
118
+ posShares += shares;
119
+ avgCost = posShares > 0 ? totalVal / posShares : 0;
120
+ } else {
121
+ if (posShares < shares) return state;
122
+ const revenue = cost - COMMISSION;
123
+ const profit = (price - avgCost) * shares;
124
+ cash += revenue;
125
+ posShares -= shares;
126
+ realizedPnL += profit;
127
+ if (posShares === 0) avgCost = 0;
128
+ }
129
+
130
+ const newEquity = cash + posShares * price;
131
+
132
+ return {
133
+ ...state,
134
+ orders: [
135
+ ...state.orders,
136
+ {
137
+ id: Date.now(),
138
+ side,
139
+ shares,
140
+ execDay: state.currentDay,
141
+ price,
142
+ status: "FILLED"
143
+ }
144
+ ],
145
+ account: { cash, equity: newEquity, realizedPnL },
146
+ position: { shares: posShares, avgCost },
147
+ logs: [...state.logs, createLog(state.currentDay, `${side} ${shares} @ ${price.toFixed(2)}`, "TRADE")]
148
+ };
149
+ }
150
+
151
+ case "ADD_DRAWING":
152
+ return { ...state, drawings: [...state.drawings, action.payload] };
153
+
154
+ case "REMOVE_DRAWING":
155
+ return { ...state, drawings: state.drawings.filter(d => d.id !== action.payload) };
156
+
157
+ default:
158
+ return state;
159
+ }
160
+ };
161
+
162
+ // --- CONTEXT ---
163
+ const GameContext = createContext(null);
164
+
165
+ const GameProvider = ({ children }) => {
166
+ const [state, dispatch] = useReducer(gameReducer, initialState);
167
+ const socketRef = useRef(null);
168
+ const clientId = useRef("user_" + Math.random().toString(36).substr(2, 5));
169
+ const stateRef = useRef(state);
170
+ useEffect(() => { stateRef.current = state; }, [state]);
171
+
172
+ useEffect(() => {
173
+ // --- FIX: correct ws/wss URL formatting for HF Spaces ---
174
+ const isLocalhost =
175
+ window.location.hostname === "localhost" ||
176
+ window.location.hostname === "127.0.0.1";
177
+
178
+ const protocol = isLocalhost ? "ws" : "wss";
179
+ const host = window.location.host;
180
+
181
+ // CHANGED: must include :// and correct path: /ws/{client_id}
182
+ const wsUrl = `${protocol}://${host}/ws/${clientId.current}`;
183
+ console.log("Attempting WS Connection:", wsUrl);
184
+
185
+ dispatch({ type: "WS_STATUS", payload: "CONNECTING" }); // CHANGED
186
+
187
+ const ws = new WebSocket(wsUrl);
188
+ socketRef.current = ws;
189
+
190
+ ws.onopen = () => {
191
+ console.log("WS Connected");
192
+ // Backend sends INIT immediately after connect; keep UI in CONNECTING until INIT
193
+ // dispatch({ type: "WS_STATUS", payload: "CONNECTED" });
194
+ };
195
+
196
+ ws.onerror = (e) => {
197
+ console.error("WS Error", e);
198
+ dispatch({ type: "WS_STATUS", payload: "ERROR" }); // CHANGED
199
+ };
200
+
201
+ ws.onclose = () => {
202
+ dispatch({ type: "WS_STATUS", payload: "DISCONNECTED" }); // CHANGED
203
+ };
204
+
205
+ ws.onmessage = (event) => {
206
+ try {
207
+ const msg = JSON.parse(event.data);
208
+
209
+ // CHANGED: exact message types expected from backend
210
+ if (msg.type === "INIT") {
211
+ dispatch({ type: "INIT_MARKET", payload: msg.payload });
212
+ } else if (msg.type === "TICK") {
213
+ dispatch({ type: "TICK", payload: msg.payload });
214
+ }
215
+ } catch (e) {
216
+ console.error("Parse Error", e);
217
+ }
218
+ };
219
+
220
+ // Heartbeat: report equity/roi to server
221
+ const hb = setInterval(() => {
222
+ if (ws.readyState === WebSocket.OPEN) {
223
+ const eq = stateRef.current.account.equity;
224
+ const roi = ((eq - INITIAL_CASH) / INITIAL_CASH) * 100;
225
+
226
+ ws.send(JSON.stringify({
227
+ type: "UPDATE_EQUITY", // must match backend
228
+ payload: {
229
+ name: clientId.current,
230
+ equity: eq,
231
+ roi: roi
232
+ }
233
+ }));
234
+ }
235
+ }, 1000);
236
+
237
+ return () => {
238
+ clearInterval(hb);
239
+ try { ws.close(); } catch (_) {}
240
+ };
241
+ }, []);
242
+
243
+ return (
244
+ <GameContext.Provider value={{ state, dispatch }}>
245
+ {children}
246
+ </GameContext.Provider>
247
+ );
248
+ };
249
+
250
+ // --- COMPONENTS ---
251
+ const ChartArea = () => {
252
+ const { state, dispatch } = useContext(GameContext);
253
+ const { market, currentDay, drawings, orders } = state;
254
+ const [mode, setMode] = useState("NONE");
255
+ const [tempPoint, setTempPoint] = useState(null);
256
+
257
+ if (!ComposedChart) return <div className="p-10 text-red-500">Error: Recharts library failed to load.</div>;
258
+ if (!market || market.length === 0) {
259
+ return <div className="flex h-full items-center justify-center text-blue-400 animate-pulse">Connecting to Server...</div>;
260
+ }
261
+
262
+ const visibleData = market.slice(Math.max(0, currentDay - 60), currentDay + 1);
263
+
264
+ const handleChartClick = (e) => {
265
+ if (!e || !e.activePayload) return;
266
+ const p = { x: e.activePayload[0].payload.i, y: e.activePayload[0].payload.close };
267
+
268
+ if (mode === "HORIZ") {
269
+ dispatch({ type: "ADD_DRAWING", payload: { id: Date.now(), type: "HORIZ", points: [p], color: "#d8b4fe" } });
270
+ setMode("NONE");
271
+ } else if (mode === "TREND") {
272
+ if (!tempPoint) setTempPoint(p);
273
+ else {
274
+ dispatch({ type: "ADD_DRAWING", payload: { id: Date.now(), type: "TREND", points: [tempPoint, p], color: "#38bdf8" } });
275
+ setTempPoint(null);
276
+ setMode("NONE");
277
+ }
278
+ }
279
+ };
280
+
281
+ return (
282
+ <div className="relative w-full h-full bg-gray-900 border border-gray-800 rounded overflow-hidden group">
283
+ <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">
284
+ <button onClick={() => setMode("NONE")} className={`p-1 rounded ${mode === "NONE" ? "bg-blue-600" : "hover:bg-gray-700"}`}><MousePointer size={14} /></button>
285
+ <button onClick={() => setMode("TREND")} className={`p-1 rounded ${mode === "TREND" ? "bg-blue-600" : "hover:bg-gray-700"}`}><Pencil size={14} /></button>
286
+ <button onClick={() => setMode("HORIZ")} className={`p-1 rounded ${mode === "HORIZ" ? "bg-blue-600" : "hover:bg-gray-700"}`}><Minus size={14} /></button>
287
+ <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>
288
+ </div>
289
+
290
+ <ResponsiveContainer width="100%" height="100%">
291
+ <ComposedChart data={visibleData} onClick={handleChartClick} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
292
+ <XAxis dataKey="i" hide domain={["dataMin", "dataMax"]} />
293
+ <YAxis orientation="right" domain={["auto", "auto"]} stroke="#4b5563" tick={{ fontSize: 10 }} />
294
+ <Tooltip
295
+ contentStyle={{ backgroundColor: "#111827", borderColor: "#374151" }}
296
+ itemStyle={{ color: "#fff" }}
297
+ labelStyle={{ display: "none" }}
298
+ formatter={(v) => (typeof v === "number" ? v.toFixed(2) : v)}
299
+ isAnimationActive={false}
300
+ />
301
+ <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" vertical={false} />
302
+ <Bar dataKey="close" barSize={3} fill="#3b82f6" isAnimationActive={false} />
303
+
304
+ {drawings.map(d => {
305
+ if (d.type === "HORIZ") return <ReferenceLine key={d.id} y={d.points[0].y} stroke={d.color} />;
306
+ if (d.type === "TREND") return <ReferenceLine key={d.id} segment={[
307
+ { x: d.points[0].x, y: d.points[0].y },
308
+ { x: d.points[1].x, y: d.points[1].y }
309
+ ]} stroke={d.color} />;
310
+ return null;
311
+ })}
312
+
313
+ {orders
314
+ .filter(o => o.execDay >= (visibleData[0]?.i ?? 0))
315
+ .map(o => (
316
+ <ReferenceDot
317
+ key={o.id}
318
+ x={o.execDay}
319
+ y={o.price}
320
+ r={4}
321
+ fill={o.side === "BUY" ? "#22c55e" : "#ef4444"}
322
+ stroke="none"
323
+ />
324
+ ))}
325
+ </ComposedChart>
326
+ </ResponsiveContainer>
327
+ </div>
328
+ );
329
+ };
330
+
331
+ const Dashboard = () => {
332
+ const { state, dispatch } = useContext(GameContext);
333
+ const [size, setSize] = useState(100);
334
+ const price = state.market[state.currentDay]?.close || 0;
335
+
336
+ return (
337
+ <div className="w-64 bg-gray-900 border-l border-gray-800 flex flex-col">
338
+ <div className="p-4 border-b border-gray-800">
339
+ <div className="text-2xl font-bold text-white mb-4 text-center tracking-wider">{price.toFixed(2)}</div>
340
+
341
+ <div className="flex gap-2 mb-4">
342
+ <button
343
+ onClick={() => dispatch({ type: "PLACE_ORDER", payload: { side: "BUY", shares: parseInt(size) } })}
344
+ className="flex-1 bg-green-600 hover:bg-green-500 text-white font-bold py-3 rounded transition"
345
+ >
346
+ BUY
347
+ </button>
348
+ <button
349
+ onClick={() => dispatch({ type: "PLACE_ORDER", payload: { side: "SELL", shares: parseInt(size) } })}
350
+ className="flex-1 bg-red-600 hover:bg-red-500 text-white font-bold py-3 rounded transition"
351
+ >
352
+ SELL
353
+ </button>
354
+ </div>
355
+
356
+ <div className="mb-4">
357
+ <label className="text-xs text-gray-500 font-bold uppercase block mb-1">Quantity</label>
358
+ <input
359
+ type="number"
360
+ value={size}
361
+ onChange={(e) => setSize(e.target.value)}
362
+ className="w-full bg-gray-800 border border-gray-700 text-white p-2 rounded text-center font-mono"
363
+ />
364
+ </div>
365
+
366
+ <div className="grid grid-cols-2 gap-2 text-xs">
367
+ <div className="text-gray-500">Cash</div><div className="text-right text-gray-300">{state.account.cash.toFixed(0)}</div>
368
+ <div className="text-gray-500">Equity</div><div className="text-right text-blue-400 font-bold">{state.account.equity.toFixed(0)}</div>
369
+ </div>
370
+ </div>
371
+
372
+ <div className="flex-1 flex flex-col min-h-0">
373
+ <div className="p-2 bg-gray-800/50 text-xs font-bold text-gray-400 uppercase flex items-center gap-2">
374
+ <Trophy size={12} className="text-yellow-500" />
375
+ Live Rankings
376
+ </div>
377
+
378
+ <div className="flex-1 overflow-y-auto">
379
+ {(state.leaderboard || [])
380
+ .slice()
381
+ .sort((a, b) => b.equity - a.equity)
382
+ .map((p, i) => (
383
+ <div key={i} className="p-2 border-b border-gray-800 flex justify-between items-center text-xs">
384
+ <span className="text-gray-300 truncate w-24">{p.name}</span>
385
+ <div className="text-right">
386
+ <div className="text-white font-mono">{(p.equity / 1000).toFixed(1)}k</div>
387
+ <div className={p.roi >= 0 ? "text-green-500" : "text-red-500"}>{p.roi.toFixed(1)}%</div>
388
+ </div>
389
+ </div>
390
+ ))}
391
+ </div>
392
+ </div>
393
+ </div>
394
+ );
395
+ };
396
+
397
+ const App = () => {
398
+ const { state } = useContext(GameContext);
399
+
400
+ const dotColor =
401
+ state.wsStatus === "CONNECTED" ? "bg-green-500" :
402
+ state.wsStatus === "CONNECTING" ? "bg-yellow-500" :
403
+ state.wsStatus === "ERROR" ? "bg-red-500" :
404
+ "bg-gray-500";
405
+
406
+ const label =
407
+ state.wsStatus === "CONNECTED" ? "Connected" :
408
+ state.wsStatus === "CONNECTING" ? "Connecting" :
409
+ state.wsStatus === "ERROR" ? "Error" :
410
+ "Disconnected";
411
+
412
+ return (
413
+ <div className="flex flex-col h-screen text-gray-100 font-sans">
414
+ <header className="h-10 bg-gray-950 border-b border-gray-800 flex items-center px-4 justify-between">
415
+ <div className="flex items-center gap-2 font-bold text-blue-500">
416
+ <Activity size={16} />
417
+ MarketSim
418
+ </div>
419
+
420
+ {/* CHANGED: reflect actual WS status */}
421
+ <div className="text-xs flex items-center gap-2">
422
+ <span className={`w-2 h-2 ${dotColor} rounded-full`} />
423
+ <span className="text-gray-300">{label}</span>
424
+ </div>
425
+ </header>
426
+
427
+ <div className="flex-1 flex overflow-hidden">
428
+ <div className="flex-1 p-1 bg-gray-950 flex flex-col">
429
+ <ChartArea />
430
+ </div>
431
+ <Dashboard />
432
+ </div>
433
+ </div>
434
+ );
435
+ };
436
+
437
+ const Root = () => (
438
+ <GameProvider>
439
+ <App />
440
+ </GameProvider>
441
+ );
442
+
443
+ const root = ReactDOM.createRoot(document.getElementById("root"));
444
+ root.render(<Root />);
445
+ </script>
446
+ </body>
447
+ </html>