Update index.html
Browse files- index.html +380 -447
index.html
CHANGED
|
@@ -1,467 +1,400 @@
|
|
| 1 |
-
<!
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
-
<meta charset="
|
| 5 |
-
<meta name="viewport" content="width=device-width,
|
| 6 |
-
<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 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
</style>
|
| 32 |
</head>
|
| 33 |
|
| 34 |
<body>
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
<
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
};
|
| 181 |
|
| 182 |
-
|
| 183 |
-
|
| 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 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
const [tempPoint, setTempPoint] = useState(null);
|
| 276 |
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
|
|
|
| 280 |
}
|
| 281 |
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 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 |
-
|
| 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 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 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 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
| 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>
|