Almaatla commited on
Commit
b11d209
·
verified ·
1 Parent(s): d7a1370

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +29 -61
app.py CHANGED
@@ -4,7 +4,7 @@ import os
4
  import random
5
  import time
6
  from dataclasses import dataclass, asdict
7
- from typing import Any, Dict, List, Optional, Tuple
8
 
9
  from fastapi import FastAPI, Header, HTTPException, WebSocket, WebSocketDisconnect
10
  from fastapi.responses import FileResponse
@@ -13,17 +13,17 @@ from fastapi.responses import FileResponse
13
  # ----------------------------
14
  # Config
15
  # ----------------------------
16
- TICK_RATE = float(os.getenv("TICK_RATE", "1.0")) # seconds
17
- MARKET_LENGTH = int(os.getenv("MARKET_LENGTH", "300")) # number of ticks/days
18
  START_PRICE = float(os.getenv("START_PRICE", "100.0"))
19
 
20
  ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", "") # set as HF Space Secret
21
  ADMIN_HEADER = "X-ADMIN-TOKEN"
22
 
23
  INDEX_FILE = os.getenv("INDEX_FILE", "index.html")
24
- ADMIN_FILE = os.getenv("ADMIN_FILE", "admin.html")
25
 
26
- INITIAL_VOLATILITY = float(os.getenv("DEFAULT_VOLATILITY", "0.8"))
27
 
28
 
29
  # ----------------------------
@@ -32,40 +32,28 @@ INITIAL_VOLATILITY = float(os.getenv("DEFAULT_VOLATILITY", "0.8"))
32
  @dataclass
33
  class ScenarioEvent:
34
  day: int
35
- shockPct: float = 0.0 # e.g. +5.0 means +5%
36
  volatility: Optional[float] = None
37
  news: Optional[str] = None
38
 
39
 
40
- @dataclass
41
- class Scenario:
42
- name: str = "default"
43
- startDay: int = 0
44
- basePrice: float = START_PRICE
45
- defaultVolatility: float = INITIAL_VOLATILITY
46
- events: List[ScenarioEvent] = None
47
-
48
-
49
  # ----------------------------
50
  # Market simulator
51
  # ----------------------------
52
  class MarketSimulator:
53
- def __init__(self, seed: int = 42, start_price: float = 100.0, base_vol: float = 0.8):
54
  self.seed = seed
55
- self.start_price = start_price
56
- self.base_vol = base_vol
57
 
58
- def generate_base_market(self, length: int) -> List[Dict[str, float]]:
59
  rng = random.Random(self.seed)
60
- price = self.start_price
61
- series: List[Dict[str, float]] = []
62
  drift = 0.02
63
 
 
64
  for i in range(length):
65
- shock = rng.gauss(0.0, self.base_vol)
66
  price = max(1.0, price + drift + shock)
67
  series.append({"i": i, "close": round(price, 2)})
68
-
69
  return series
70
 
71
 
@@ -121,38 +109,33 @@ class ConnectionManager:
121
  self.active.pop(cid, None)
122
 
123
  async def broadcast_tick(self, day: int) -> None:
124
- payload = {
125
  "type": "TICK",
126
  "payload": {
127
  "day": day,
128
  "leaderboard": await self._snapshot_leaderboard(),
129
  },
130
- }
131
- await self.broadcast(payload)
132
 
133
  async def broadcast_news(self, day: int, text: str) -> None:
134
- payload = {"type": "NEWS", "payload": {"day": day, "text": text}}
135
- await self.broadcast(payload)
136
 
137
 
138
- app = FastAPI(title="Trading Game (WebSocket + Admin)")
139
  manager = ConnectionManager()
 
140
 
141
  # ----------------------------
142
  # Global game state
143
  # ----------------------------
144
  DAY_LOCK = asyncio.Lock()
145
- STATE_LOCK = asyncio.Lock() # protects scenario/events/volatility/market
146
 
147
  CURRENT_DAY = 0
148
- CURRENT_VOL = INITIAL_VOLATILITY
149
-
150
- # Market is the "timeline" clients receive on INIT; we will adjust it when events happen.
151
- market_sim = MarketSimulator(seed=42, start_price=START_PRICE, base_vol=INITIAL_VOLATILITY)
152
- MARKET: List[Dict[str, float]] = market_sim.generate_base_market(MARKET_LENGTH)
153
 
154
- # Scheduled events by day
155
- EVENTS: Dict[int, List[ScenarioEvent]] = {} # day -> [ScenarioEvent,...]
156
 
157
 
158
  # ----------------------------
@@ -160,7 +143,6 @@ EVENTS: Dict[int, List[ScenarioEvent]] = {} # day -> [ScenarioEvent,...]
160
  # ----------------------------
161
  def require_admin(token: Optional[str]) -> None:
162
  if not ADMIN_TOKEN:
163
- # If not set, keep endpoints locked down by default.
164
  raise HTTPException(status_code=403, detail="Admin token not configured on server.")
165
  if token != ADMIN_TOKEN:
166
  raise HTTPException(status_code=401, detail="Invalid admin token.")
@@ -186,10 +168,6 @@ def snapshot_events() -> List[Dict[str, Any]]:
186
 
187
 
188
  def apply_price_shock_from_day(day: int, shock_pct: float) -> None:
189
- """
190
- Applies a multiplicative factor to MARKET[day:] so the event shifts the future path.
191
- This matches the "future path shift" interpretation.
192
- """
193
  if day < 0 or day >= len(MARKET):
194
  return
195
  factor = 1.0 + (shock_pct / 100.0)
@@ -197,9 +175,8 @@ def apply_price_shock_from_day(day: int, shock_pct: float) -> None:
197
  MARKET[i]["close"] = round(max(1.0, MARKET[i]["close"] * factor), 2)
198
 
199
 
200
- def regen_market_with_volatility(seed: int, start_price: float, base_vol: float) -> List[Dict[str, float]]:
201
- sim = MarketSimulator(seed=seed, start_price=start_price, base_vol=base_vol)
202
- return sim.generate_base_market(MARKET_LENGTH)
203
 
204
 
205
  # ----------------------------
@@ -212,11 +189,14 @@ async def root():
212
 
213
  @app.get("/admin")
214
  async def admin_page():
 
 
 
215
  return FileResponse(ADMIN_FILE)
216
 
217
 
218
  # ----------------------------
219
- # Admin REST API
220
  # ----------------------------
221
  @app.get("/admin/state")
222
  async def admin_state(x_admin_token: Optional[str] = Header(default=None, alias=ADMIN_HEADER)):
@@ -245,7 +225,6 @@ async def admin_clear_events(x_admin_token: Optional[str] = Header(default=None,
245
  async def admin_add_event(body: Dict[str, Any], x_admin_token: Optional[str] = Header(default=None, alias=ADMIN_HEADER)):
246
  require_admin(x_admin_token)
247
 
248
- # Supports either "day" (absolute) or "offset" (relative to current day)
249
  async with DAY_LOCK:
250
  cur = CURRENT_DAY
251
 
@@ -260,7 +239,6 @@ async def admin_add_event(body: Dict[str, Any], x_admin_token: Optional[str] = H
260
  raise HTTPException(status_code=400, detail=f"Event day {day} is in the past (current day {cur}).")
261
 
262
  ev = parse_event({**body, "day": day})
263
-
264
  async with STATE_LOCK:
265
  EVENTS.setdefault(day, []).append(ev)
266
 
@@ -271,21 +249,18 @@ async def admin_add_event(body: Dict[str, Any], x_admin_token: Optional[str] = H
271
  async def admin_load_scenario(body: Dict[str, Any], x_admin_token: Optional[str] = Header(default=None, alias=ADMIN_HEADER)):
272
  require_admin(x_admin_token)
273
 
274
- name = str(body.get("name", "scenario"))
275
  start_day = int(body.get("startDay", 0))
276
  base_price = float(body.get("basePrice", START_PRICE))
277
- default_vol = float(body.get("defaultVolatility", INITIAL_VOLATILITY))
278
  evs_raw = body.get("events", [])
279
  if not isinstance(evs_raw, list):
280
  raise HTTPException(status_code=400, detail="'events' must be a list.")
281
-
282
  evs = [parse_event(e) for e in evs_raw]
283
 
284
- # Reset market + events, reset day, reset volatility
285
  async with STATE_LOCK:
286
  global MARKET, CURRENT_VOL
287
  CURRENT_VOL = default_vol
288
- MARKET = regen_market_with_volatility(seed=42, start_price=base_price, base_vol=default_vol)
289
  EVENTS.clear()
290
  for ev in evs:
291
  EVENTS.setdefault(ev.day, []).append(ev)
@@ -294,7 +269,7 @@ async def admin_load_scenario(body: Dict[str, Any], x_admin_token: Optional[str]
294
  global CURRENT_DAY
295
  CURRENT_DAY = max(0, min(start_day, len(MARKET) - 1))
296
 
297
- return {"ok": True, "name": name, "startDay": CURRENT_DAY, "eventsLoaded": len(evs)}
298
 
299
 
300
  # ----------------------------
@@ -304,7 +279,6 @@ async def admin_load_scenario(body: Dict[str, Any], x_admin_token: Optional[str]
304
  async def websocket_endpoint(websocket: WebSocket, client_id: str):
305
  await manager.connect(websocket, client_id)
306
 
307
- # Send INIT immediately
308
  async with STATE_LOCK:
309
  init_payload = {"market": MARKET, "startDay": 0}
310
  await websocket.send_text(json.dumps({"type": "INIT", "payload": init_payload}))
@@ -330,7 +304,6 @@ async def websocket_endpoint(websocket: WebSocket, client_id: str):
330
  roi_f = float(payload.get("roi", 0.0))
331
  except Exception:
332
  roi_f = 0.0
333
-
334
  await manager.update_equity(name=name, equity=equity_f, roi=roi_f)
335
 
336
  except WebSocketDisconnect:
@@ -348,29 +321,24 @@ async def game_loop():
348
  while True:
349
  await asyncio.sleep(TICK_RATE)
350
 
351
- # advance day
352
  async with DAY_LOCK:
353
  CURRENT_DAY = (CURRENT_DAY + 1) % len(MARKET)
354
  day = CURRENT_DAY
355
 
356
- # apply scheduled events
357
  news_to_broadcast: List[str] = []
358
  async with STATE_LOCK:
359
  if day in EVENTS and EVENTS[day]:
360
  for ev in EVENTS[day]:
361
  if ev.shockPct:
362
- # shift entire future path from this day onwards
363
  apply_price_shock_from_day(day, ev.shockPct)
364
  if ev.volatility is not None:
365
  CURRENT_VOL = float(ev.volatility)
366
  if ev.news:
367
  news_to_broadcast.append(ev.news)
368
 
369
- # broadcast optional news first
370
  for text in news_to_broadcast:
371
  await manager.broadcast_news(day, text)
372
 
373
- # broadcast tick
374
  await manager.broadcast_tick(day)
375
 
376
 
 
4
  import random
5
  import time
6
  from dataclasses import dataclass, asdict
7
+ from typing import Any, Dict, List, Optional
8
 
9
  from fastapi import FastAPI, Header, HTTPException, WebSocket, WebSocketDisconnect
10
  from fastapi.responses import FileResponse
 
13
  # ----------------------------
14
  # Config
15
  # ----------------------------
16
+ TICK_RATE = float(os.getenv("TICK_RATE", "1.0")) # seconds per tick
17
+ MARKET_LENGTH = int(os.getenv("MARKET_LENGTH", "600")) # timeline length
18
  START_PRICE = float(os.getenv("START_PRICE", "100.0"))
19
 
20
  ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", "") # set as HF Space Secret
21
  ADMIN_HEADER = "X-ADMIN-TOKEN"
22
 
23
  INDEX_FILE = os.getenv("INDEX_FILE", "index.html")
24
+ ADMIN_FILE = os.getenv("ADMIN_FILE", "admin.html") # optional if you keep admin.html
25
 
26
+ DEFAULT_VOLATILITY = float(os.getenv("DEFAULT_VOLATILITY", "0.8"))
27
 
28
 
29
  # ----------------------------
 
32
  @dataclass
33
  class ScenarioEvent:
34
  day: int
35
+ shockPct: float = 0.0 # price shock applied to future path from day onward
36
  volatility: Optional[float] = None
37
  news: Optional[str] = None
38
 
39
 
 
 
 
 
 
 
 
 
 
40
  # ----------------------------
41
  # Market simulator
42
  # ----------------------------
43
  class MarketSimulator:
44
+ def __init__(self, seed: int = 42):
45
  self.seed = seed
 
 
46
 
47
+ def generate_base_market(self, length: int, start_price: float, vol: float) -> List[Dict[str, float]]:
48
  rng = random.Random(self.seed)
49
+ price = float(start_price)
 
50
  drift = 0.02
51
 
52
+ series: List[Dict[str, float]] = []
53
  for i in range(length):
54
+ shock = rng.gauss(0.0, vol)
55
  price = max(1.0, price + drift + shock)
56
  series.append({"i": i, "close": round(price, 2)})
 
57
  return series
58
 
59
 
 
109
  self.active.pop(cid, None)
110
 
111
  async def broadcast_tick(self, day: int) -> None:
112
+ await self.broadcast({
113
  "type": "TICK",
114
  "payload": {
115
  "day": day,
116
  "leaderboard": await self._snapshot_leaderboard(),
117
  },
118
+ })
 
119
 
120
  async def broadcast_news(self, day: int, text: str) -> None:
121
+ await self.broadcast({"type": "NEWS", "payload": {"day": day, "text": text}})
 
122
 
123
 
124
+ app = FastAPI(title="MPTrading (FastAPI + WebSocket)")
125
  manager = ConnectionManager()
126
+ sim = MarketSimulator(seed=42)
127
 
128
  # ----------------------------
129
  # Global game state
130
  # ----------------------------
131
  DAY_LOCK = asyncio.Lock()
132
+ STATE_LOCK = asyncio.Lock()
133
 
134
  CURRENT_DAY = 0
135
+ CURRENT_VOL = DEFAULT_VOLATILITY
 
 
 
 
136
 
137
+ MARKET: List[Dict[str, float]] = sim.generate_base_market(MARKET_LENGTH, START_PRICE, DEFAULT_VOLATILITY)
138
+ EVENTS: Dict[int, List[ScenarioEvent]] = {}
139
 
140
 
141
  # ----------------------------
 
143
  # ----------------------------
144
  def require_admin(token: Optional[str]) -> None:
145
  if not ADMIN_TOKEN:
 
146
  raise HTTPException(status_code=403, detail="Admin token not configured on server.")
147
  if token != ADMIN_TOKEN:
148
  raise HTTPException(status_code=401, detail="Invalid admin token.")
 
168
 
169
 
170
  def apply_price_shock_from_day(day: int, shock_pct: float) -> None:
 
 
 
 
171
  if day < 0 or day >= len(MARKET):
172
  return
173
  factor = 1.0 + (shock_pct / 100.0)
 
175
  MARKET[i]["close"] = round(max(1.0, MARKET[i]["close"] * factor), 2)
176
 
177
 
178
+ def regen_market(start_price: float, vol: float) -> List[Dict[str, float]]:
179
+ return sim.generate_base_market(MARKET_LENGTH, start_price, vol)
 
180
 
181
 
182
  # ----------------------------
 
189
 
190
  @app.get("/admin")
191
  async def admin_page():
192
+ # If you don't include admin.html, you can remove this endpoint.
193
+ if not os.path.exists(ADMIN_FILE):
194
+ raise HTTPException(status_code=404, detail="admin.html not found in repo root.")
195
  return FileResponse(ADMIN_FILE)
196
 
197
 
198
  # ----------------------------
199
+ # Admin REST API (unchanged)
200
  # ----------------------------
201
  @app.get("/admin/state")
202
  async def admin_state(x_admin_token: Optional[str] = Header(default=None, alias=ADMIN_HEADER)):
 
225
  async def admin_add_event(body: Dict[str, Any], x_admin_token: Optional[str] = Header(default=None, alias=ADMIN_HEADER)):
226
  require_admin(x_admin_token)
227
 
 
228
  async with DAY_LOCK:
229
  cur = CURRENT_DAY
230
 
 
239
  raise HTTPException(status_code=400, detail=f"Event day {day} is in the past (current day {cur}).")
240
 
241
  ev = parse_event({**body, "day": day})
 
242
  async with STATE_LOCK:
243
  EVENTS.setdefault(day, []).append(ev)
244
 
 
249
  async def admin_load_scenario(body: Dict[str, Any], x_admin_token: Optional[str] = Header(default=None, alias=ADMIN_HEADER)):
250
  require_admin(x_admin_token)
251
 
 
252
  start_day = int(body.get("startDay", 0))
253
  base_price = float(body.get("basePrice", START_PRICE))
254
+ default_vol = float(body.get("defaultVolatility", DEFAULT_VOLATILITY))
255
  evs_raw = body.get("events", [])
256
  if not isinstance(evs_raw, list):
257
  raise HTTPException(status_code=400, detail="'events' must be a list.")
 
258
  evs = [parse_event(e) for e in evs_raw]
259
 
 
260
  async with STATE_LOCK:
261
  global MARKET, CURRENT_VOL
262
  CURRENT_VOL = default_vol
263
+ MARKET = regen_market(start_price=base_price, vol=default_vol)
264
  EVENTS.clear()
265
  for ev in evs:
266
  EVENTS.setdefault(ev.day, []).append(ev)
 
269
  global CURRENT_DAY
270
  CURRENT_DAY = max(0, min(start_day, len(MARKET) - 1))
271
 
272
+ return {"ok": True, "startDay": CURRENT_DAY, "eventsLoaded": len(evs)}
273
 
274
 
275
  # ----------------------------
 
279
  async def websocket_endpoint(websocket: WebSocket, client_id: str):
280
  await manager.connect(websocket, client_id)
281
 
 
282
  async with STATE_LOCK:
283
  init_payload = {"market": MARKET, "startDay": 0}
284
  await websocket.send_text(json.dumps({"type": "INIT", "payload": init_payload}))
 
304
  roi_f = float(payload.get("roi", 0.0))
305
  except Exception:
306
  roi_f = 0.0
 
307
  await manager.update_equity(name=name, equity=equity_f, roi=roi_f)
308
 
309
  except WebSocketDisconnect:
 
321
  while True:
322
  await asyncio.sleep(TICK_RATE)
323
 
 
324
  async with DAY_LOCK:
325
  CURRENT_DAY = (CURRENT_DAY + 1) % len(MARKET)
326
  day = CURRENT_DAY
327
 
 
328
  news_to_broadcast: List[str] = []
329
  async with STATE_LOCK:
330
  if day in EVENTS and EVENTS[day]:
331
  for ev in EVENTS[day]:
332
  if ev.shockPct:
 
333
  apply_price_shock_from_day(day, ev.shockPct)
334
  if ev.volatility is not None:
335
  CURRENT_VOL = float(ev.volatility)
336
  if ev.news:
337
  news_to_broadcast.append(ev.news)
338
 
 
339
  for text in news_to_broadcast:
340
  await manager.broadcast_news(day, text)
341
 
 
342
  await manager.broadcast_tick(day)
343
 
344