NOT-OMEGA commited on
Commit
7a7284d
·
verified ·
1 Parent(s): 23070de

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -256
app.py DELETED
@@ -1,256 +0,0 @@
1
- """
2
- HFT Exchange Simulator — FastAPI server
3
- Wraps the C++ hft_engine binary via persistent subprocess.
4
- """
5
- import json
6
- import os
7
- import subprocess
8
- import sys
9
- import threading
10
- from pathlib import Path
11
- from typing import Optional
12
-
13
- from fastapi import FastAPI, HTTPException
14
- from fastapi.middleware.cors import CORSMiddleware
15
- from fastapi.responses import FileResponse
16
- from pydantic import BaseModel
17
-
18
- # BUG-FIX #1: Detect platform and pick the correct binary name.
19
- # On Windows the compiled binary has a .exe extension.
20
- _engine_name = "hft_engine.exe" if sys.platform == "win32" else "hft_engine"
21
- ENGINE_PATH = Path(__file__).parent / "build" / _engine_name
22
- ENGINE_TIMEOUT = 10.0
23
-
24
-
25
- class EngineProcess:
26
- """Manages a persistent child process that speaks line-delimited JSON."""
27
-
28
- def __init__(self):
29
- self.proc: Optional[subprocess.Popen] = None
30
- self.lock = threading.Lock()
31
- self._start()
32
-
33
- # ------------------------------------------------------------------
34
- def _start(self):
35
- if not ENGINE_PATH.exists():
36
- # BUG-FIX #2: Do not crash the whole server if the binary is
37
- # missing — let individual requests report the problem instead.
38
- self.proc = None
39
- return
40
- self.proc = subprocess.Popen(
41
- [str(ENGINE_PATH)],
42
- stdin=subprocess.PIPE,
43
- stdout=subprocess.PIPE,
44
- stderr=subprocess.DEVNULL,
45
- text=True,
46
- bufsize=1,
47
- )
48
-
49
- # ------------------------------------------------------------------
50
- def send(self, cmd: str, payload: dict = None) -> dict:
51
- if payload is None:
52
- payload = {}
53
- line = f"{cmd} {json.dumps(payload)}\n"
54
-
55
- with self.lock:
56
- # BUG-FIX #3: If the engine binary was never found, report it
57
- # clearly instead of an opaque NoneType crash.
58
- if self.proc is None or self.proc.poll() is not None:
59
- self._start()
60
- if self.proc is None:
61
- raise HTTPException(
62
- 503,
63
- f"Engine binary not found at {ENGINE_PATH}. "
64
- "Build the C++ engine first.",
65
- )
66
-
67
- try:
68
- self.proc.stdin.write(line)
69
- self.proc.stdin.flush()
70
-
71
- # BUG-FIX #4 (THE CRASH BUG):
72
- # The old code used `select.select()` which ONLY works with
73
- # sockets on Windows — it raises OSError on pipe file
74
- # descriptors. Replaced with a cross-platform thread-based
75
- # readline with timeout.
76
- response_container: list = []
77
- error_container: list = []
78
-
79
- def _read():
80
- try:
81
- resp = self.proc.stdout.readline()
82
- response_container.append(resp)
83
- except Exception as exc:
84
- error_container.append(exc)
85
-
86
- reader = threading.Thread(target=_read, daemon=True)
87
- reader.start()
88
- reader.join(timeout=ENGINE_TIMEOUT)
89
-
90
- if reader.is_alive():
91
- # Timed out — kill and restart the engine
92
- self.proc.kill()
93
- self.proc = None
94
- self._start()
95
- raise HTTPException(504, "Engine timeout — restarted")
96
-
97
- if error_container:
98
- raise error_container[0]
99
-
100
- response = (
101
- response_container[0].strip() if response_container else ""
102
- )
103
- if not response:
104
- raise HTTPException(500, "Engine returned empty response")
105
-
106
- return json.loads(response)
107
-
108
- except HTTPException:
109
- raise
110
- except Exception as e:
111
- try:
112
- self.proc.kill()
113
- except Exception:
114
- pass
115
- self.proc = None
116
- self._start()
117
- raise HTTPException(500, f"Engine error: {e}")
118
-
119
-
120
- # BUG-FIX #5: Lazy-initialise so the import doesn't blow up when running
121
- # under test-harnesses or when the binary hasn't been built yet.
122
- engine: Optional[EngineProcess] = None
123
-
124
-
125
- def _get_engine() -> EngineProcess:
126
- global engine
127
- if engine is None:
128
- engine = EngineProcess()
129
- return engine
130
-
131
-
132
- app = FastAPI(title="HFT Exchange Simulator", version="1.0.0")
133
-
134
- app.add_middleware(
135
- CORSMiddleware,
136
- allow_origins=["*"],
137
- allow_methods=["*"],
138
- allow_headers=["*"],
139
- )
140
-
141
-
142
- # ── Pydantic models ──────────────────────────────────────────────────────────
143
-
144
- class OrderRequest(BaseModel):
145
- symbol: str = "AAPL"
146
- side: str = "BUY"
147
- type: str = "LIMIT"
148
- price: float = 0.0
149
- qty: int = 100
150
- client_id: str = ""
151
- market_price: float = 0.0
152
-
153
-
154
- class CancelRequest(BaseModel):
155
- symbol: str
156
- id: int
157
-
158
-
159
- class FeedStepRequest(BaseModel):
160
- n: int = 10
161
-
162
-
163
- class BacktestRequest(BaseModel):
164
- strategy: str = "MarketMaking"
165
- symbol: str = "AAPL"
166
- ticks: int = 5000
167
- start_price: float = 185.0
168
-
169
-
170
- class RiskLimitsRequest(BaseModel):
171
- max_order_qty: int = 10000
172
- max_position: int = 500000
173
- max_notional_usd: float = 50_000_000.0
174
- max_orders_per_sec: int = 2000
175
-
176
-
177
- # ── API routes ────────────────────────────────────────────────────────────────
178
-
179
- @app.get("/api/status")
180
- def status():
181
- return _get_engine().send("status")
182
-
183
-
184
- @app.get("/api/symbols")
185
- def symbols():
186
- return _get_engine().send("symbols")
187
-
188
-
189
- @app.get("/api/book/{symbol}")
190
- def book(symbol: str, depth: int = 10):
191
- # BUG-FIX #6: The `depth` query-param was accepted but never forwarded
192
- # to the engine. Now included in the payload.
193
- return _get_engine().send("book", {"symbol": symbol, "depth": depth})
194
-
195
-
196
- @app.get("/api/positions")
197
- def positions():
198
- return _get_engine().send("positions")
199
-
200
-
201
- @app.get("/api/trades")
202
- def trades():
203
- return _get_engine().send("trades")
204
-
205
-
206
- @app.get("/api/orders")
207
- def orders():
208
- return _get_engine().send("orders")
209
-
210
-
211
- @app.post("/api/order")
212
- def submit_order(req: OrderRequest):
213
- return _get_engine().send("order", req.model_dump())
214
-
215
-
216
- @app.delete("/api/order")
217
- def cancel_order(req: CancelRequest):
218
- return _get_engine().send("cancel", req.model_dump())
219
-
220
-
221
- @app.post("/api/feed/step")
222
- def feed_step(req: FeedStepRequest):
223
- return _get_engine().send("feed_step", {"n": req.n})
224
-
225
-
226
- @app.post("/api/backtest")
227
- def backtest(req: BacktestRequest):
228
- return _get_engine().send("backtest", req.model_dump())
229
-
230
-
231
- @app.get("/api/risk/limits")
232
- def get_limits():
233
- return _get_engine().send("get_limits")
234
-
235
-
236
- @app.post("/api/risk/limits")
237
- def set_limits(req: RiskLimitsRequest):
238
- return _get_engine().send("risk_limits", req.model_dump())
239
-
240
-
241
- @app.post("/api/reset")
242
- def reset():
243
- return _get_engine().send("reset")
244
-
245
-
246
- @app.get("/")
247
- def serve_index():
248
- return FileResponse(str(Path(__file__).parent / "index.html"))
249
-
250
-
251
- # ── Entry point ───────────────────────────────────────────────────────────────
252
-
253
- if __name__ == "__main__":
254
- import uvicorn
255
- port = int(os.environ.get("PORT", 7860))
256
- uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")