onesecondtrader 0.43.0__py3-none-any.whl → 0.45.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. onesecondtrader/__init__.py +0 -60
  2. onesecondtrader/events/__init__.py +7 -0
  3. onesecondtrader/events/base.py +19 -0
  4. onesecondtrader/models/__init__.py +10 -0
  5. onesecondtrader/models/bar_fields.py +23 -0
  6. onesecondtrader/models/bar_period.py +21 -0
  7. onesecondtrader/models/order_types.py +21 -0
  8. onesecondtrader/models/trade_sides.py +20 -0
  9. {onesecondtrader-0.43.0.dist-info → onesecondtrader-0.45.0.dist-info}/METADATA +2 -2
  10. onesecondtrader-0.45.0.dist-info/RECORD +12 -0
  11. onesecondtrader/connectors/__init__.py +0 -3
  12. onesecondtrader/connectors/brokers/__init__.py +0 -4
  13. onesecondtrader/connectors/brokers/ib.py +0 -418
  14. onesecondtrader/connectors/brokers/simulated.py +0 -349
  15. onesecondtrader/connectors/datafeeds/__init__.py +0 -4
  16. onesecondtrader/connectors/datafeeds/ib.py +0 -286
  17. onesecondtrader/connectors/datafeeds/simulated.py +0 -198
  18. onesecondtrader/connectors/gateways/__init__.py +0 -3
  19. onesecondtrader/connectors/gateways/ib.py +0 -314
  20. onesecondtrader/core/__init__.py +0 -7
  21. onesecondtrader/core/brokers/__init__.py +0 -3
  22. onesecondtrader/core/brokers/base.py +0 -46
  23. onesecondtrader/core/datafeeds/__init__.py +0 -3
  24. onesecondtrader/core/datafeeds/base.py +0 -32
  25. onesecondtrader/core/events/__init__.py +0 -33
  26. onesecondtrader/core/events/bases.py +0 -29
  27. onesecondtrader/core/events/market.py +0 -22
  28. onesecondtrader/core/events/requests.py +0 -33
  29. onesecondtrader/core/events/responses.py +0 -54
  30. onesecondtrader/core/indicators/__init__.py +0 -13
  31. onesecondtrader/core/indicators/averages.py +0 -56
  32. onesecondtrader/core/indicators/bar.py +0 -47
  33. onesecondtrader/core/indicators/base.py +0 -60
  34. onesecondtrader/core/messaging/__init__.py +0 -7
  35. onesecondtrader/core/messaging/eventbus.py +0 -47
  36. onesecondtrader/core/messaging/subscriber.py +0 -69
  37. onesecondtrader/core/models/__init__.py +0 -15
  38. onesecondtrader/core/models/data.py +0 -18
  39. onesecondtrader/core/models/orders.py +0 -27
  40. onesecondtrader/core/models/params.py +0 -21
  41. onesecondtrader/core/models/records.py +0 -34
  42. onesecondtrader/core/strategies/__init__.py +0 -7
  43. onesecondtrader/core/strategies/base.py +0 -331
  44. onesecondtrader/core/strategies/examples.py +0 -47
  45. onesecondtrader/dashboard/__init__.py +0 -3
  46. onesecondtrader/dashboard/app.py +0 -2972
  47. onesecondtrader/dashboard/registry.py +0 -100
  48. onesecondtrader/orchestrator/__init__.py +0 -7
  49. onesecondtrader/orchestrator/orchestrator.py +0 -105
  50. onesecondtrader/orchestrator/recorder.py +0 -199
  51. onesecondtrader/orchestrator/schema.sql +0 -212
  52. onesecondtrader/secmaster/__init__.py +0 -6
  53. onesecondtrader/secmaster/schema.sql +0 -740
  54. onesecondtrader/secmaster/utils.py +0 -737
  55. onesecondtrader-0.43.0.dist-info/RECORD +0 -49
  56. {onesecondtrader-0.43.0.dist-info → onesecondtrader-0.45.0.dist-info}/WHEEL +0 -0
  57. {onesecondtrader-0.43.0.dist-info → onesecondtrader-0.45.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,2972 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import enum
4
- import json
5
- import os
6
- import pathlib
7
- import shutil
8
- import sqlite3
9
-
10
- from fastapi import FastAPI, BackgroundTasks
11
- from fastapi.responses import HTMLResponse, FileResponse
12
- from pydantic import BaseModel
13
-
14
- import matplotlib
15
-
16
- matplotlib.use("Agg")
17
- import matplotlib.pyplot as plt
18
- import matplotlib.dates as mdates
19
- from matplotlib.patches import Rectangle
20
- import pandas as pd
21
-
22
- from onesecondtrader.orchestrator import Orchestrator
23
- from onesecondtrader.connectors.brokers import SimulatedBroker
24
- from onesecondtrader.connectors.datafeeds import SimulatedDatafeed
25
- from onesecondtrader.core.models.orders import ActionType
26
- from . import registry
27
-
28
- CHARTS_DIR = pathlib.Path(os.environ.get("CHARTS_DIR", "charts"))
29
-
30
- app = FastAPI(title="OneSecondTrader Dashboard")
31
-
32
- _running_jobs: dict[str, str] = {}
33
-
34
- RTYPE_TO_BAR_PERIOD = {32: "SECOND", 33: "MINUTE", 34: "HOUR", 35: "DAY"}
35
- BAR_PERIOD_TO_RTYPE = {"SECOND": 32, "MINUTE": 33, "HOUR": 34, "DAY": 35}
36
- BAR_PERIOD_ENUM_TO_NAME = {1: "SECOND", 2: "MINUTE", 3: "HOUR", 4: "DAY"}
37
-
38
-
39
- def _normalize_bar_period(value) -> str | None:
40
- if value is None:
41
- return None
42
- if isinstance(value, str):
43
- if value in BAR_PERIOD_TO_RTYPE:
44
- return value
45
- try:
46
- value = int(value)
47
- except ValueError:
48
- return value
49
- if isinstance(value, int):
50
- return BAR_PERIOD_ENUM_TO_NAME.get(value, str(value))
51
- return str(value)
52
-
53
-
54
- def _get_secmaster_path() -> str:
55
- return os.environ.get("SECMASTER_DB_PATH", "secmaster.db")
56
-
57
-
58
- def _get_available_symbols(bar_period: str | None = None) -> list[str]:
59
- db_path = _get_secmaster_path()
60
- if not os.path.exists(db_path):
61
- return []
62
-
63
- conn = sqlite3.connect(db_path)
64
- cursor = conn.cursor()
65
- if bar_period and bar_period in BAR_PERIOD_TO_RTYPE:
66
- rtype = BAR_PERIOD_TO_RTYPE[bar_period]
67
- cursor.execute(
68
- "SELECT DISTINCT symbol FROM symbol_coverage WHERE rtype = ? ORDER BY symbol",
69
- (rtype,),
70
- )
71
- else:
72
- cursor.execute("SELECT DISTINCT symbol FROM symbol_coverage ORDER BY symbol")
73
- symbols = [row[0] for row in cursor.fetchall()]
74
- conn.close()
75
- return symbols
76
-
77
-
78
- def _get_symbols_with_coverage(bar_period: str) -> list[dict]:
79
- db_path = _get_secmaster_path()
80
- if not os.path.exists(db_path) or bar_period not in BAR_PERIOD_TO_RTYPE:
81
- return []
82
-
83
- rtype = BAR_PERIOD_TO_RTYPE[bar_period]
84
- conn = sqlite3.connect(db_path)
85
- cursor = conn.cursor()
86
- cursor.execute(
87
- "SELECT symbol, min_ts, max_ts FROM symbol_coverage WHERE rtype = ? ORDER BY symbol",
88
- (rtype,),
89
- )
90
- symbols = [
91
- {"symbol": row[0], "min_ts": row[1], "max_ts": row[2]}
92
- for row in cursor.fetchall()
93
- ]
94
- conn.close()
95
- return symbols
96
-
97
-
98
- def _get_available_bar_periods() -> list[str]:
99
- db_path = _get_secmaster_path()
100
- if not os.path.exists(db_path):
101
- return []
102
-
103
- conn = sqlite3.connect(db_path)
104
- cursor = conn.cursor()
105
- cursor.execute("SELECT DISTINCT rtype FROM symbol_coverage ORDER BY rtype")
106
- periods = [
107
- RTYPE_TO_BAR_PERIOD[row[0]]
108
- for row in cursor.fetchall()
109
- if row[0] in RTYPE_TO_BAR_PERIOD
110
- ]
111
- conn.close()
112
- return periods
113
-
114
-
115
- BASE_STYLE = """
116
- * { margin: 0; padding: 0; box-sizing: border-box; }
117
- body {
118
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
119
- background: #0d1117;
120
- color: #e6edf3;
121
- min-height: 100vh;
122
- display: flex;
123
- }
124
- .sidebar {
125
- width: 220px;
126
- background: #161b22;
127
- border-right: 1px solid #30363d;
128
- min-height: 100vh;
129
- position: fixed;
130
- left: 0;
131
- top: 0;
132
- display: flex;
133
- flex-direction: column;
134
- }
135
- .sidebar-header {
136
- padding: 20px 16px;
137
- border-bottom: 1px solid #30363d;
138
- }
139
- .sidebar-header h1 {
140
- font-size: 16px;
141
- font-weight: 600;
142
- color: #e6edf3;
143
- }
144
- .sidebar-nav {
145
- padding: 12px 8px;
146
- flex: 1;
147
- }
148
- .sidebar-nav a {
149
- display: flex;
150
- align-items: center;
151
- gap: 10px;
152
- padding: 10px 12px;
153
- color: #8b949e;
154
- text-decoration: none;
155
- font-size: 14px;
156
- border-radius: 6px;
157
- margin-bottom: 2px;
158
- }
159
- .sidebar-nav a:hover {
160
- background: #21262d;
161
- color: #e6edf3;
162
- }
163
- .sidebar-nav a.active {
164
- background: #21262d;
165
- color: #e6edf3;
166
- }
167
- .sidebar-nav svg {
168
- width: 16px;
169
- height: 16px;
170
- flex-shrink: 0;
171
- }
172
- .nav-group {
173
- margin-bottom: 2px;
174
- }
175
- .nav-group-toggle {
176
- display: flex;
177
- align-items: center;
178
- gap: 10px;
179
- padding: 10px 12px;
180
- color: #8b949e;
181
- text-decoration: none;
182
- font-size: 14px;
183
- border-radius: 6px;
184
- cursor: pointer;
185
- width: 100%;
186
- background: none;
187
- border: none;
188
- }
189
- .nav-group-toggle:hover {
190
- background: #21262d;
191
- color: #e6edf3;
192
- }
193
- .nav-group-toggle.active {
194
- background: #21262d;
195
- color: #e6edf3;
196
- }
197
- .nav-group-toggle .chevron {
198
- margin-left: auto;
199
- transition: transform 0.2s;
200
- }
201
- .nav-group.open .chevron {
202
- transform: rotate(90deg);
203
- }
204
- .nav-group-items {
205
- display: none;
206
- padding-left: 26px;
207
- }
208
- .nav-group.open .nav-group-items {
209
- display: block;
210
- }
211
- .nav-group-items a {
212
- padding: 8px 12px;
213
- font-size: 13px;
214
- }
215
- .main-content {
216
- margin-left: 220px;
217
- flex: 1;
218
- min-height: 100vh;
219
- }
220
- .container {
221
- max-width: 1200px;
222
- margin: 0 auto;
223
- padding: 32px 24px;
224
- }
225
- .card {
226
- background: #161b22;
227
- border: 1px solid #30363d;
228
- border-radius: 8px;
229
- padding: 24px;
230
- margin-bottom: 16px;
231
- }
232
- .card h2 {
233
- font-size: 16px;
234
- font-weight: 600;
235
- margin-bottom: 16px;
236
- color: #e6edf3;
237
- }
238
- .empty-state {
239
- text-align: center;
240
- padding: 48px;
241
- color: #8b949e;
242
- }
243
- .empty-state p { margin-top: 8px; font-size: 14px; }
244
- .badge {
245
- display: inline-block;
246
- padding: 4px 10px;
247
- border-radius: 12px;
248
- font-size: 12px;
249
- font-weight: 500;
250
- }
251
- .badge-green { background: #238636; color: #fff; }
252
- .badge-yellow { background: #9e6a03; color: #fff; }
253
- .badge-red { background: #da3633; color: #fff; }
254
- """
255
-
256
-
257
- SIDEBAR_HTML = """
258
- <aside class="sidebar">
259
- <div class="sidebar-header">
260
- <h1>OneSecondTrader</h1>
261
- </div>
262
- <nav class="sidebar-nav">
263
- <a href="/" class="{runs_active}">
264
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>
265
- Runs
266
- </a>
267
- <a href="/backtest" class="{backtest_active}">
268
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
269
- Backtest
270
- </a>
271
- <a href="/secmaster" class="{secmaster_active}">
272
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg>
273
- Securities Master
274
- </a>
275
- <div class="nav-group {pipeline_open}">
276
- <button class="nav-group-toggle {pipeline_active}" onclick="this.parentElement.classList.toggle('open')">
277
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>
278
- Dev Pipeline
279
- <svg class="chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24" width="12" height="12"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
280
- </button>
281
- <div class="nav-group-items">
282
- <a href="/pipeline/signal-validation" class="{pipeline_sub1_active}">Signal Validation</a>
283
- <a href="/pipeline/sub2" class="{pipeline_sub2_active}">Sub Tab 2</a>
284
- </div>
285
- </div>
286
- </nav>
287
- </aside>
288
- """
289
-
290
-
291
- RUNS_STYLE = """
292
- .runs-table { width: 100%; border-collapse: collapse; }
293
- .runs-table th, .runs-table td { padding: 12px; text-align: left; border-bottom: 1px solid #30363d; }
294
- .runs-table th { font-size: 12px; color: #8b949e; text-transform: uppercase; font-weight: 500; }
295
- .runs-table tbody tr { cursor: pointer; }
296
- .runs-table tbody tr:hover { background: #21262d; }
297
- .runs-table .symbols { font-family: monospace; font-size: 12px; color: #8b949e; }
298
- .runs-table .date-range { font-size: 12px; color: #8b949e; }
299
- .runs-table .delete-btn { background: none; border: none; cursor: pointer; padding: 4px 8px; color: #8b949e; font-size: 16px; border-radius: 4px; }
300
- .runs-table .delete-btn:hover { background: #f8514926; color: #f85149; }
301
- .runs-table .actions { text-align: center; width: 50px; }
302
- """
303
-
304
- RUNS_SCRIPT = """
305
- async function deleteRun(runId, event) {
306
- event.stopPropagation();
307
- if (!confirm('Delete this run? This cannot be undone.')) return;
308
- const res = await fetch(`/api/run/${runId}`, { method: 'DELETE' });
309
- if (res.ok) loadRuns();
310
- else alert('Failed to delete run');
311
- }
312
- async function loadRuns() {
313
- const container = document.getElementById('runs-content');
314
- const res = await fetch('/api/runs');
315
- const data = await res.json();
316
- if (!data.runs || data.runs.length === 0) {
317
- container.innerHTML = `
318
- <div class="empty-state">
319
- <p>No runs yet.</p>
320
- <p>Run a strategy with RunRecorder to see results here.</p>
321
- </div>
322
- `;
323
- return;
324
- }
325
- const rows = data.runs.map(r => {
326
- const statusClass = r.status === 'completed' ? 'badge-green' : r.status === 'running' ? 'badge-yellow' : 'badge-red';
327
- const createdAt = r.created_at ? r.created_at.split('T')[0] + ' ' + r.created_at.split('T')[1].split('.')[0] : '-';
328
- const dateRange = r.first_bar && r.last_bar
329
- ? r.first_bar.split('T')[0] + ' → ' + r.last_bar.split('T')[0]
330
- : '-';
331
- return `<tr onclick="window.location='/run/${r.run_id}'">
332
- <td>${createdAt}</td>
333
- <td>${r.strategy}</td>
334
- <td class="symbols">${r.symbols.length}</td>
335
- <td>${r.bar_period || '-'}</td>
336
- <td class="date-range">${dateRange}</td>
337
- <td><span class="badge ${statusClass}">${r.status}</span></td>
338
- <td class="actions"><button class="delete-btn" onclick="deleteRun('${r.run_id}', event)" title="Delete run">🗑</button></td>
339
- </tr>`;
340
- }).join('');
341
- container.innerHTML = `
342
- <table class="runs-table">
343
- <thead><tr><th>Created</th><th>Strategy</th><th>Symbols</th><th>Bar Period</th><th>Date Range</th><th>Status</th><th></th></tr></thead>
344
- <tbody>${rows}</tbody>
345
- </table>
346
- `;
347
- }
348
- document.addEventListener('DOMContentLoaded', loadRuns);
349
- """
350
-
351
-
352
- @app.get("/", response_class=HTMLResponse)
353
- async def index():
354
- sidebar = SIDEBAR_HTML.format(
355
- runs_active="active",
356
- backtest_active="",
357
- secmaster_active="",
358
- pipeline_active="",
359
- pipeline_open="",
360
- pipeline_sub1_active="",
361
- pipeline_sub2_active="",
362
- )
363
- return f"""
364
- <!DOCTYPE html>
365
- <html>
366
- <head>
367
- <title>OneSecondTrader Dashboard</title>
368
- <style>{BASE_STYLE}{RUNS_STYLE}</style>
369
- </head>
370
- <body>
371
- {sidebar}
372
- <main class="main-content">
373
- <div class="container">
374
- <div class="card">
375
- <h2>Recent Runs</h2>
376
- <div id="runs-content">
377
- <p style="color: #8b949e;">Loading...</p>
378
- </div>
379
- </div>
380
- </div>
381
- </main>
382
- <script>{RUNS_SCRIPT}</script>
383
- </body>
384
- </html>
385
- """
386
-
387
-
388
- RUN_DETAIL_STYLE = """
389
- .run-header { margin-bottom: 24px; }
390
- .run-header h2 { margin-bottom: 8px; }
391
- .run-meta { display: flex; gap: 24px; flex-wrap: wrap; color: #8b949e; font-size: 14px; }
392
- .run-meta span { display: flex; align-items: center; gap: 6px; }
393
- .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-bottom: 24px; }
394
- .stat-card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 16px; }
395
- .stat-card .label { font-size: 12px; color: #8b949e; margin-bottom: 4px; }
396
- .stat-card .value { font-size: 24px; font-weight: 600; }
397
- .back-link { color: #58a6ff; text-decoration: none; font-size: 14px; display: inline-block; margin-bottom: 16px; }
398
- .back-link:hover { text-decoration: underline; }
399
- .positions-table { width: 100%; border-collapse: collapse; margin-top: 16px; font-size: 14px; }
400
- .positions-table th { text-align: left; padding: 12px 8px; border-bottom: 1px solid #30363d; color: #8b949e; font-weight: 500; }
401
- .positions-table td { padding: 10px 8px; border-bottom: 1px solid #21262d; }
402
- .positions-table tr:hover { background: #161b22; }
403
- .pnl-positive { color: #3fb950; }
404
- .pnl-negative { color: #f85149; }
405
- .side-long { color: #3fb950; }
406
- .side-short { color: #f85149; }
407
- .positions-summary { display: flex; gap: 24px; margin-bottom: 16px; padding: 12px; background: #161b22; border-radius: 6px; }
408
- .positions-summary .item { display: flex; flex-direction: column; }
409
- .positions-summary .item-label { font-size: 11px; color: #8b949e; text-transform: uppercase; }
410
- .positions-summary .item-value { font-size: 18px; font-weight: 600; }
411
- .positions-filter { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
412
- .positions-filter select { background: #21262d; border: 1px solid #30363d; border-radius: 6px; padding: 8px 12px; color: #c9d1d9; font-size: 14px; min-width: 150px; }
413
- .positions-filter select:focus { outline: none; border-color: #58a6ff; }
414
- .positions-filter label { color: #8b949e; font-size: 14px; }
415
- .position-row { cursor: pointer; }
416
- .position-row:hover { background: #21262d !important; }
417
- .chart-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; justify-content: center; align-items: center; }
418
- .chart-modal-content { background: #161b22; border: 1px solid #30363d; border-radius: 8px; max-width: 95%; max-height: 95%; overflow: auto; }
419
- .chart-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid #30363d; }
420
- .chart-modal-header h3 { margin: 0; font-size: 16px; }
421
- .chart-modal-close { background: none; border: none; color: #8b949e; font-size: 24px; cursor: pointer; padding: 0; line-height: 1; }
422
- .chart-modal-close:hover { color: #e6edf3; }
423
- .chart-modal-body { padding: 20px; }
424
- """
425
-
426
- RUN_DETAIL_SCRIPT = """
427
- let allPositions = [];
428
- function formatTime(ts) {
429
- if (!ts) return '-';
430
- return ts.split('T')[0] + ' ' + ts.split('T')[1].split('.')[0];
431
- }
432
- function formatPnl(pnl) {
433
- const cls = pnl >= 0 ? 'pnl-positive' : 'pnl-negative';
434
- const sign = pnl >= 0 ? '+' : '';
435
- return `<span class="${cls}">${sign}$${pnl.toFixed(2)}</span>`;
436
- }
437
- function computeSummary(positions) {
438
- const totalPnl = positions.reduce((sum, p) => sum + p.pnl, 0);
439
- const winners = positions.filter(p => p.pnl > 0).length;
440
- const losers = positions.filter(p => p.pnl < 0).length;
441
- const winRate = positions.length > 0 ? winners / positions.length : 0;
442
- return { total_positions: positions.length, total_pnl: totalPnl, winners, losers, win_rate: winRate };
443
- }
444
- function renderPositions(positions, selectedSymbol) {
445
- const filtered = selectedSymbol ? positions.filter(p => p.symbol === selectedSymbol) : positions;
446
- const s = computeSummary(filtered);
447
- const winRate = s.win_rate ? (s.win_rate * 100).toFixed(1) + '%' : '-';
448
- const summaryPnlClass = s.total_pnl >= 0 ? 'pnl-positive' : 'pnl-negative';
449
- document.getElementById('summary-pnl').className = 'item-value ' + summaryPnlClass;
450
- document.getElementById('summary-pnl').textContent = (s.total_pnl >= 0 ? '+' : '') + '$' + s.total_pnl.toFixed(2);
451
- document.getElementById('summary-positions').textContent = s.total_positions;
452
- document.getElementById('summary-winners').textContent = s.winners;
453
- document.getElementById('summary-losers').textContent = s.losers;
454
- document.getElementById('summary-winrate').textContent = winRate;
455
- const tbody = document.getElementById('positions-tbody');
456
- tbody.innerHTML = filtered.map((p, i) => `
457
- <tr class="position-row" data-symbol="${p.symbol}" data-position-id="${p.position_id}">
458
- <td>${i + 1}</td>
459
- <td>${p.symbol}</td>
460
- <td class="${p.side === 'LONG' ? 'side-long' : 'side-short'}">${p.side}</td>
461
- <td>${p.quantity}</td>
462
- <td>${formatTime(p.entry_time)}</td>
463
- <td>${formatTime(p.exit_time)}</td>
464
- <td>$${p.avg_entry_price.toFixed(2)}</td>
465
- <td>$${p.avg_exit_price.toFixed(2)}</td>
466
- <td>${formatPnl(p.pnl)}</td>
467
- </tr>
468
- `).join('');
469
- tbody.querySelectorAll('.position-row').forEach(row => {
470
- row.addEventListener('click', () => {
471
- const symbol = row.dataset.symbol;
472
- const positionId = row.dataset.positionId;
473
- showPositionChart(symbol, positionId);
474
- });
475
- });
476
- }
477
- function onSymbolFilterChange() {
478
- const sel = document.getElementById('symbol-filter');
479
- renderPositions(allPositions, sel.value);
480
- }
481
- async function loadRunDetail() {
482
- const runId = window.location.pathname.split('/run/')[1];
483
- const container = document.getElementById('run-content');
484
- const [runRes, posRes] = await Promise.all([
485
- fetch(`/api/run/${runId}`),
486
- fetch(`/api/run/${runId}/positions`)
487
- ]);
488
- if (!runRes.ok) {
489
- container.innerHTML = '<p style="color: #f85149;">Run not found.</p>';
490
- return;
491
- }
492
- const r = await runRes.json();
493
- const posData = await posRes.json();
494
- allPositions = posData.positions || [];
495
- const statusClass = r.status === 'completed' ? 'badge-green' : r.status === 'running' ? 'badge-yellow' : 'badge-red';
496
- const createdAt = formatTime(r.created_at);
497
- const dateRange = r.first_bar && r.last_bar
498
- ? r.first_bar.split('T')[0] + ' → ' + r.last_bar.split('T')[0]
499
- : '-';
500
- const symbols = [...new Set(allPositions.map(p => p.symbol))].sort();
501
- const s = computeSummary(allPositions);
502
- const winRate = s.win_rate ? (s.win_rate * 100).toFixed(1) + '%' : '-';
503
- let positionsHtml = '';
504
- if (allPositions.length > 0) {
505
- const summaryPnlClass = s.total_pnl >= 0 ? 'pnl-positive' : 'pnl-negative';
506
- const symbolOptions = symbols.map(sym => `<option value="${sym}">${sym}</option>`).join('');
507
- positionsHtml = `
508
- <div class="card" style="margin-top: 24px;">
509
- <h3>Positions (Round-Trip Trades)</h3>
510
- <div class="positions-filter">
511
- <label for="symbol-filter">Filter by Symbol:</label>
512
- <select id="symbol-filter" onchange="onSymbolFilterChange()">
513
- <option value="">All Symbols</option>
514
- ${symbolOptions}
515
- </select>
516
- </div>
517
- <div class="positions-summary">
518
- <div class="item">
519
- <span class="item-label">Total P&L</span>
520
- <span id="summary-pnl" class="item-value ${summaryPnlClass}">${s.total_pnl >= 0 ? '+' : ''}$${s.total_pnl.toFixed(2)}</span>
521
- </div>
522
- <div class="item">
523
- <span class="item-label">Positions</span>
524
- <span id="summary-positions" class="item-value">${s.total_positions}</span>
525
- </div>
526
- <div class="item">
527
- <span class="item-label">Winners</span>
528
- <span id="summary-winners" class="item-value pnl-positive">${s.winners}</span>
529
- </div>
530
- <div class="item">
531
- <span class="item-label">Losers</span>
532
- <span id="summary-losers" class="item-value pnl-negative">${s.losers}</span>
533
- </div>
534
- <div class="item">
535
- <span class="item-label">Win Rate</span>
536
- <span id="summary-winrate" class="item-value">${winRate}</span>
537
- </div>
538
- </div>
539
- <table class="positions-table">
540
- <thead>
541
- <tr>
542
- <th>#</th><th>Symbol</th><th>Side</th><th>Qty</th>
543
- <th>Entry Time</th><th>Exit Time</th><th>Avg Entry</th><th>Avg Exit</th><th>P&L</th>
544
- </tr>
545
- </thead>
546
- <tbody id="positions-tbody"></tbody>
547
- </table>
548
- </div>
549
- `;
550
- } else {
551
- positionsHtml = `
552
- <div class="card" style="margin-top: 24px;">
553
- <h3>Positions (Round-Trip Trades)</h3>
554
- <p style="color: #8b949e;">No completed positions.</p>
555
- </div>
556
- `;
557
- }
558
-
559
- container.innerHTML = `
560
- <a href="/" class="back-link">← Back to Runs</a>
561
- <div class="run-header">
562
- <h2>${r.strategy}</h2>
563
- <div class="run-meta">
564
- <span><span class="badge ${statusClass}">${r.status}</span></span>
565
- <span>Created: ${createdAt}</span>
566
- <span>Mode: ${r.mode}</span>
567
- </div>
568
- </div>
569
- <div class="stats-grid">
570
- <div class="stat-card">
571
- <div class="label">Symbols</div>
572
- <div class="value">${r.symbols.length}</div>
573
- </div>
574
- <div class="stat-card">
575
- <div class="label">Bar Period</div>
576
- <div class="value">${r.bar_period || '-'}</div>
577
- </div>
578
- <div class="stat-card">
579
- <div class="label">Total Bars</div>
580
- <div class="value">${r.bar_count || 0}</div>
581
- </div>
582
- <div class="stat-card">
583
- <div class="label">Date Range</div>
584
- <div class="value" style="font-size: 14px;">${dateRange}</div>
585
- </div>
586
- </div>
587
- <div class="card">
588
- <h3>Symbols</h3>
589
- <p style="color: #8b949e; font-family: monospace;">${r.symbols.join(', ') || 'None'}</p>
590
- </div>
591
- ${positionsHtml}
592
- `;
593
- if (allPositions.length > 0) {
594
- renderPositions(allPositions, '');
595
- }
596
- }
597
- function showPositionChart(symbol, positionId) {
598
- const modal = document.getElementById('chart-modal');
599
- const modalTitle = document.getElementById('chart-modal-title');
600
- const modalBody = document.getElementById('chart-modal-body');
601
- const runId = window.location.pathname.split('/run/')[1];
602
-
603
- modalTitle.textContent = `Position Chart - ${symbol} #${positionId}`;
604
- modalBody.innerHTML = '<div style="text-align: center; padding: 40px;"><p>Generating chart...</p></div>';
605
- modal.style.display = 'flex';
606
-
607
- fetch(`/api/run/${runId}/positions/${encodeURIComponent(symbol)}/${positionId}/chart`)
608
- .then(response => {
609
- if (!response.ok) {
610
- return response.json().then(data => { throw new Error(data.error || 'Failed to load chart'); });
611
- }
612
- return response.blob();
613
- })
614
- .then(blob => {
615
- const url = URL.createObjectURL(blob);
616
- modalBody.innerHTML = `<img src="${url}" style="max-width: 100%; max-height: 80vh;">`;
617
- })
618
- .catch(error => {
619
- modalBody.innerHTML = `<div style="color: #f85149; padding: 20px;">${error.message}</div>`;
620
- });
621
- }
622
- function closeChartModal() {
623
- document.getElementById('chart-modal').style.display = 'none';
624
- }
625
- document.addEventListener('DOMContentLoaded', loadRunDetail);
626
- """
627
-
628
-
629
- SIGNAL_VALIDATION_STYLE = """
630
- .sv-layout { display: flex; gap: 0; height: calc(100vh - 40px); }
631
- .sv-sidebar { width: 280px; background: #161b22; border-right: 1px solid #30363d; overflow-y: auto; flex-shrink: 0; }
632
- .sv-main { flex: 1; overflow-y: auto; padding: 20px; }
633
- .sv-run-selector { padding: 16px; border-bottom: 1px solid #30363d; }
634
- .sv-run-selector select { width: 100%; background: #21262d; border: 1px solid #30363d; border-radius: 6px; padding: 10px 12px; color: #c9d1d9; font-size: 14px; }
635
- .sv-run-selector select:focus { outline: none; border-color: #58a6ff; }
636
- .sv-run-selector label { display: block; color: #8b949e; font-size: 12px; text-transform: uppercase; margin-bottom: 8px; }
637
- .sv-action-group { border-bottom: 1px solid #30363d; }
638
- .sv-action-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; cursor: pointer; color: #c9d1d9; font-weight: 500; }
639
- .sv-action-header:hover { background: #21262d; }
640
- .sv-action-header .count { background: #30363d; color: #8b949e; padding: 2px 8px; border-radius: 10px; font-size: 12px; font-weight: normal; }
641
- .sv-action-header .chevron { transition: transform 0.2s; }
642
- .sv-action-group.open .sv-action-header .chevron { transform: rotate(90deg); }
643
- .sv-action-signals { display: none; padding: 8px 16px 12px; }
644
- .sv-action-group.open .sv-action-signals { display: block; }
645
- .sv-signal-pill { display: inline-block; background: #21262d; border: 1px solid #30363d; border-radius: 16px; padding: 4px 12px; margin: 4px; font-size: 13px; color: #c9d1d9; cursor: pointer; transition: all 0.15s; }
646
- .sv-signal-pill:hover { border-color: #58a6ff; background: #1f6feb22; }
647
- .sv-signal-pill.active { background: #1f6feb; border-color: #1f6feb; color: white; }
648
- .sv-signal-pill .pill-count { color: #8b949e; font-size: 11px; margin-left: 4px; }
649
- .sv-signal-pill.active .pill-count { color: rgba(255,255,255,0.7); }
650
- .sv-content-header { margin-bottom: 20px; }
651
- .sv-content-header h2 { margin: 0 0 4px; font-size: 20px; }
652
- .sv-content-header .subtitle { color: #8b949e; font-size: 14px; }
653
- .sv-empty { color: #8b949e; text-align: center; padding: 60px 20px; }
654
- .sv-table { width: 100%; border-collapse: collapse; }
655
- .sv-table th, .sv-table td { padding: 12px; text-align: left; border-bottom: 1px solid #30363d; }
656
- .sv-table th { font-size: 12px; color: #8b949e; text-transform: uppercase; font-weight: 500; }
657
- .sv-table tbody tr { cursor: pointer; }
658
- .sv-table tbody tr:hover { background: #21262d; }
659
- .sv-status { padding: 3px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; }
660
- .sv-status-filled { background: #23863633; color: #3fb950; }
661
- .sv-status-accepted { background: #1f6feb33; color: #58a6ff; }
662
- .sv-status-rejected { background: #f8514933; color: #f85149; }
663
- .sv-status-expired { background: #6e768133; color: #8b949e; }
664
- .sv-status-cancelled { background: #6e768133; color: #8b949e; }
665
- .sv-status-pending { background: #d29922; color: #0d1117; }
666
- .sv-side-buy { color: #3fb950; }
667
- .sv-side-sell { color: #f85149; }
668
- """
669
-
670
- SIGNAL_VALIDATION_SCRIPT = """
671
- let currentRun = null;
672
- let currentAction = null;
673
- let currentSignal = null;
674
- let signalData = {};
675
-
676
- async function loadRuns() {
677
- const select = document.getElementById('run-select');
678
- const res = await fetch('/api/runs');
679
- const data = await res.json();
680
- select.innerHTML = '<option value="">Select a run...</option>' +
681
- data.runs.map(r => `<option value="${r.run_id}">${r.run_id}</option>`).join('');
682
- }
683
-
684
- async function onRunChange() {
685
- const runId = document.getElementById('run-select').value;
686
- if (!runId) {
687
- currentRun = null;
688
- signalData = {};
689
- renderSidebar();
690
- renderContent();
691
- return;
692
- }
693
- currentRun = runId;
694
- currentAction = null;
695
- currentSignal = null;
696
- const res = await fetch(`/api/signals/${runId}`);
697
- signalData = await res.json();
698
- renderSidebar();
699
- renderContent();
700
- }
701
-
702
- function renderSidebar() {
703
- const container = document.getElementById('action-groups');
704
- if (!currentRun || !signalData.actions) {
705
- container.innerHTML = '<div class="sv-empty">Select a run to view signals</div>';
706
- return;
707
- }
708
- const actions = signalData.action_types || [];
709
- container.innerHTML = actions.map(action => {
710
- const signals = signalData.actions[action] || {};
711
- const signalNames = Object.keys(signals);
712
- const totalCount = signalNames.reduce((sum, s) => sum + signals[s].length, 0);
713
- const isOpen = currentAction === action ? 'open' : '';
714
- const pillsHtml = signalNames.length > 0
715
- ? signalNames.map(sig => {
716
- const isActive = currentAction === action && currentSignal === sig ? 'active' : '';
717
- return `<span class="sv-signal-pill ${isActive}" onclick="selectSignal('${action}', '${sig}')">${sig}<span class="pill-count">(${signals[sig].length})</span></span>`;
718
- }).join('')
719
- : '<span style="color: #6e7681; font-size: 13px;">No signals</span>';
720
- return `
721
- <div class="sv-action-group ${isOpen}">
722
- <div class="sv-action-header" onclick="toggleAction('${action}')">
723
- <span>${action}</span>
724
- <span>
725
- <span class="count">${totalCount}</span>
726
- <svg class="chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
727
- </span>
728
- </div>
729
- <div class="sv-action-signals">${pillsHtml}</div>
730
- </div>
731
- `;
732
- }).join('');
733
- }
734
-
735
- function toggleAction(action) {
736
- if (currentAction === action) {
737
- currentAction = null;
738
- currentSignal = null;
739
- } else {
740
- currentAction = action;
741
- currentSignal = null;
742
- }
743
- renderSidebar();
744
- renderContent();
745
- }
746
-
747
- function selectSignal(action, signal) {
748
- currentAction = action;
749
- currentSignal = signal;
750
- renderSidebar();
751
- renderContent();
752
- }
753
-
754
- function formatTime(ts) {
755
- if (!ts) return '-';
756
- return ts.split('T')[0] + ' ' + ts.split('T')[1].split('.')[0];
757
- }
758
-
759
- function renderContent() {
760
- const container = document.getElementById('sv-content');
761
- if (!currentRun) {
762
- container.innerHTML = '<div class="sv-empty">Select a run from the dropdown to begin</div>';
763
- return;
764
- }
765
- if (!currentAction || !currentSignal) {
766
- container.innerHTML = '<div class="sv-empty">Select an action and signal from the sidebar</div>';
767
- return;
768
- }
769
- const orders = signalData.actions[currentAction]?.[currentSignal] || [];
770
- if (orders.length === 0) {
771
- container.innerHTML = '<div class="sv-empty">No orders for this signal</div>';
772
- return;
773
- }
774
- container.innerHTML = `
775
- <div class="sv-content-header">
776
- <h2>${currentSignal}</h2>
777
- <div class="subtitle">Action: ${currentAction} &bull; ${orders.length} order(s)</div>
778
- </div>
779
- <table class="sv-table">
780
- <thead>
781
- <tr>
782
- <th>Time</th>
783
- <th>Symbol</th>
784
- <th>Side</th>
785
- <th>Type</th>
786
- <th>Qty</th>
787
- <th>Price</th>
788
- <th>Status</th>
789
- </tr>
790
- </thead>
791
- <tbody>
792
- ${orders.map(o => `
793
- <tr onclick="showSignalChart('${o.order_id}', '${o.symbol}')">
794
- <td>${formatTime(o.ts_event)}</td>
795
- <td style="font-family: monospace;">${o.symbol}</td>
796
- <td class="sv-side-${o.side.toLowerCase()}">${o.side}</td>
797
- <td>${o.order_type || '-'}</td>
798
- <td>${o.quantity || '-'}</td>
799
- <td>${o.limit_price ? '$' + o.limit_price.toFixed(2) : (o.stop_price ? 'Stop $' + o.stop_price.toFixed(2) : 'MKT')}</td>
800
- <td><span class="sv-status sv-status-${o.status.toLowerCase()}">${o.status}</span></td>
801
- </tr>
802
- `).join('')}
803
- </tbody>
804
- </table>
805
- `;
806
- }
807
-
808
- function showSignalChart(orderId, symbol) {
809
- const modal = document.getElementById('chart-modal');
810
- const modalTitle = document.getElementById('chart-modal-title');
811
- const modalBody = document.getElementById('chart-modal-body');
812
- modalTitle.textContent = `Signal Chart - ${symbol}`;
813
- modalBody.innerHTML = '<div style="text-align: center; padding: 40px;"><p>Generating chart...</p></div>';
814
- modal.style.display = 'flex';
815
- fetch(`/api/signals/${currentRun}/chart/${orderId}`)
816
- .then(response => {
817
- if (!response.ok) {
818
- return response.json().then(data => { throw new Error(data.detail || 'Failed to load chart'); });
819
- }
820
- return response.blob();
821
- })
822
- .then(blob => {
823
- const url = URL.createObjectURL(blob);
824
- modalBody.innerHTML = `<img src="${url}" style="max-width: 100%; max-height: 80vh;">`;
825
- })
826
- .catch(error => {
827
- modalBody.innerHTML = `<div style="color: #f85149; padding: 20px;">${error.message}</div>`;
828
- });
829
- }
830
-
831
- function closeChartModal() {
832
- document.getElementById('chart-modal').style.display = 'none';
833
- }
834
-
835
- document.addEventListener('DOMContentLoaded', loadRuns);
836
- """
837
-
838
-
839
- @app.get("/run/{run_id}", response_class=HTMLResponse)
840
- async def run_detail_page(run_id: str):
841
- sidebar = SIDEBAR_HTML.format(
842
- runs_active="active",
843
- backtest_active="",
844
- secmaster_active="",
845
- pipeline_active="",
846
- pipeline_open="",
847
- pipeline_sub1_active="",
848
- pipeline_sub2_active="",
849
- )
850
- return f"""
851
- <!DOCTYPE html>
852
- <html>
853
- <head>
854
- <title>Run Detail - OneSecondTrader</title>
855
- <style>{BASE_STYLE}{RUN_DETAIL_STYLE}</style>
856
- </head>
857
- <body>
858
- {sidebar}
859
- <main class="main-content">
860
- <div class="container">
861
- <div id="run-content">
862
- <p style="color: #8b949e;">Loading...</p>
863
- </div>
864
- </div>
865
- </main>
866
- <div id="chart-modal" class="chart-modal" onclick="if(event.target===this)closeChartModal()">
867
- <div class="chart-modal-content">
868
- <div class="chart-modal-header">
869
- <h3 id="chart-modal-title">Position Chart</h3>
870
- <button class="chart-modal-close" onclick="closeChartModal()">&times;</button>
871
- </div>
872
- <div id="chart-modal-body" class="chart-modal-body"></div>
873
- </div>
874
- </div>
875
- <script>{RUN_DETAIL_SCRIPT}</script>
876
- </body>
877
- </html>
878
- """
879
-
880
-
881
- def _get_runs_db_path() -> str:
882
- return os.environ.get("RUNS_DB_PATH", "runs.db")
883
-
884
-
885
- def _get_recent_runs(limit: int = 50) -> list[dict]:
886
- import json
887
-
888
- db_path = _get_runs_db_path()
889
- if not os.path.exists(db_path):
890
- return []
891
-
892
- conn = sqlite3.connect(db_path)
893
- cursor = conn.cursor()
894
- cursor.execute(
895
- """
896
- SELECT
897
- r.run_id,
898
- r.strategy,
899
- r.symbols,
900
- r.bar_period,
901
- r.mode,
902
- r.status,
903
- r.created_at,
904
- r.completed_at,
905
- COUNT(b.id) as bar_count,
906
- MIN(b.ts_event) as first_bar,
907
- MAX(b.ts_event) as last_bar
908
- FROM runs r
909
- LEFT JOIN bars b ON r.run_id = b.run_id
910
- GROUP BY r.run_id
911
- ORDER BY r.created_at DESC
912
- LIMIT ?
913
- """,
914
- (limit,),
915
- )
916
- runs = []
917
- for row in cursor.fetchall():
918
- runs.append(
919
- {
920
- "run_id": row[0],
921
- "strategy": row[1],
922
- "symbols": json.loads(row[2]) if row[2] else [],
923
- "bar_period": _normalize_bar_period(row[3]),
924
- "mode": row[4],
925
- "status": row[5],
926
- "created_at": row[6],
927
- "completed_at": row[7],
928
- "bar_count": row[8],
929
- "first_bar": row[9],
930
- "last_bar": row[10],
931
- }
932
- )
933
- conn.close()
934
- return runs
935
-
936
-
937
- @app.get("/api/runs")
938
- async def list_runs(limit: int = 50):
939
- return {"runs": _get_recent_runs(limit)}
940
-
941
-
942
- def _get_run_by_id(run_id: str) -> dict | None:
943
- import json
944
-
945
- db_path = _get_runs_db_path()
946
- if not os.path.exists(db_path):
947
- return None
948
-
949
- conn = sqlite3.connect(db_path)
950
- cursor = conn.cursor()
951
- cursor.execute(
952
- """
953
- SELECT
954
- r.run_id,
955
- r.strategy,
956
- r.symbols,
957
- r.bar_period,
958
- r.mode,
959
- r.status,
960
- r.created_at,
961
- r.completed_at,
962
- COUNT(b.id) as bar_count,
963
- MIN(b.ts_event) as first_bar,
964
- MAX(b.ts_event) as last_bar
965
- FROM runs r
966
- LEFT JOIN bars b ON r.run_id = b.run_id
967
- WHERE r.run_id = ?
968
- GROUP BY r.run_id
969
- """,
970
- (run_id,),
971
- )
972
- row = cursor.fetchone()
973
- conn.close()
974
- if not row:
975
- return None
976
- return {
977
- "run_id": row[0],
978
- "strategy": row[1],
979
- "symbols": json.loads(row[2]) if row[2] else [],
980
- "bar_period": _normalize_bar_period(row[3]),
981
- "mode": row[4],
982
- "status": row[5],
983
- "created_at": row[6],
984
- "completed_at": row[7],
985
- "bar_count": row[8],
986
- "first_bar": row[9],
987
- "last_bar": row[10],
988
- }
989
-
990
-
991
- @app.get("/api/run/{run_id}")
992
- async def get_run(run_id: str):
993
- run = _get_run_by_id(run_id)
994
- if not run:
995
- from fastapi import HTTPException
996
-
997
- raise HTTPException(status_code=404, detail="Run not found")
998
- return run
999
-
1000
-
1001
- @app.delete("/api/run/{run_id}")
1002
- async def delete_run(run_id: str):
1003
- db_path = _get_runs_db_path()
1004
- if not os.path.exists(db_path):
1005
- from fastapi import HTTPException
1006
-
1007
- raise HTTPException(status_code=404, detail="Run not found")
1008
- conn = sqlite3.connect(db_path)
1009
- cursor = conn.cursor()
1010
- cursor.execute("DELETE FROM bars WHERE run_id = ?", (run_id,))
1011
- cursor.execute("DELETE FROM order_requests WHERE run_id = ?", (run_id,))
1012
- cursor.execute("DELETE FROM order_responses WHERE run_id = ?", (run_id,))
1013
- cursor.execute("DELETE FROM fills WHERE run_id = ?", (run_id,))
1014
- cursor.execute("DELETE FROM runs WHERE run_id = ?", (run_id,))
1015
- conn.commit()
1016
- conn.close()
1017
- charts_path = CHARTS_DIR / run_id
1018
- if charts_path.exists():
1019
- shutil.rmtree(charts_path)
1020
- return {"status": "deleted"}
1021
-
1022
-
1023
- def _normalize_side(side) -> str:
1024
- if side in ("BUY", 1, "1"):
1025
- return "BUY"
1026
- return "SELL"
1027
-
1028
-
1029
- def _extract_positions(fills: list[dict]) -> list[dict]:
1030
- if not fills:
1031
- return []
1032
- for f in fills:
1033
- f["side"] = _normalize_side(f["side"])
1034
- positions = []
1035
- by_symbol: dict[str, list[dict]] = {}
1036
- for f in fills:
1037
- by_symbol.setdefault(f["symbol"], []).append(f)
1038
- for symbol, symbol_fills in by_symbol.items():
1039
- symbol_fills.sort(key=lambda x: x["ts_event"])
1040
- position = 0.0
1041
- position_fills: list[dict] = []
1042
- position_id = 0
1043
- for fill in symbol_fills:
1044
- qty = fill["quantity"]
1045
- signed_qty = qty if fill["side"] == "BUY" else -qty
1046
- new_position = position + signed_qty
1047
- position_fills.append(fill)
1048
- if new_position == 0.0 and position != 0.0:
1049
- position_id += 1
1050
- pnl = 0.0
1051
- total_commission = 0.0
1052
- for pf in position_fills:
1053
- value = pf["price"] * pf["quantity"]
1054
- commission = pf.get("commission") or 0.0
1055
- total_commission += commission
1056
- if pf["side"] == "BUY":
1057
- pnl -= value
1058
- else:
1059
- pnl += value
1060
- pnl -= total_commission
1061
- entry_fill = position_fills[0]
1062
- exit_fill = position_fills[-1]
1063
- entry_side = entry_fill["side"]
1064
- entry_qty = sum(
1065
- pf["quantity"] for pf in position_fills if pf["side"] == entry_side
1066
- )
1067
- entry_value = sum(
1068
- pf["quantity"] * pf["price"]
1069
- for pf in position_fills
1070
- if pf["side"] == entry_side
1071
- )
1072
- avg_entry = entry_value / entry_qty if entry_qty else 0
1073
- exit_side = "SELL" if entry_side == "BUY" else "BUY"
1074
- exit_qty = sum(
1075
- pf["quantity"] for pf in position_fills if pf["side"] == exit_side
1076
- )
1077
- exit_value = sum(
1078
- pf["quantity"] * pf["price"]
1079
- for pf in position_fills
1080
- if pf["side"] == exit_side
1081
- )
1082
- avg_exit = exit_value / exit_qty if exit_qty else 0
1083
- positions.append(
1084
- {
1085
- "position_id": position_id,
1086
- "symbol": symbol,
1087
- "side": "LONG" if entry_side == "BUY" else "SHORT",
1088
- "quantity": entry_qty,
1089
- "entry_time": entry_fill["ts_event"],
1090
- "exit_time": exit_fill["ts_event"],
1091
- "avg_entry_price": avg_entry,
1092
- "avg_exit_price": avg_exit,
1093
- "pnl": pnl,
1094
- "commission": total_commission,
1095
- "num_fills": len(position_fills),
1096
- }
1097
- )
1098
- position_fills = []
1099
- position = new_position
1100
- positions.sort(key=lambda x: x["entry_time"])
1101
- return positions
1102
-
1103
-
1104
- @app.get("/api/run/{run_id}/positions")
1105
- async def get_run_positions(run_id: str):
1106
- db_path = _get_runs_db_path()
1107
- if not os.path.exists(db_path):
1108
- return {"positions": []}
1109
- conn = sqlite3.connect(db_path)
1110
- cursor = conn.cursor()
1111
- cursor.execute(
1112
- """
1113
- SELECT ts_event, symbol, side, quantity, price, commission
1114
- FROM fills WHERE run_id = ? ORDER BY ts_event
1115
- """,
1116
- (run_id,),
1117
- )
1118
- rows = cursor.fetchall()
1119
- conn.close()
1120
- fills = [
1121
- {
1122
- "ts_event": row[0],
1123
- "symbol": row[1],
1124
- "side": row[2],
1125
- "quantity": row[3],
1126
- "price": row[4],
1127
- "commission": row[5],
1128
- }
1129
- for row in rows
1130
- ]
1131
- positions = _extract_positions(fills)
1132
- total_pnl = sum(p["pnl"] for p in positions)
1133
- winners = sum(1 for p in positions if p["pnl"] > 0)
1134
- losers = sum(1 for p in positions if p["pnl"] < 0)
1135
- return {
1136
- "positions": positions,
1137
- "summary": {
1138
- "total_positions": len(positions),
1139
- "total_pnl": total_pnl,
1140
- "winners": winners,
1141
- "losers": losers,
1142
- "win_rate": winners / len(positions) if positions else 0,
1143
- },
1144
- }
1145
-
1146
-
1147
- def _load_bars_for_chart(run_id: str, symbol: str) -> pd.DataFrame | None:
1148
- db_path = _get_runs_db_path()
1149
- if not os.path.exists(db_path):
1150
- return None
1151
- conn = sqlite3.connect(db_path)
1152
- bars_df = pd.read_sql(
1153
- "SELECT * FROM bars WHERE run_id = ? AND symbol = ? ORDER BY ts_event",
1154
- conn,
1155
- params=(run_id, symbol),
1156
- )
1157
- conn.close()
1158
- if bars_df.empty:
1159
- return None
1160
- bars_df["ts_event"] = pd.to_datetime(bars_df["ts_event"])
1161
- if "indicators" in bars_df.columns:
1162
- indicator_rows = []
1163
- for _, row in bars_df.iterrows():
1164
- indicators_json = row.get("indicators")
1165
- if indicators_json and isinstance(indicators_json, str):
1166
- try:
1167
- indicators = json.loads(indicators_json)
1168
- indicator_rows.append(indicators)
1169
- except json.JSONDecodeError:
1170
- indicator_rows.append({})
1171
- else:
1172
- indicator_rows.append({})
1173
- if indicator_rows:
1174
- indicators_df = pd.DataFrame(indicator_rows)
1175
- for col in indicators_df.columns:
1176
- bars_df[col] = indicators_df[col].values
1177
- return bars_df
1178
-
1179
-
1180
- def _load_fills_for_chart(run_id: str, symbol: str) -> list[dict]:
1181
- db_path = _get_runs_db_path()
1182
- if not os.path.exists(db_path):
1183
- return []
1184
- conn = sqlite3.connect(db_path)
1185
- cursor = conn.cursor()
1186
- cursor.execute(
1187
- """SELECT ts_event, symbol, side, quantity, price, commission, order_id
1188
- FROM fills WHERE run_id = ? AND symbol = ? ORDER BY ts_event""",
1189
- (run_id, symbol),
1190
- )
1191
- rows = cursor.fetchall()
1192
- conn.close()
1193
- return [
1194
- {
1195
- "ts_event": pd.to_datetime(row[0]),
1196
- "symbol": row[1],
1197
- "side": row[2],
1198
- "quantity": row[3],
1199
- "price": row[4],
1200
- "commission": row[5] or 0.0,
1201
- "order_id": row[6],
1202
- }
1203
- for row in rows
1204
- ]
1205
-
1206
-
1207
- _OHLCV_COLUMNS = {
1208
- "ts_event",
1209
- "symbol",
1210
- "bar_period",
1211
- "open",
1212
- "high",
1213
- "low",
1214
- "close",
1215
- "volume",
1216
- "run_id",
1217
- "indicators",
1218
- }
1219
- _MAIN_COLORS = ["orange", "purple", "brown", "pink", "gray", "cyan", "magenta"]
1220
- _SUBPLOT_COLORS = [
1221
- "#1f77b4",
1222
- "#ff7f0e",
1223
- "#2ca02c",
1224
- "#d62728",
1225
- "#9467bd",
1226
- "#8c564b",
1227
- "#e377c2",
1228
- "#7f7f7f",
1229
- ]
1230
-
1231
-
1232
- def _parse_indicator_column(col: str) -> tuple[int, str] | None:
1233
- if col in _OHLCV_COLUMNS or len(col) < 4 or col[2] != "_":
1234
- return None
1235
- try:
1236
- plot_at = int(col[:2])
1237
- display_name = col[3:]
1238
- return (plot_at, display_name)
1239
- except ValueError:
1240
- return None
1241
-
1242
-
1243
- def _get_indicator_columns(data: pd.DataFrame) -> dict[str, tuple[int, str]]:
1244
- result = {}
1245
- for col in data.columns:
1246
- parsed = _parse_indicator_column(col)
1247
- if parsed is not None:
1248
- result[col] = parsed
1249
- return result
1250
-
1251
-
1252
- def _main_indicator_columns(data: pd.DataFrame) -> list[tuple[str, str]]:
1253
- indicators = _get_indicator_columns(data)
1254
- return [(col, name) for col, (plot_at, name) in indicators.items() if plot_at == 0]
1255
-
1256
-
1257
- def _subplot_groups_from_data(data: pd.DataFrame) -> dict[int, list[tuple[str, str]]]:
1258
- indicators = _get_indicator_columns(data)
1259
- groups: dict[int, list[tuple[str, str]]] = {}
1260
- for col, (plot_at, name) in indicators.items():
1261
- if 1 <= plot_at <= 98:
1262
- groups.setdefault(plot_at, []).append((col, name))
1263
- return groups
1264
-
1265
-
1266
- def _extract_positions_for_chart(run_id: str) -> list[dict]:
1267
- db_path = _get_runs_db_path()
1268
- if not os.path.exists(db_path):
1269
- return []
1270
- conn = sqlite3.connect(db_path)
1271
- cursor = conn.cursor()
1272
- cursor.execute(
1273
- """SELECT ts_event, symbol, side, quantity, price, commission, order_id
1274
- FROM fills WHERE run_id = ? ORDER BY ts_event""",
1275
- (run_id,),
1276
- )
1277
- rows = cursor.fetchall()
1278
- conn.close()
1279
- fills = [
1280
- {
1281
- "ts_event": pd.to_datetime(row[0]),
1282
- "symbol": row[1],
1283
- "side": _normalize_side(row[2]),
1284
- "quantity": row[3],
1285
- "price": row[4],
1286
- "commission": row[5] or 0.0,
1287
- "order_id": row[6],
1288
- }
1289
- for row in rows
1290
- ]
1291
- by_symbol: dict[str, list] = {}
1292
- for f in fills:
1293
- by_symbol.setdefault(f["symbol"], []).append(f)
1294
- positions = []
1295
- for symbol, symbol_fills in by_symbol.items():
1296
- symbol_fills.sort(key=lambda x: x["ts_event"])
1297
- position = 0.0
1298
- position_fills: list = []
1299
- position_id = 0
1300
- for fill in symbol_fills:
1301
- qty = fill["quantity"]
1302
- signed_qty = qty if fill["side"] == "BUY" else -qty
1303
- new_position = position + signed_qty
1304
- position_fills.append(fill)
1305
- if new_position == 0.0 and position != 0.0:
1306
- position_id += 1
1307
- pnl = 0.0
1308
- total_commission = 0.0
1309
- for pf in position_fills:
1310
- value = pf["price"] * pf["quantity"]
1311
- commission = pf.get("commission") or 0.0
1312
- total_commission += commission
1313
- if pf["side"] == "BUY":
1314
- pnl -= value
1315
- else:
1316
- pnl += value
1317
- pnl -= total_commission
1318
- positions.append(
1319
- {
1320
- "position_id": position_id,
1321
- "symbol": symbol,
1322
- "fills": list(position_fills),
1323
- "first_fill_time": position_fills[0]["ts_event"],
1324
- "last_fill_time": position_fills[-1]["ts_event"],
1325
- "pnl": pnl,
1326
- }
1327
- )
1328
- position_fills = []
1329
- position = new_position
1330
- positions.sort(key=lambda x: x["first_fill_time"])
1331
- return positions
1332
-
1333
-
1334
- def _format_indicator_name(raw_name: str) -> str:
1335
- name_with_spaces = raw_name.replace("_", " ")
1336
- words = name_with_spaces.split()
1337
- formatted_words = []
1338
- for word in words:
1339
- if word.isupper():
1340
- formatted_words.append(word)
1341
- elif word.islower():
1342
- formatted_words.append(word.capitalize())
1343
- else:
1344
- formatted_words.append(word)
1345
- return " ".join(formatted_words)
1346
-
1347
-
1348
- def _generate_position_chart(
1349
- output_path: pathlib.Path,
1350
- data: pd.DataFrame,
1351
- target_pos: dict,
1352
- symbol: str,
1353
- position_id: int,
1354
- highlight_start: int,
1355
- highlight_end: int,
1356
- ) -> None:
1357
- groups = _subplot_groups_from_data(data)
1358
- n = 2 + len(groups)
1359
- ratios = [1, 4] + [1] * len(groups)
1360
- fig, axes = plt.subplots(
1361
- n, 1, figsize=(16, 12), sharex=True, gridspec_kw={"height_ratios": ratios}
1362
- )
1363
- axes = [axes] if n == 1 else list(axes)
1364
-
1365
- ax_pnl = axes[0]
1366
- ax_main = axes[1]
1367
-
1368
- _plot_pnl_tracking(ax_pnl, data, target_pos, highlight_start, highlight_end)
1369
- _plot_price_data(ax_main, data, highlight_start, highlight_end)
1370
- _plot_main_indicators(ax_main, data)
1371
- _plot_trade_markers(ax_main, target_pos)
1372
-
1373
- for i, (_, ind_cols) in enumerate(sorted(groups.items())):
1374
- ax_sub = axes[i + 2]
1375
- for j, (col, display_name) in enumerate(ind_cols):
1376
- if col in data.columns:
1377
- ax_sub.plot(
1378
- data["ts_event"],
1379
- data[col],
1380
- label=display_name,
1381
- linewidth=1.5,
1382
- alpha=0.8,
1383
- color=_SUBPLOT_COLORS[j % len(_SUBPLOT_COLORS)],
1384
- )
1385
- ax_sub.legend(loc="upper left", fontsize=8)
1386
- ax_sub.grid(True, alpha=0.3)
1387
-
1388
- label = (
1389
- "WIN"
1390
- if target_pos["pnl"] > 0
1391
- else "LOSS"
1392
- if target_pos["pnl"] < 0
1393
- else "BREAK-EVEN"
1394
- )
1395
- duration_secs = (
1396
- target_pos["last_fill_time"] - target_pos["first_fill_time"]
1397
- ).total_seconds()
1398
- duration_mins = duration_secs / 60
1399
- ax_pnl.set_title(
1400
- f"Position #{position_id} - {symbol} - {label} - P&L: ${target_pos['pnl']:.2f} - Duration: {duration_mins:.0f}min",
1401
- fontsize=14,
1402
- fontweight="bold",
1403
- )
1404
-
1405
- total_seconds = (
1406
- data["ts_event"].iloc[-1] - data["ts_event"].iloc[0]
1407
- ).total_seconds()
1408
- hours = total_seconds / 3600
1409
- days = total_seconds / 86400
1410
-
1411
- for a in axes:
1412
- if days > 30:
1413
- a.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d"))
1414
- a.xaxis.set_major_locator(mdates.WeekdayLocator(interval=1))
1415
- elif days > 7:
1416
- a.xaxis.set_major_formatter(mdates.DateFormatter("%m/%d"))
1417
- a.xaxis.set_major_locator(mdates.DayLocator(interval=2))
1418
- elif days > 2:
1419
- a.xaxis.set_major_formatter(mdates.DateFormatter("%m/%d"))
1420
- a.xaxis.set_major_locator(mdates.DayLocator(interval=1))
1421
- elif hours > 24:
1422
- a.xaxis.set_major_formatter(mdates.DateFormatter("%m/%d %H:%M"))
1423
- a.xaxis.set_major_locator(mdates.HourLocator(interval=6))
1424
- elif hours > 8:
1425
- a.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
1426
- a.xaxis.set_major_locator(mdates.HourLocator(interval=2))
1427
- elif hours > 2:
1428
- a.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
1429
- a.xaxis.set_major_locator(mdates.HourLocator(interval=1))
1430
- elif hours > 0.5:
1431
- a.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
1432
- a.xaxis.set_major_locator(mdates.MinuteLocator(interval=15))
1433
- else:
1434
- a.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M:%S"))
1435
- a.xaxis.set_major_locator(mdates.MinuteLocator(interval=5))
1436
-
1437
- plt.xticks(rotation=45, fontsize=9)
1438
- plt.tight_layout()
1439
- plt.savefig(output_path, dpi=300, bbox_inches="tight")
1440
- plt.close(fig)
1441
-
1442
-
1443
- def _plot_pnl_tracking(
1444
- ax, data: pd.DataFrame, target_pos: dict, highlight_start: int, highlight_end: int
1445
- ) -> None:
1446
- pnl_series = []
1447
- max_pnl = 0
1448
- drawdown_series = []
1449
-
1450
- entry_price = target_pos["fills"][0]["price"] if target_pos["fills"] else 0
1451
- direction = target_pos["fills"][0]["side"] if target_pos["fills"] else "BUY"
1452
- in_position = False
1453
-
1454
- for i in range(len(data)):
1455
- ts = data["ts_event"].iloc[i]
1456
- close = data["close"].iloc[i]
1457
-
1458
- if ts >= target_pos["first_fill_time"] and ts <= target_pos["last_fill_time"]:
1459
- in_position = True
1460
- if direction == "BUY":
1461
- pnl = close - entry_price
1462
- else:
1463
- pnl = entry_price - close
1464
- else:
1465
- in_position = False
1466
- pnl = 0
1467
-
1468
- if in_position:
1469
- max_pnl = max(max_pnl, pnl)
1470
- drawdown = pnl - max_pnl
1471
- else:
1472
- drawdown = 0
1473
-
1474
- pnl_series.append(pnl)
1475
- drawdown_series.append(drawdown)
1476
-
1477
- ax.plot(
1478
- data["ts_event"],
1479
- pnl_series,
1480
- color="blue",
1481
- linewidth=2,
1482
- label="Unrealized P&L",
1483
- alpha=0.8,
1484
- )
1485
- ax.fill_between(
1486
- data["ts_event"], drawdown_series, 0, color="red", alpha=0.3, label="Drawdown"
1487
- )
1488
- ax.axhline(y=0, color="black", linestyle="-", alpha=0.5, linewidth=0.8)
1489
-
1490
- if 0 <= highlight_start < len(data) and 0 <= highlight_end < len(data):
1491
- start_time = mdates.date2num(data["ts_event"].iloc[highlight_start])
1492
- end_time = mdates.date2num(data["ts_event"].iloc[highlight_end])
1493
- y_min, y_max = ax.get_ylim()
1494
- rect = Rectangle(
1495
- (start_time, y_min),
1496
- end_time - start_time,
1497
- y_max - y_min,
1498
- facecolor="lightblue",
1499
- alpha=0.2,
1500
- )
1501
- ax.add_patch(rect)
1502
-
1503
- ax.legend(loc="upper left", fontsize=8)
1504
- ax.grid(True, alpha=0.3)
1505
- ax.set_ylabel("P&L ($)", fontsize=10)
1506
-
1507
-
1508
- def _plot_price_data(
1509
- ax, data: pd.DataFrame, highlight_start: int, highlight_end: int
1510
- ) -> None:
1511
- for i in range(len(data)):
1512
- date = data["ts_event"].iloc[i]
1513
- ax.plot(
1514
- [date, date],
1515
- [data["low"].iloc[i], data["high"].iloc[i]],
1516
- color="black",
1517
- linewidth=0.8,
1518
- alpha=0.7,
1519
- )
1520
- ax.plot([date], [data["close"].iloc[i]], marker="_", color="blue", markersize=3)
1521
-
1522
- if 0 <= highlight_start < len(data) and 0 <= highlight_end < len(data):
1523
- start_time = mdates.date2num(data["ts_event"].iloc[highlight_start])
1524
- end_time = mdates.date2num(data["ts_event"].iloc[highlight_end])
1525
- y_min, y_max = ax.get_ylim()
1526
- rect = Rectangle(
1527
- (start_time, y_min),
1528
- end_time - start_time,
1529
- y_max - y_min,
1530
- facecolor="lightblue",
1531
- alpha=0.2,
1532
- label="Position Period",
1533
- )
1534
- ax.add_patch(rect)
1535
-
1536
- ax.set_ylabel("Price", fontsize=10)
1537
- ax.grid(True, alpha=0.3)
1538
-
1539
-
1540
- def _plot_main_indicators(ax, data: pd.DataFrame) -> None:
1541
- main_indicators = _main_indicator_columns(data)
1542
- for i, (col, display_name) in enumerate(main_indicators):
1543
- if col in data.columns:
1544
- ax.plot(
1545
- data["ts_event"],
1546
- data[col],
1547
- label=display_name,
1548
- linewidth=1.5,
1549
- alpha=0.8,
1550
- color=_MAIN_COLORS[i % len(_MAIN_COLORS)],
1551
- )
1552
- if main_indicators:
1553
- ax.legend(loc="upper right", fontsize=8)
1554
-
1555
-
1556
- def _plot_trade_markers(ax, target_pos: dict) -> None:
1557
- for fill in target_pos["fills"]:
1558
- marker = "^" if fill["side"] == "BUY" else "v"
1559
- color = "green" if fill["side"] == "BUY" else "red"
1560
- qty = fill.get("quantity", 1)
1561
- base_size = 120
1562
- quantity_multiplier = min(3.0, max(0.5, qty))
1563
- size = base_size * quantity_multiplier
1564
-
1565
- ax.scatter(
1566
- fill["ts_event"],
1567
- fill["price"],
1568
- marker=marker,
1569
- color=color,
1570
- s=size,
1571
- edgecolors="black",
1572
- linewidth=1,
1573
- zorder=5,
1574
- alpha=0.8,
1575
- )
1576
-
1577
- y_lim = ax.get_ylim()
1578
- offset_y = (y_lim[1] - y_lim[0]) * 0.02
1579
- ax.annotate(
1580
- f"{qty}",
1581
- (fill["ts_event"], fill["price"] + offset_y),
1582
- ha="center",
1583
- va="bottom",
1584
- fontsize=8,
1585
- fontweight="bold",
1586
- bbox=dict(boxstyle="round,pad=0.2", facecolor="white", alpha=0.7),
1587
- )
1588
-
1589
-
1590
- @app.get("/api/run/{run_id}/positions/{symbol}/{position_id}/chart")
1591
- async def get_position_chart(run_id: str, symbol: str, position_id: int):
1592
- from fastapi import HTTPException
1593
-
1594
- chart_dir = CHARTS_DIR / run_id
1595
- chart_path = chart_dir / f"{symbol}_{position_id}.png"
1596
-
1597
- if chart_path.exists():
1598
- return FileResponse(chart_path, media_type="image/png")
1599
-
1600
- bars_df = _load_bars_for_chart(run_id, symbol)
1601
- if bars_df is None or bars_df.empty:
1602
- raise HTTPException(status_code=404, detail="No bar data found for this symbol")
1603
-
1604
- all_positions = _extract_positions_for_chart(run_id)
1605
- target_pos = None
1606
- for p in all_positions:
1607
- if p["symbol"] == symbol and p["position_id"] == position_id:
1608
- target_pos = p
1609
- break
1610
-
1611
- if target_pos is None:
1612
- raise HTTPException(
1613
- status_code=404, detail=f"Position {position_id} not found for {symbol}"
1614
- )
1615
-
1616
- bars_before, bars_after = 100, 100
1617
- mask_start = bars_df["ts_event"] >= target_pos["first_fill_time"]
1618
- mask_end = bars_df["ts_event"] <= target_pos["last_fill_time"]
1619
- if not mask_start.any() or not mask_end.any():
1620
- raise HTTPException(
1621
- status_code=404, detail="Position time range not found in bar data"
1622
- )
1623
-
1624
- start_idx = int(bars_df.index.get_loc(bars_df[mask_start].index[0])) # type: ignore[arg-type]
1625
- end_idx = int(bars_df.index.get_loc(bars_df[mask_end].index[-1])) # type: ignore[arg-type]
1626
- chart_start = max(0, start_idx - bars_before)
1627
- chart_end = min(len(bars_df) - 1, end_idx + bars_after)
1628
- data = bars_df.iloc[chart_start : chart_end + 1].copy()
1629
- highlight_start = start_idx - chart_start
1630
- highlight_end = end_idx - chart_start
1631
-
1632
- chart_dir.mkdir(parents=True, exist_ok=True)
1633
- _generate_position_chart(
1634
- chart_path,
1635
- data,
1636
- target_pos,
1637
- symbol,
1638
- position_id,
1639
- highlight_start,
1640
- highlight_end,
1641
- )
1642
-
1643
- return FileResponse(chart_path, media_type="image/png")
1644
-
1645
-
1646
- @app.get("/health")
1647
- async def health():
1648
- return {"status": "ok"}
1649
-
1650
-
1651
- @app.get("/api/strategies")
1652
- async def list_strategies():
1653
- strategies = []
1654
- for class_name, cls in registry.get_strategies().items():
1655
- display_name = getattr(cls, "name", "") or class_name
1656
- strategies.append({"class_name": class_name, "display_name": display_name})
1657
- return {"strategies": strategies}
1658
-
1659
-
1660
- @app.get("/api/brokers")
1661
- async def list_brokers():
1662
- return {"brokers": list(registry.get_brokers().keys())}
1663
-
1664
-
1665
- @app.get("/api/datafeeds")
1666
- async def list_datafeeds():
1667
- return {"datafeeds": list(registry.get_datafeeds().keys())}
1668
-
1669
-
1670
- @app.get("/api/strategies/{name}")
1671
- async def get_strategy(name: str):
1672
- schema = registry.get_strategy_schema(name)
1673
- if schema is None:
1674
- return {"error": "Strategy not found"}
1675
- return schema
1676
-
1677
-
1678
- @app.get("/api/brokers/{name}")
1679
- async def get_broker(name: str):
1680
- schema = registry.get_broker_schema(name)
1681
- if schema is None:
1682
- return {"error": "Broker not found"}
1683
- return schema
1684
-
1685
-
1686
- @app.get("/api/datafeeds/{name}")
1687
- async def get_datafeed(name: str):
1688
- schema = registry.get_datafeed_schema(name)
1689
- if schema is None:
1690
- return {"error": "Datafeed not found"}
1691
- return schema
1692
-
1693
-
1694
- @app.get("/api/secmaster/symbols")
1695
- async def get_symbols(bar_period: str | None = None):
1696
- return {"symbols": _get_available_symbols(bar_period)}
1697
-
1698
-
1699
- @app.get("/api/secmaster/symbols_coverage")
1700
- async def get_symbols_coverage(bar_period: str):
1701
- return {"symbols": _get_symbols_with_coverage(bar_period)}
1702
-
1703
-
1704
- @app.get("/api/secmaster/bar_periods")
1705
- async def get_bar_periods():
1706
- return {"bar_periods": _get_available_bar_periods()}
1707
-
1708
-
1709
- @app.get("/api/presets")
1710
- async def list_presets():
1711
- db_path = _get_secmaster_path()
1712
- if not os.path.exists(db_path):
1713
- return {"presets": []}
1714
- conn = sqlite3.connect(db_path)
1715
- cursor = conn.cursor()
1716
- cursor.execute("SELECT name FROM symbol_presets ORDER BY name")
1717
- presets = [row[0] for row in cursor.fetchall()]
1718
- conn.close()
1719
- return {"presets": presets}
1720
-
1721
-
1722
- @app.get("/api/presets/{name}")
1723
- async def get_preset(name: str):
1724
- import json
1725
-
1726
- db_path = _get_secmaster_path()
1727
- if not os.path.exists(db_path):
1728
- return {"error": "Preset not found"}
1729
- conn = sqlite3.connect(db_path)
1730
- cursor = conn.cursor()
1731
- cursor.execute("SELECT symbols FROM symbol_presets WHERE name = ?", (name,))
1732
- row = cursor.fetchone()
1733
- conn.close()
1734
- if row is None:
1735
- return {"error": "Preset not found"}
1736
- return {"name": name, "symbols": json.loads(row[0])}
1737
-
1738
-
1739
- class PresetRequest(BaseModel):
1740
- name: str
1741
- symbols: list[str]
1742
-
1743
-
1744
- @app.post("/api/presets")
1745
- async def create_preset(request: PresetRequest):
1746
- import json
1747
-
1748
- db_path = _get_secmaster_path()
1749
- conn = sqlite3.connect(db_path)
1750
- cursor = conn.cursor()
1751
- cursor.execute(
1752
- "INSERT INTO symbol_presets (name, symbols) VALUES (?, ?)",
1753
- (request.name, json.dumps(request.symbols)),
1754
- )
1755
- conn.commit()
1756
- conn.close()
1757
- return {"status": "created", "name": request.name}
1758
-
1759
-
1760
- @app.put("/api/presets/{name}")
1761
- async def update_preset(name: str, request: PresetRequest):
1762
- import json
1763
-
1764
- db_path = _get_secmaster_path()
1765
- conn = sqlite3.connect(db_path)
1766
- cursor = conn.cursor()
1767
- cursor.execute(
1768
- "UPDATE symbol_presets SET symbols = ? WHERE name = ?",
1769
- (json.dumps(request.symbols), name),
1770
- )
1771
- conn.commit()
1772
- conn.close()
1773
- return {"status": "updated", "name": name}
1774
-
1775
-
1776
- @app.delete("/api/presets/{name}")
1777
- async def delete_preset(name: str):
1778
- db_path = _get_secmaster_path()
1779
- conn = sqlite3.connect(db_path)
1780
- cursor = conn.cursor()
1781
- cursor.execute("DELETE FROM symbol_presets WHERE name = ?", (name,))
1782
- conn.commit()
1783
- conn.close()
1784
- return {"status": "deleted", "name": name}
1785
-
1786
-
1787
- class BacktestRequest(BaseModel):
1788
- strategy: str
1789
- strategy_params: dict
1790
- symbols: list[str]
1791
- start_date: str | None = None
1792
- end_date: str | None = None
1793
-
1794
-
1795
- def _run_backtest(request: BacktestRequest, run_id: str) -> None:
1796
- import pandas as pd
1797
-
1798
- try:
1799
- _running_jobs[run_id] = "running"
1800
-
1801
- strategy_cls = registry.get_strategies().get(request.strategy)
1802
- if not strategy_cls:
1803
- _running_jobs[run_id] = "error: invalid strategy"
1804
- return
1805
-
1806
- strategy_params = _deserialize_params(
1807
- request.strategy_params, getattr(strategy_cls, "parameters", {})
1808
- )
1809
-
1810
- updated_parameters = {}
1811
- for name, spec in strategy_cls.parameters.items():
1812
- if name in strategy_params:
1813
- updated_parameters[name] = type(spec)(
1814
- default=strategy_params[name],
1815
- **{k: v for k, v in spec.__dict__.items() if k != "default"},
1816
- )
1817
- else:
1818
- updated_parameters[name] = spec
1819
-
1820
- configured_strategy = type(
1821
- f"Configured{request.strategy}",
1822
- (strategy_cls,),
1823
- {"symbols": request.symbols, "parameters": updated_parameters},
1824
- )
1825
-
1826
- datafeed_attrs = {}
1827
- if request.start_date:
1828
- datafeed_attrs["start_ts"] = int(
1829
- pd.Timestamp(request.start_date, tz="UTC").value
1830
- )
1831
- if request.end_date:
1832
- end_dt = (
1833
- pd.Timestamp(request.end_date, tz="UTC")
1834
- + pd.Timedelta(days=1)
1835
- - pd.Timedelta(1, unit="ns")
1836
- )
1837
- datafeed_attrs["end_ts"] = int(end_dt.value)
1838
-
1839
- configured_datafeed = type(
1840
- "ConfiguredSimulatedDatafeed",
1841
- (SimulatedDatafeed,),
1842
- datafeed_attrs,
1843
- )
1844
-
1845
- configured_orchestrator = type(
1846
- "ConfiguredOrchestrator",
1847
- (Orchestrator,),
1848
- {"mode": "backtest"},
1849
- )
1850
-
1851
- orchestrator = configured_orchestrator(
1852
- strategies=[configured_strategy],
1853
- broker=SimulatedBroker,
1854
- datafeed=configured_datafeed,
1855
- )
1856
-
1857
- orchestrator.run()
1858
- _running_jobs[run_id] = "completed"
1859
- except Exception as e:
1860
- _running_jobs[run_id] = f"error: {e}"
1861
-
1862
-
1863
- def _deserialize_params(params: dict, param_specs: dict) -> dict:
1864
- result = {}
1865
- for name, value in params.items():
1866
- spec = param_specs.get(name)
1867
- if spec is None:
1868
- result[name] = value
1869
- continue
1870
- if isinstance(spec.default, enum.Enum):
1871
- enum_cls = type(spec.default)
1872
- result[name] = enum_cls[value]
1873
- else:
1874
- result[name] = value
1875
- return result
1876
-
1877
-
1878
- @app.post("/api/backtest/run")
1879
- async def run_backtest(request: BacktestRequest, background_tasks: BackgroundTasks):
1880
- import uuid
1881
-
1882
- run_id = str(uuid.uuid4())[:8]
1883
- background_tasks.add_task(_run_backtest, request, run_id)
1884
- return {"run_id": run_id, "status": "started"}
1885
-
1886
-
1887
- @app.get("/api/backtest/status/{run_id}")
1888
- async def backtest_status(run_id: str):
1889
- status = _running_jobs.get(run_id, "not found")
1890
- return {"run_id": run_id, "status": status}
1891
-
1892
-
1893
- BACKTEST_FORM_STYLE = """
1894
- .form-group { margin-bottom: 16px; }
1895
- .form-group label { display: block; margin-bottom: 6px; font-size: 14px; color: #8b949e; }
1896
- .form-group select, .form-group input {
1897
- width: 100%; padding: 8px 12px; background: #0d1117; border: 1px solid #30363d;
1898
- border-radius: 6px; color: #e6edf3; font-size: 14px;
1899
- }
1900
- .form-group select:focus, .form-group input:focus { outline: none; border-color: #58a6ff; }
1901
- .params-container { margin-top: 12px; padding: 12px; background: #0d1117; border-radius: 6px; }
1902
- .param-row { display: flex; gap: 12px; margin-bottom: 8px; align-items: center; }
1903
- .param-row label { min-width: 120px; font-size: 13px; }
1904
- .param-row input, .param-row select { flex: 1; }
1905
- .btn { padding: 10px 20px; background: #238636; border: none; border-radius: 6px;
1906
- color: #fff; font-size: 14px; cursor: pointer; }
1907
- .btn:hover { background: #2ea043; }
1908
- .btn:disabled { background: #30363d; cursor: not-allowed; }
1909
- .btn-sm { padding: 6px 12px; font-size: 12px; }
1910
- .btn-secondary { background: #30363d; }
1911
- .btn-secondary:hover { background: #484f58; }
1912
- .btn-danger { background: #da3633; }
1913
- .btn-danger:hover { background: #f85149; }
1914
- #status { margin-top: 16px; padding: 12px; border-radius: 6px; display: none; }
1915
- .status-running { background: #9e6a03; }
1916
- .status-completed { background: #238636; }
1917
- .status-error { background: #da3633; }
1918
- .date-row { display: flex; gap: 12px; }
1919
- .date-row .form-group { flex: 1; margin-bottom: 0; }
1920
- .symbol-section { background: #0d1117; border-radius: 6px; padding: 12px; }
1921
- .preset-row { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
1922
- .preset-row select { flex: 1; }
1923
- .preset-row input { flex: 1; }
1924
- .search-row { display: flex; gap: 8px; margin-bottom: 8px; }
1925
- .search-row input { flex: 1; }
1926
- .search-results { max-height: 150px; overflow-y: auto; border: 1px solid #30363d; border-radius: 4px; margin-bottom: 12px; }
1927
- .search-results:empty { display: none; }
1928
- .search-result { display: flex; justify-content: space-between; align-items: center; padding: 6px 10px; border-bottom: 1px solid #21262d; }
1929
- .search-result:last-child { border-bottom: none; }
1930
- .search-result:hover { background: #161b22; }
1931
- .search-result .symbol { font-family: monospace; }
1932
- .selected-symbols { display: flex; flex-wrap: wrap; gap: 6px; min-height: 32px; max-height: 150px; overflow-y: auto; }
1933
- .selected-tag { display: inline-flex; align-items: center; gap: 4px; background: #238636; color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-family: monospace; }
1934
- .selected-tag .remove { cursor: pointer; opacity: 0.7; }
1935
- .selected-tag .remove:hover { opacity: 1; }
1936
- .selected-label { font-size: 13px; color: #8b949e; margin-bottom: 6px; }
1937
- .section-header { font-size: 14px; color: #8b949e; margin-bottom: 6px; margin-top: 8px; }
1938
- """
1939
-
1940
- BACKTEST_FORM_SCRIPT = """
1941
- let strategyParams = [];
1942
- let symbolCoverage = {};
1943
- let allSymbols = [];
1944
- let selectedSymbols = [];
1945
- let presets = [];
1946
- let barPeriods = [];
1947
- let currentBarPeriod = '';
1948
- let globalMinTs = null;
1949
- let globalMaxTs = null;
1950
- let globalMinDate = null;
1951
- let globalMaxDate = null;
1952
-
1953
- async function loadStrategies() {
1954
- const res = await fetch('/api/strategies');
1955
- const data = await res.json();
1956
- const sel = document.getElementById('strategy');
1957
- data.strategies.forEach(s => sel.innerHTML += `<option value="${s.class_name}">${s.display_name}</option>`);
1958
- if (data.strategies.length > 0) { sel.value = data.strategies[0].class_name; loadStrategyParams(); }
1959
- }
1960
-
1961
- async function loadBarPeriods() {
1962
- const res = await fetch('/api/secmaster/bar_periods');
1963
- const data = await res.json();
1964
- barPeriods = data.bar_periods || [];
1965
- const sel = document.getElementById('bar-period');
1966
- sel.innerHTML = '';
1967
- barPeriods.forEach(p => sel.innerHTML += `<option value="${p}">${p}</option>`);
1968
- if (barPeriods.length > 0) {
1969
- currentBarPeriod = barPeriods[0];
1970
- sel.value = currentBarPeriod;
1971
- await loadSymbolsForPeriod();
1972
- }
1973
- }
1974
-
1975
- async function onBarPeriodChange() {
1976
- currentBarPeriod = document.getElementById('bar-period').value;
1977
- selectedSymbols = [];
1978
- renderSelectedSymbols();
1979
- await loadSymbolsForPeriod();
1980
- updateDateRange();
1981
- }
1982
-
1983
- async function loadSymbolsForPeriod() {
1984
- const res = await fetch(`/api/secmaster/symbols_coverage?bar_period=${currentBarPeriod}`);
1985
- const data = await res.json();
1986
- symbolCoverage = {};
1987
- allSymbols = [];
1988
- (data.symbols || []).forEach(s => {
1989
- symbolCoverage[s.symbol] = {min_ts: s.min_ts, max_ts: s.max_ts};
1990
- allSymbols.push(s.symbol);
1991
- });
1992
- }
1993
-
1994
- async function loadPresets() {
1995
- const res = await fetch('/api/presets');
1996
- const data = await res.json();
1997
- presets = data.presets || [];
1998
- renderPresetSelect();
1999
- }
2000
-
2001
- function renderPresetSelect() {
2002
- const sel = document.getElementById('preset-select');
2003
- sel.innerHTML = '<option value="">-- Select Preset --</option>';
2004
- presets.forEach(p => sel.innerHTML += `<option value="${p}">${p}</option>`);
2005
- }
2006
-
2007
- async function loadPreset() {
2008
- const name = document.getElementById('preset-select').value;
2009
- if (!name) return;
2010
- const res = await fetch(`/api/presets/${encodeURIComponent(name)}`);
2011
- const data = await res.json();
2012
- if (data.symbols) {
2013
- selectedSymbols = data.symbols.filter(s => allSymbols.includes(s));
2014
- renderSelectedSymbols();
2015
- updateDateRange();
2016
- }
2017
- }
2018
-
2019
- async function savePreset() {
2020
- const nameInput = document.getElementById('preset-name');
2021
- const name = nameInput.value.trim();
2022
- if (!name) { alert('Enter a preset name'); return; }
2023
- if (selectedSymbols.length === 0) { alert('Select at least one symbol'); return; }
2024
- const exists = presets.includes(name);
2025
- const method = exists ? 'PUT' : 'POST';
2026
- const url = exists ? `/api/presets/${encodeURIComponent(name)}` : '/api/presets';
2027
- await fetch(url, {
2028
- method, headers: {'Content-Type': 'application/json'},
2029
- body: JSON.stringify({name, symbols: selectedSymbols})
2030
- });
2031
- nameInput.value = '';
2032
- await loadPresets();
2033
- document.getElementById('preset-select').value = name;
2034
- }
2035
-
2036
- async function deletePreset() {
2037
- const name = document.getElementById('preset-select').value;
2038
- if (!name) { alert('Select a preset to delete'); return; }
2039
- if (!confirm(`Delete preset "${name}"?`)) return;
2040
- await fetch(`/api/presets/${encodeURIComponent(name)}`, {method: 'DELETE'});
2041
- await loadPresets();
2042
- selectedSymbols = [];
2043
- renderSelectedSymbols();
2044
- updateDateRange();
2045
- }
2046
-
2047
- function searchSymbols() {
2048
- const query = document.getElementById('symbol-search').value.toUpperCase();
2049
- const container = document.getElementById('search-results');
2050
- if (query.length < 1) { container.innerHTML = ''; return; }
2051
- const matches = allSymbols.filter(s => s.toUpperCase().includes(query) && !selectedSymbols.includes(s)).slice(0, 20);
2052
- container.innerHTML = matches.map(s => `<div class="search-result"><span class="symbol">${s}</span><button class="btn btn-sm" onclick="addSymbol('${s}')">+</button></div>`).join('');
2053
- }
2054
-
2055
- function addSymbol(symbol) {
2056
- if (!selectedSymbols.includes(symbol)) {
2057
- selectedSymbols.push(symbol);
2058
- selectedSymbols.sort();
2059
- renderSelectedSymbols();
2060
- searchSymbols();
2061
- updateDateRange();
2062
- }
2063
- }
2064
-
2065
- function removeSymbol(symbol) {
2066
- selectedSymbols = selectedSymbols.filter(s => s !== symbol);
2067
- renderSelectedSymbols();
2068
- searchSymbols();
2069
- updateDateRange();
2070
- }
2071
-
2072
- function renderSelectedSymbols() {
2073
- const container = document.getElementById('selected-symbols');
2074
- const label = document.getElementById('selected-label');
2075
- label.textContent = `Selected (${selectedSymbols.length}):`;
2076
- container.innerHTML = selectedSymbols.map(s => `<span class="selected-tag">${s}<span class="remove" onclick="removeSymbol('${s}')">&times;</span></span>`).join('');
2077
- }
2078
-
2079
- function updateDateRange() {
2080
- if (selectedSymbols.length === 0) {
2081
- globalMinTs = null;
2082
- globalMaxTs = null;
2083
- document.getElementById('start-date').value = '';
2084
- document.getElementById('end-date').value = '';
2085
- document.getElementById('range-slider-container').style.display = 'none';
2086
- return;
2087
- }
2088
- let minTs = Infinity, maxTs = -Infinity;
2089
- selectedSymbols.forEach(s => {
2090
- const cov = symbolCoverage[s];
2091
- if (cov) {
2092
- if (cov.min_ts < minTs) minTs = cov.min_ts;
2093
- if (cov.max_ts > maxTs) maxTs = cov.max_ts;
2094
- }
2095
- });
2096
- if (minTs === Infinity) return;
2097
- globalMinTs = minTs;
2098
- globalMaxTs = maxTs;
2099
- globalMinDate = tsToDate(minTs);
2100
- globalMaxDate = tsToDate(maxTs);
2101
- const startInput = document.getElementById('start-date');
2102
- const endInput = document.getElementById('end-date');
2103
- startInput.min = globalMinDate;
2104
- startInput.max = globalMaxDate;
2105
- startInput.value = globalMinDate;
2106
- endInput.min = globalMinDate;
2107
- endInput.max = globalMaxDate;
2108
- endInput.value = globalMaxDate;
2109
- }
2110
-
2111
- function tsToDate(ts) {
2112
- return new Date(ts / 1000000).toISOString().split('T')[0];
2113
- }
2114
-
2115
- function clampDate(inputId) {
2116
- if (!globalMinDate || !globalMaxDate) return;
2117
- const input = document.getElementById(inputId);
2118
- if (input.value < globalMinDate) input.value = globalMinDate;
2119
- if (input.value > globalMaxDate) input.value = globalMaxDate;
2120
- }
2121
-
2122
- async function loadStrategyParams() {
2123
- const name = document.getElementById('strategy').value;
2124
- const res = await fetch(`/api/strategies/${name}`);
2125
- const data = await res.json();
2126
- strategyParams = (data.parameters || []).filter(p => p.name !== 'bar_period');
2127
- renderParams('strategy-params', strategyParams, 'sp_');
2128
- }
2129
-
2130
- function formatParamName(name) {
2131
- return name.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
2132
- }
2133
-
2134
- function renderParams(containerId, params, prefix) {
2135
- const container = document.getElementById(containerId);
2136
- container.innerHTML = '';
2137
- params.forEach(p => {
2138
- const row = document.createElement('div');
2139
- row.className = 'param-row';
2140
- let input;
2141
- if (p.choices) {
2142
- input = `<select id="${prefix}${p.name}">` +
2143
- p.choices.map(c => `<option value="${c}" ${c===p.default?'selected':''}>${c}</option>`).join('') +
2144
- '</select>';
2145
- } else if (p.type === 'bool') {
2146
- input = `<select id="${prefix}${p.name}"><option value="true" ${p.default?'selected':''}>true</option><option value="false" ${!p.default?'selected':''}>false</option></select>`;
2147
- } else {
2148
- const attrs = [];
2149
- if (p.min !== undefined) attrs.push(`min="${p.min}"`);
2150
- if (p.max !== undefined) attrs.push(`max="${p.max}"`);
2151
- if (p.step !== undefined) attrs.push(`step="${p.step}"`);
2152
- input = `<input type="${p.type==='int'||p.type==='float'?'number':'text'}" id="${prefix}${p.name}" value="${p.default}" ${attrs.join(' ')}>`;
2153
- }
2154
- row.innerHTML = `<label>${formatParamName(p.name)}</label>${input}`;
2155
- container.appendChild(row);
2156
- });
2157
- }
2158
-
2159
- function collectParams(params, prefix) {
2160
- const result = {};
2161
- params.forEach(p => {
2162
- const el = document.getElementById(prefix + p.name);
2163
- let val = el.value;
2164
- if (p.type === 'int') val = parseInt(val);
2165
- else if (p.type === 'float') val = parseFloat(val);
2166
- else if (p.type === 'bool') val = val === 'true';
2167
- result[p.name] = val;
2168
- });
2169
- return result;
2170
- }
2171
-
2172
- async function runBacktest() {
2173
- const btn = document.getElementById('run-btn');
2174
- const status = document.getElementById('status');
2175
- if (selectedSymbols.length === 0) {
2176
- alert('Please select at least one symbol');
2177
- return;
2178
- }
2179
- btn.disabled = true;
2180
- status.style.display = 'block';
2181
- status.className = 'status-running';
2182
- status.textContent = 'Starting backtest...';
2183
-
2184
- const params = collectParams(strategyParams, 'sp_');
2185
- params.bar_period = currentBarPeriod;
2186
- const payload = {
2187
- strategy: document.getElementById('strategy').value,
2188
- strategy_params: params,
2189
- symbols: selectedSymbols,
2190
- start_date: document.getElementById('start-date').value || null,
2191
- end_date: document.getElementById('end-date').value || null
2192
- };
2193
-
2194
- const res = await fetch('/api/backtest/run', {
2195
- method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)
2196
- });
2197
- const data = await res.json();
2198
- const runId = data.run_id;
2199
- status.textContent = `Backtest ${runId} running...`;
2200
-
2201
- const poll = setInterval(async () => {
2202
- const r = await fetch(`/api/backtest/status/${runId}`);
2203
- const d = await r.json();
2204
- if (d.status === 'completed') {
2205
- clearInterval(poll);
2206
- status.className = 'status-completed';
2207
- status.textContent = `Backtest ${runId} completed!`;
2208
- btn.disabled = false;
2209
- } else if (d.status.startsWith('error')) {
2210
- clearInterval(poll);
2211
- status.className = 'status-error';
2212
- status.textContent = `Backtest ${runId}: ${d.status}`;
2213
- btn.disabled = false;
2214
- }
2215
- }, 1000);
2216
- }
2217
-
2218
- document.addEventListener('DOMContentLoaded', () => {
2219
- loadStrategies();
2220
- loadBarPeriods();
2221
- loadPresets();
2222
- });
2223
- """
2224
-
2225
-
2226
- @app.get("/backtest", response_class=HTMLResponse)
2227
- async def backtest_form():
2228
- sidebar = SIDEBAR_HTML.format(
2229
- runs_active="",
2230
- backtest_active="active",
2231
- secmaster_active="",
2232
- pipeline_active="",
2233
- pipeline_open="",
2234
- pipeline_sub1_active="",
2235
- pipeline_sub2_active="",
2236
- )
2237
- return f"""
2238
- <!DOCTYPE html>
2239
- <html>
2240
- <head>
2241
- <title>Backtest - OneSecondTrader</title>
2242
- <style>{BASE_STYLE}{BACKTEST_FORM_STYLE}</style>
2243
- </head>
2244
- <body>
2245
- {sidebar}
2246
- <main class="main-content">
2247
- <div class="container">
2248
- <div class="card">
2249
- <h2>Run Backtest</h2>
2250
- <div class="form-group">
2251
- <label>Strategy</label>
2252
- <select id="strategy" onchange="loadStrategyParams()"></select>
2253
- <div class="section-header">Parameters</div>
2254
- <div id="strategy-params" class="params-container"></div>
2255
- </div>
2256
- <div class="form-group">
2257
- <label>Bar Period</label>
2258
- <select id="bar-period" onchange="onBarPeriodChange()"></select>
2259
- </div>
2260
- <div class="form-group">
2261
- <label>Symbols</label>
2262
- <div class="symbol-section">
2263
- <div class="preset-row">
2264
- <select id="preset-select" onchange="loadPreset()"></select>
2265
- <input type="text" id="preset-name" placeholder="New preset name...">
2266
- <button class="btn btn-sm btn-secondary" onclick="savePreset()">Save</button>
2267
- <button class="btn btn-sm btn-danger" onclick="deletePreset()">Delete</button>
2268
- </div>
2269
- <div class="search-row">
2270
- <input type="text" id="symbol-search" placeholder="Search symbols..." oninput="searchSymbols()">
2271
- </div>
2272
- <div id="search-results" class="search-results"></div>
2273
- <div id="selected-label" class="selected-label">Selected (0):</div>
2274
- <div id="selected-symbols" class="selected-symbols"></div>
2275
- </div>
2276
- </div>
2277
- <div class="date-row">
2278
- <div class="form-group">
2279
- <label>Start Date</label>
2280
- <input type="date" id="start-date" onchange="clampDate('start-date')">
2281
- </div>
2282
- <div class="form-group">
2283
- <label>End Date</label>
2284
- <input type="date" id="end-date" onchange="clampDate('end-date')">
2285
- </div>
2286
- </div>
2287
- <div class="form-group" style="margin-top: 16px;">
2288
- <button id="run-btn" class="btn" onclick="runBacktest()">Run Backtest</button>
2289
- </div>
2290
- <div id="status"></div>
2291
- </div>
2292
- </div>
2293
- </main>
2294
- <script>{BACKTEST_FORM_SCRIPT}</script>
2295
- </body>
2296
- </html>
2297
- """
2298
-
2299
-
2300
- def _get_secmaster_summary() -> dict:
2301
- db_path = _get_secmaster_path()
2302
- if not os.path.exists(db_path):
2303
- return {"exists": False}
2304
-
2305
- conn = sqlite3.connect(db_path)
2306
- cursor = conn.cursor()
2307
-
2308
- cursor.execute("SELECT key, value FROM meta")
2309
- meta = {row[0]: row[1] for row in cursor.fetchall()}
2310
- conn.close()
2311
-
2312
- if not meta:
2313
- return {"exists": True, "db_path": db_path, "needs_reindex": True}
2314
-
2315
- rtypes_str = meta.get("ohlcv_schemas", "")
2316
- rtypes = [RTYPE_TO_BAR_PERIOD.get(int(r), r) for r in rtypes_str.split(",") if r]
2317
-
2318
- db_size_bytes = os.path.getsize(db_path)
2319
-
2320
- return {
2321
- "exists": True,
2322
- "db_path": db_path,
2323
- "db_size_mb": round(db_size_bytes / (1024 * 1024), 1),
2324
- "symbol_count": int(meta.get("symbol_count", 0)),
2325
- "ohlcv_record_count": int(meta.get("ohlcv_record_count", 0)),
2326
- "min_ts": int(meta.get("ohlcv_min_ts", 0)),
2327
- "max_ts": int(meta.get("ohlcv_max_ts", 0)),
2328
- "schemas": rtypes,
2329
- }
2330
-
2331
-
2332
- def _search_symbol(query: str) -> list[dict]:
2333
- db_path = _get_secmaster_path()
2334
- if not os.path.exists(db_path):
2335
- return []
2336
-
2337
- conn = sqlite3.connect(db_path)
2338
- cursor = conn.cursor()
2339
-
2340
- cursor.execute(
2341
- """
2342
- SELECT symbol, rtype, min_ts, max_ts, record_count
2343
- FROM symbol_coverage
2344
- WHERE symbol LIKE ?
2345
- ORDER BY symbol
2346
- LIMIT 100
2347
- """,
2348
- (f"%{query}%",),
2349
- )
2350
-
2351
- results = []
2352
- for row in cursor.fetchall():
2353
- results.append(
2354
- {
2355
- "symbol": row[0],
2356
- "min_ts": row[2],
2357
- "max_ts": row[3],
2358
- "record_count": row[4],
2359
- "schema": (
2360
- RTYPE_TO_BAR_PERIOD.get(row[1], str(row[1])) if row[1] else None
2361
- ),
2362
- }
2363
- )
2364
-
2365
- conn.close()
2366
- return results
2367
-
2368
-
2369
- @app.get("/api/secmaster/summary")
2370
- async def get_secmaster_summary():
2371
- return _get_secmaster_summary()
2372
-
2373
-
2374
- @app.get("/api/secmaster/search")
2375
- async def search_secmaster(q: str = ""):
2376
- if len(q) < 1:
2377
- return {"results": []}
2378
- return {"results": _search_symbol(q)}
2379
-
2380
-
2381
- SECMASTER_STYLE = """
2382
- .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
2383
- .stat-card { background: #0d1117; border-radius: 8px; padding: 20px; }
2384
- .stat-card .label { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; }
2385
- .stat-card .value { font-size: 28px; font-weight: 600; margin-top: 4px; }
2386
- .stat-card .sub { font-size: 12px; color: #8b949e; margin-top: 4px; }
2387
- .asset-table { width: 100%; border-collapse: collapse; }
2388
- .asset-table th, .asset-table td { padding: 12px; text-align: left; border-bottom: 1px solid #30363d; }
2389
- .asset-table th { font-size: 12px; color: #8b949e; text-transform: uppercase; font-weight: 500; }
2390
- .asset-table tr:hover { background: #21262d; }
2391
- .search-box { width: 100%; padding: 12px 16px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e6edf3; font-size: 14px; margin-bottom: 16px; }
2392
- .search-box:focus { outline: none; border-color: #58a6ff; }
2393
- .search-results { max-height: 400px; overflow-y: auto; }
2394
- .no-db { text-align: center; padding: 48px; color: #8b949e; }
2395
- .no-db code { background: #21262d; padding: 2px 6px; border-radius: 4px; }
2396
- .loading { display: flex; align-items: center; gap: 12px; color: #8b949e; padding: 24px; }
2397
- .spinner { width: 20px; height: 20px; border: 2px solid #30363d; border-top-color: #58a6ff; border-radius: 50%; animation: spin 0.8s linear infinite; }
2398
- @keyframes spin { to { transform: rotate(360deg); } }
2399
- """
2400
-
2401
- SECMASTER_SCRIPT = """
2402
- function showLoading(elementId, message) {
2403
- document.getElementById(elementId).innerHTML = `<div class="loading"><div class="spinner"></div><span>${message}</span></div>`;
2404
- }
2405
-
2406
- async function loadSummary() {
2407
- showLoading('content', 'Loading database summary...');
2408
- const res = await fetch('/api/secmaster/summary');
2409
- const data = await res.json();
2410
-
2411
- if (!data.exists) {
2412
- document.getElementById('content').innerHTML = `
2413
- <div class="no-db">
2414
- <p>No securities master database found.</p>
2415
- <p style="margin-top: 8px;">Set <code>SECMASTER_DB_PATH</code> environment variable or place <code>secmaster.db</code> in the working directory.</p>
2416
- </div>
2417
- `;
2418
- return;
2419
- }
2420
-
2421
- if (data.needs_reindex) {
2422
- document.getElementById('content').innerHTML = `
2423
- <div class="no-db">
2424
- <p>Database metadata not found.</p>
2425
- <p style="margin-top: 8px;">Run <code>from onesecondtrader.secmaster.utils import update_meta; update_meta(path)</code> to rebuild stats.</p>
2426
- </div>
2427
- `;
2428
- return;
2429
- }
2430
-
2431
- const minDate = new Date(data.min_ts / 1000000).toISOString().split('T')[0];
2432
- const maxDate = new Date(data.max_ts / 1000000).toISOString().split('T')[0];
2433
-
2434
- document.getElementById('content').innerHTML = `
2435
- <div class="stats-grid">
2436
- <div class="stat-card">
2437
- <div class="label">Symbols</div>
2438
- <div class="value">${data.symbol_count.toLocaleString()}</div>
2439
- </div>
2440
- <div class="stat-card">
2441
- <div class="label">OHLCV Records</div>
2442
- <div class="value">${data.ohlcv_record_count.toLocaleString()}</div>
2443
- </div>
2444
- <div class="stat-card">
2445
- <div class="label">Date Range</div>
2446
- <div class="value" style="font-size: 18px;">${minDate}</div>
2447
- <div class="sub">to ${maxDate}</div>
2448
- </div>
2449
- <div class="stat-card">
2450
- <div class="label">Schemas</div>
2451
- <div class="value" style="font-size: 18px;">${data.schemas.join(', ')}</div>
2452
- </div>
2453
- <div class="stat-card">
2454
- <div class="label">Database Size</div>
2455
- <div class="value">${data.db_size_mb} MB</div>
2456
- <div class="sub">${data.db_path}</div>
2457
- </div>
2458
- </div>
2459
- <div class="card">
2460
- <h2>Symbol Search</h2>
2461
- <input type="text" class="search-box" id="search-input" placeholder="Search symbols..." oninput="searchSymbols()">
2462
- <div id="search-results" class="search-results"></div>
2463
- </div>
2464
- `;
2465
- }
2466
-
2467
- let searchTimeout;
2468
- async function searchSymbols() {
2469
- const q = document.getElementById('search-input').value;
2470
- clearTimeout(searchTimeout);
2471
- if (q.length < 1) {
2472
- document.getElementById('search-results').innerHTML = '';
2473
- return;
2474
- }
2475
- showLoading('search-results', 'Searching...');
2476
- searchTimeout = setTimeout(async () => {
2477
- const res = await fetch('/api/secmaster/search?q=' + encodeURIComponent(q));
2478
- const data = await res.json();
2479
- if (data.results.length === 0) {
2480
- document.getElementById('search-results').innerHTML = '<p style="color: #8b949e;">No results found.</p>';
2481
- return;
2482
- }
2483
- let rows = data.results.map(r => {
2484
- const minDate = r.min_ts ? new Date(r.min_ts / 1000000).toISOString().split('T')[0] : '-';
2485
- const maxDate = r.max_ts ? new Date(r.max_ts / 1000000).toISOString().split('T')[0] : '-';
2486
- return `<tr>
2487
- <td>${r.symbol}</td>
2488
- <td>${r.schema || '-'}</td>
2489
- <td>${minDate} → ${maxDate}</td>
2490
- <td>${r.record_count ? r.record_count.toLocaleString() : '-'}</td>
2491
- </tr>`;
2492
- }).join('');
2493
- document.getElementById('search-results').innerHTML = `
2494
- <table class="asset-table">
2495
- <thead><tr><th>Symbol</th><th>Schema</th><th>Date Range</th><th>Records</th></tr></thead>
2496
- <tbody>${rows}</tbody>
2497
- </table>
2498
- `;
2499
- }, 300);
2500
- }
2501
-
2502
- document.addEventListener('DOMContentLoaded', loadSummary);
2503
- """
2504
-
2505
-
2506
- @app.get("/secmaster", response_class=HTMLResponse)
2507
- async def secmaster_page():
2508
- sidebar = SIDEBAR_HTML.format(
2509
- runs_active="",
2510
- backtest_active="",
2511
- secmaster_active="active",
2512
- pipeline_active="",
2513
- pipeline_open="",
2514
- pipeline_sub1_active="",
2515
- pipeline_sub2_active="",
2516
- )
2517
- return f"""
2518
- <!DOCTYPE html>
2519
- <html>
2520
- <head>
2521
- <title>Securities Master - OneSecondTrader</title>
2522
- <style>{BASE_STYLE}{SECMASTER_STYLE}</style>
2523
- </head>
2524
- <body>
2525
- {sidebar}
2526
- <main class="main-content">
2527
- <div class="container" id="content">
2528
- <p style="color: #8b949e;">Loading...</p>
2529
- </div>
2530
- </main>
2531
- <script>{SECMASTER_SCRIPT}</script>
2532
- </body>
2533
- </html>
2534
- """
2535
-
2536
-
2537
- @app.get("/api/signals/{run_id}")
2538
- async def get_signals_for_run(run_id: str):
2539
- db_path = _get_runs_db_path()
2540
- if not os.path.exists(db_path):
2541
- return {"actions": {}}
2542
- conn = sqlite3.connect(db_path)
2543
- cursor = conn.cursor()
2544
- cursor.execute(
2545
- """SELECT id, ts_event, order_id, symbol, order_type, side, quantity,
2546
- limit_price, stop_price, action, signal
2547
- FROM order_requests
2548
- WHERE run_id = ? AND request_type = 'submission'
2549
- ORDER BY ts_event""",
2550
- (run_id,),
2551
- )
2552
- requests = cursor.fetchall()
2553
- cursor.execute(
2554
- """SELECT order_id, response_type FROM order_responses WHERE run_id = ?""",
2555
- (run_id,),
2556
- )
2557
- responses = {row[0]: row[1] for row in cursor.fetchall()}
2558
- cursor.execute(
2559
- """SELECT order_id FROM fills WHERE run_id = ?""",
2560
- (run_id,),
2561
- )
2562
- filled_orders = {row[0] for row in cursor.fetchall()}
2563
- conn.close()
2564
-
2565
- actions: dict[str, dict[str, list]] = {}
2566
- for row in requests:
2567
- (
2568
- _,
2569
- ts_event,
2570
- order_id,
2571
- symbol,
2572
- order_type,
2573
- side,
2574
- quantity,
2575
- limit_price,
2576
- stop_price,
2577
- action,
2578
- signal,
2579
- ) = row
2580
- action = action or "UNKNOWN"
2581
- signal = signal or "unnamed"
2582
- if action not in actions:
2583
- actions[action] = {}
2584
- if signal not in actions[action]:
2585
- actions[action][signal] = []
2586
- if order_id in filled_orders:
2587
- status = "FILLED"
2588
- elif order_id in responses:
2589
- resp = responses[order_id]
2590
- if "accepted" in resp:
2591
- status = "ACCEPTED"
2592
- elif "rejected" in resp:
2593
- status = "REJECTED"
2594
- elif resp == "expired":
2595
- status = "EXPIRED"
2596
- elif "cancellation" in resp:
2597
- status = "CANCELLED"
2598
- else:
2599
- status = resp.upper()
2600
- else:
2601
- status = "PENDING"
2602
- actions[action][signal].append(
2603
- {
2604
- "order_id": order_id,
2605
- "ts_event": ts_event,
2606
- "symbol": symbol,
2607
- "order_type": order_type,
2608
- "side": side or "BUY",
2609
- "quantity": quantity,
2610
- "limit_price": limit_price,
2611
- "stop_price": stop_price,
2612
- "status": status,
2613
- }
2614
- )
2615
- action_types = [at.name for at in ActionType]
2616
- return {"actions": actions, "action_types": action_types}
2617
-
2618
-
2619
- @app.get("/api/signals/{run_id}/chart/{order_id}")
2620
- async def get_signal_chart(run_id: str, order_id: str):
2621
- from fastapi import HTTPException
2622
-
2623
- db_path = _get_runs_db_path()
2624
- if not os.path.exists(db_path):
2625
- raise HTTPException(status_code=404, detail="Database not found")
2626
-
2627
- conn = sqlite3.connect(db_path)
2628
- cursor = conn.cursor()
2629
- cursor.execute(
2630
- """SELECT ts_event, symbol, order_type, side, quantity, limit_price, stop_price, action, signal
2631
- FROM order_requests
2632
- WHERE run_id = ? AND order_id = ? AND request_type = 'submission'""",
2633
- (run_id, order_id),
2634
- )
2635
- req_row = cursor.fetchone()
2636
- if not req_row:
2637
- conn.close()
2638
- raise HTTPException(status_code=404, detail="Order not found")
2639
-
2640
- (
2641
- ts_event,
2642
- symbol,
2643
- order_type,
2644
- side,
2645
- quantity,
2646
- limit_price,
2647
- stop_price,
2648
- action,
2649
- signal,
2650
- ) = req_row
2651
- submission_time = pd.to_datetime(ts_event)
2652
-
2653
- cursor.execute(
2654
- """SELECT ts_event, response_type FROM order_responses
2655
- WHERE run_id = ? AND order_id = ?
2656
- ORDER BY ts_event""",
2657
- (run_id, order_id),
2658
- )
2659
- response_rows = cursor.fetchall()
2660
-
2661
- cursor.execute(
2662
- """SELECT ts_event, price, quantity FROM fills
2663
- WHERE run_id = ? AND order_id = ?
2664
- ORDER BY ts_event""",
2665
- (run_id, order_id),
2666
- )
2667
- fill_rows = cursor.fetchall()
2668
- conn.close()
2669
-
2670
- resolution_time = None
2671
- resolution_type = None
2672
- fill_price = None
2673
- if fill_rows:
2674
- resolution_time = pd.to_datetime(fill_rows[-1][0])
2675
- resolution_type = "FILLED"
2676
- fill_price = fill_rows[-1][1]
2677
- elif response_rows:
2678
- for ts, resp_type in response_rows:
2679
- if resp_type in ("expired", "cancellation_accepted", "submission_rejected"):
2680
- resolution_time = pd.to_datetime(ts)
2681
- resolution_type = resp_type.upper().replace("_", " ")
2682
- break
2683
-
2684
- bars_df = _load_bars_for_chart(run_id, symbol)
2685
- if bars_df is None or bars_df.empty:
2686
- raise HTTPException(status_code=404, detail="No bar data found for this symbol")
2687
-
2688
- bars_before, bars_after = 100, 100
2689
- mask_start = bars_df["ts_event"] >= submission_time
2690
- if not mask_start.any():
2691
- raise HTTPException(
2692
- status_code=404, detail="Submission time not found in bar data"
2693
- )
2694
-
2695
- loc_result = bars_df.index.get_loc(bars_df[mask_start].index[0])
2696
- start_idx = (
2697
- int(loc_result)
2698
- if isinstance(loc_result, int)
2699
- else int(loc_result.start)
2700
- if isinstance(loc_result, slice)
2701
- else int(loc_result.argmax())
2702
- )
2703
- if resolution_time:
2704
- mask_end = bars_df["ts_event"] <= resolution_time
2705
- if mask_end.any():
2706
- loc_result_end = bars_df.index.get_loc(bars_df[mask_end].index[-1])
2707
- end_idx = (
2708
- int(loc_result_end)
2709
- if isinstance(loc_result_end, int)
2710
- else int(loc_result_end.start)
2711
- if isinstance(loc_result_end, slice)
2712
- else int(loc_result_end.argmax())
2713
- )
2714
- else:
2715
- end_idx = start_idx
2716
- else:
2717
- end_idx = min(start_idx + 50, len(bars_df) - 1)
2718
-
2719
- chart_start = max(0, start_idx - bars_before)
2720
- chart_end = min(len(bars_df) - 1, end_idx + bars_after)
2721
- data = bars_df.iloc[chart_start : chart_end + 1].copy()
2722
- highlight_start = start_idx - chart_start
2723
- highlight_end = end_idx - chart_start
2724
-
2725
- chart_dir = CHARTS_DIR / run_id / "signals"
2726
- chart_dir.mkdir(parents=True, exist_ok=True)
2727
- chart_path = chart_dir / f"{order_id}.png"
2728
-
2729
- order_info = {
2730
- "submission_time": submission_time,
2731
- "resolution_time": resolution_time,
2732
- "resolution_type": resolution_type,
2733
- "side": side or "BUY",
2734
- "order_type": order_type,
2735
- "quantity": quantity,
2736
- "limit_price": limit_price,
2737
- "stop_price": stop_price,
2738
- "fill_price": fill_price,
2739
- "action": action,
2740
- "signal": signal,
2741
- }
2742
- _generate_signal_chart(
2743
- chart_path, data, order_info, symbol, highlight_start, highlight_end
2744
- )
2745
- return FileResponse(chart_path, media_type="image/png")
2746
-
2747
-
2748
- def _generate_signal_chart(
2749
- output_path: pathlib.Path,
2750
- data: pd.DataFrame,
2751
- order_info: dict,
2752
- symbol: str,
2753
- highlight_start: int,
2754
- highlight_end: int,
2755
- ) -> None:
2756
- groups = _subplot_groups_from_data(data)
2757
- n = 1 + len(groups)
2758
- ratios = [4] + [1] * len(groups)
2759
- fig, axes = plt.subplots(
2760
- n, 1, figsize=(16, 10), sharex=True, gridspec_kw={"height_ratios": ratios}
2761
- )
2762
- axes = [axes] if n == 1 else list(axes)
2763
- ax_main = axes[0]
2764
-
2765
- _plot_price_data(ax_main, data, highlight_start, highlight_end)
2766
- _plot_main_indicators(ax_main, data)
2767
- _plot_signal_markers(ax_main, data, order_info)
2768
-
2769
- for i, (_, ind_cols) in enumerate(sorted(groups.items())):
2770
- ax_sub = axes[i + 1]
2771
- for j, (col, display_name) in enumerate(ind_cols):
2772
- if col in data.columns:
2773
- ax_sub.plot(
2774
- data["ts_event"],
2775
- data[col],
2776
- label=display_name,
2777
- linewidth=1.5,
2778
- alpha=0.8,
2779
- color=_SUBPLOT_COLORS[j % len(_SUBPLOT_COLORS)],
2780
- )
2781
- ax_sub.legend(loc="upper left", fontsize=8)
2782
- ax_sub.grid(True, alpha=0.3)
2783
-
2784
- status = order_info["resolution_type"] or "PENDING"
2785
- title = f"Signal: {order_info['signal'] or 'unnamed'} - {symbol} - {order_info['action'] or 'UNKNOWN'} - {status}"
2786
- ax_main.set_title(title, fontsize=14, fontweight="bold")
2787
-
2788
- total_seconds = (
2789
- data["ts_event"].iloc[-1] - data["ts_event"].iloc[0]
2790
- ).total_seconds()
2791
- hours = total_seconds / 3600
2792
- days = total_seconds / 86400
2793
-
2794
- for a in axes:
2795
- if days > 30:
2796
- a.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d"))
2797
- a.xaxis.set_major_locator(mdates.WeekdayLocator(interval=1))
2798
- elif days > 7:
2799
- a.xaxis.set_major_formatter(mdates.DateFormatter("%m/%d"))
2800
- a.xaxis.set_major_locator(mdates.DayLocator(interval=2))
2801
- elif days > 2:
2802
- a.xaxis.set_major_formatter(mdates.DateFormatter("%m/%d"))
2803
- a.xaxis.set_major_locator(mdates.DayLocator(interval=1))
2804
- elif hours > 24:
2805
- a.xaxis.set_major_formatter(mdates.DateFormatter("%m/%d %H:%M"))
2806
- a.xaxis.set_major_locator(mdates.HourLocator(interval=6))
2807
- elif hours > 8:
2808
- a.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
2809
- a.xaxis.set_major_locator(mdates.HourLocator(interval=2))
2810
- elif hours > 2:
2811
- a.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
2812
- a.xaxis.set_major_locator(mdates.HourLocator(interval=1))
2813
- elif hours > 0.5:
2814
- a.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
2815
- a.xaxis.set_major_locator(mdates.MinuteLocator(interval=15))
2816
- else:
2817
- a.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M:%S"))
2818
- a.xaxis.set_major_locator(mdates.MinuteLocator(interval=5))
2819
-
2820
- plt.xticks(rotation=45, fontsize=9)
2821
- plt.tight_layout()
2822
- plt.savefig(output_path, dpi=300, bbox_inches="tight")
2823
- plt.close(fig)
2824
-
2825
-
2826
- def _plot_signal_markers(ax, data: pd.DataFrame, order_info: dict) -> None:
2827
- submission_time = order_info["submission_time"]
2828
- mask = data["ts_event"] >= submission_time
2829
- if mask.any():
2830
- idx = data[mask].index[0]
2831
- price = data.loc[idx, "close"]
2832
- marker = "^" if order_info["side"] == "BUY" else "v"
2833
- color = "blue"
2834
- ax.scatter(
2835
- submission_time,
2836
- price,
2837
- marker=marker,
2838
- color=color,
2839
- s=200,
2840
- edgecolors="black",
2841
- linewidth=1.5,
2842
- zorder=5,
2843
- alpha=0.9,
2844
- label="Submission",
2845
- )
2846
- y_lim = ax.get_ylim()
2847
- offset_y = (y_lim[1] - y_lim[0]) * 0.03
2848
- label_text = f"{order_info['side']} {order_info['quantity'] or ''}"
2849
- ax.annotate(
2850
- label_text,
2851
- (submission_time, price + offset_y),
2852
- ha="center",
2853
- va="bottom",
2854
- fontsize=9,
2855
- fontweight="bold",
2856
- bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.9),
2857
- )
2858
-
2859
- if order_info["resolution_time"]:
2860
- res_time = order_info["resolution_time"]
2861
- mask_res = data["ts_event"] >= res_time
2862
- if mask_res.any():
2863
- idx_res = data[mask_res].index[0]
2864
- if order_info["fill_price"]:
2865
- res_price = order_info["fill_price"]
2866
- else:
2867
- res_price = data.loc[idx_res, "close"]
2868
- if order_info["resolution_type"] == "FILLED":
2869
- res_marker = "o"
2870
- res_color = "green"
2871
- else:
2872
- res_marker = "x"
2873
- res_color = "red"
2874
- ax.scatter(
2875
- res_time,
2876
- res_price,
2877
- marker=res_marker,
2878
- color=res_color,
2879
- s=200,
2880
- edgecolors="black",
2881
- linewidth=1.5,
2882
- zorder=5,
2883
- alpha=0.9,
2884
- label=order_info["resolution_type"],
2885
- )
2886
- ax.legend(loc="upper right", fontsize=8)
2887
-
2888
-
2889
- @app.get("/pipeline/signal-validation", response_class=HTMLResponse)
2890
- async def signal_validation_page():
2891
- sidebar = SIDEBAR_HTML.format(
2892
- runs_active="",
2893
- backtest_active="",
2894
- secmaster_active="",
2895
- pipeline_active="active",
2896
- pipeline_open="open",
2897
- pipeline_sub1_active="active",
2898
- pipeline_sub2_active="",
2899
- )
2900
- return f"""
2901
- <!DOCTYPE html>
2902
- <html>
2903
- <head>
2904
- <title>Signal Validation - OneSecondTrader</title>
2905
- <style>{BASE_STYLE}{SIGNAL_VALIDATION_STYLE}{RUN_DETAIL_STYLE}</style>
2906
- </head>
2907
- <body>
2908
- {sidebar}
2909
- <main class="main-content" style="padding: 0;">
2910
- <div class="sv-layout">
2911
- <div class="sv-sidebar">
2912
- <div class="sv-run-selector">
2913
- <label>Run</label>
2914
- <select id="run-select" onchange="onRunChange()">
2915
- <option value="">Loading...</option>
2916
- </select>
2917
- </div>
2918
- <div id="action-groups"></div>
2919
- </div>
2920
- <div class="sv-main">
2921
- <div id="sv-content">
2922
- <div class="sv-empty">Select a run from the dropdown to begin</div>
2923
- </div>
2924
- </div>
2925
- </div>
2926
- </main>
2927
- <div id="chart-modal" class="chart-modal" onclick="if(event.target===this)closeChartModal()">
2928
- <div class="chart-modal-content">
2929
- <div class="chart-modal-header">
2930
- <h3 id="chart-modal-title">Signal Chart</h3>
2931
- <button class="chart-modal-close" onclick="closeChartModal()">&times;</button>
2932
- </div>
2933
- <div id="chart-modal-body" class="chart-modal-body"></div>
2934
- </div>
2935
- </div>
2936
- <script>{SIGNAL_VALIDATION_SCRIPT}</script>
2937
- </body>
2938
- </html>
2939
- """
2940
-
2941
-
2942
- @app.get("/pipeline/sub2", response_class=HTMLResponse)
2943
- async def pipeline_sub2():
2944
- sidebar = SIDEBAR_HTML.format(
2945
- runs_active="",
2946
- backtest_active="",
2947
- secmaster_active="",
2948
- pipeline_active="active",
2949
- pipeline_open="open",
2950
- pipeline_sub1_active="",
2951
- pipeline_sub2_active="active",
2952
- )
2953
- return f"""
2954
- <!DOCTYPE html>
2955
- <html>
2956
- <head>
2957
- <title>Sub Tab 2 - OneSecondTrader</title>
2958
- <style>{BASE_STYLE}</style>
2959
- </head>
2960
- <body>
2961
- {sidebar}
2962
- <main class="main-content">
2963
- <div class="container">
2964
- <div class="card">
2965
- <h2>Sub Tab 2</h2>
2966
- <p style="color: #8b949e;">This is Sub Tab 2 of the Dev Pipeline.</p>
2967
- </div>
2968
- </div>
2969
- </main>
2970
- </body>
2971
- </html>
2972
- """