onesecondtrader 0.41.0__py3-none-any.whl → 0.44.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 (55) hide show
  1. onesecondtrader/__init__.py +0 -58
  2. onesecondtrader/models/__init__.py +11 -0
  3. onesecondtrader/models/bar_fields.py +23 -0
  4. onesecondtrader/models/bar_period.py +21 -0
  5. onesecondtrader/models/order_types.py +21 -0
  6. onesecondtrader/models/trade_sides.py +20 -0
  7. {onesecondtrader-0.41.0.dist-info → onesecondtrader-0.44.0.dist-info}/METADATA +2 -2
  8. onesecondtrader-0.44.0.dist-info/RECORD +10 -0
  9. onesecondtrader/connectors/__init__.py +0 -3
  10. onesecondtrader/connectors/brokers/__init__.py +0 -4
  11. onesecondtrader/connectors/brokers/ib.py +0 -418
  12. onesecondtrader/connectors/brokers/simulated.py +0 -349
  13. onesecondtrader/connectors/datafeeds/__init__.py +0 -4
  14. onesecondtrader/connectors/datafeeds/ib.py +0 -286
  15. onesecondtrader/connectors/datafeeds/simulated.py +0 -167
  16. onesecondtrader/connectors/gateways/__init__.py +0 -3
  17. onesecondtrader/connectors/gateways/ib.py +0 -314
  18. onesecondtrader/core/__init__.py +0 -7
  19. onesecondtrader/core/brokers/__init__.py +0 -3
  20. onesecondtrader/core/brokers/base.py +0 -46
  21. onesecondtrader/core/datafeeds/__init__.py +0 -3
  22. onesecondtrader/core/datafeeds/base.py +0 -32
  23. onesecondtrader/core/events/__init__.py +0 -33
  24. onesecondtrader/core/events/bases.py +0 -29
  25. onesecondtrader/core/events/market.py +0 -22
  26. onesecondtrader/core/events/requests.py +0 -31
  27. onesecondtrader/core/events/responses.py +0 -54
  28. onesecondtrader/core/indicators/__init__.py +0 -13
  29. onesecondtrader/core/indicators/averages.py +0 -56
  30. onesecondtrader/core/indicators/bar.py +0 -47
  31. onesecondtrader/core/indicators/base.py +0 -60
  32. onesecondtrader/core/messaging/__init__.py +0 -7
  33. onesecondtrader/core/messaging/eventbus.py +0 -47
  34. onesecondtrader/core/messaging/subscriber.py +0 -69
  35. onesecondtrader/core/models/__init__.py +0 -14
  36. onesecondtrader/core/models/data.py +0 -18
  37. onesecondtrader/core/models/orders.py +0 -15
  38. onesecondtrader/core/models/params.py +0 -21
  39. onesecondtrader/core/models/records.py +0 -32
  40. onesecondtrader/core/strategies/__init__.py +0 -7
  41. onesecondtrader/core/strategies/base.py +0 -324
  42. onesecondtrader/core/strategies/examples.py +0 -43
  43. onesecondtrader/dashboard/__init__.py +0 -3
  44. onesecondtrader/dashboard/app.py +0 -1677
  45. onesecondtrader/dashboard/registry.py +0 -100
  46. onesecondtrader/orchestrator/__init__.py +0 -7
  47. onesecondtrader/orchestrator/orchestrator.py +0 -105
  48. onesecondtrader/orchestrator/recorder.py +0 -196
  49. onesecondtrader/orchestrator/schema.sql +0 -208
  50. onesecondtrader/secmaster/__init__.py +0 -6
  51. onesecondtrader/secmaster/schema.sql +0 -740
  52. onesecondtrader/secmaster/utils.py +0 -737
  53. onesecondtrader-0.41.0.dist-info/RECORD +0 -49
  54. {onesecondtrader-0.41.0.dist-info → onesecondtrader-0.44.0.dist-info}/WHEEL +0 -0
  55. {onesecondtrader-0.41.0.dist-info → onesecondtrader-0.44.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,1677 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import enum
4
- import os
5
- import sqlite3
6
-
7
- from fastapi import FastAPI, BackgroundTasks
8
- from fastapi.responses import HTMLResponse
9
- from pydantic import BaseModel
10
-
11
- from onesecondtrader.orchestrator import Orchestrator
12
- from onesecondtrader.connectors.brokers import SimulatedBroker
13
- from onesecondtrader.connectors.datafeeds import SimulatedDatafeed
14
- from . import registry
15
-
16
- app = FastAPI(title="OneSecondTrader Dashboard")
17
-
18
- _running_jobs: dict[str, str] = {}
19
-
20
- RTYPE_TO_BAR_PERIOD = {32: "SECOND", 33: "MINUTE", 34: "HOUR", 35: "DAY"}
21
- BAR_PERIOD_TO_RTYPE = {"SECOND": 32, "MINUTE": 33, "HOUR": 34, "DAY": 35}
22
- BAR_PERIOD_ENUM_TO_NAME = {1: "SECOND", 2: "MINUTE", 3: "HOUR", 4: "DAY"}
23
-
24
-
25
- def _normalize_bar_period(value) -> str | None:
26
- if value is None:
27
- return None
28
- if isinstance(value, str):
29
- if value in BAR_PERIOD_TO_RTYPE:
30
- return value
31
- try:
32
- value = int(value)
33
- except ValueError:
34
- return value
35
- if isinstance(value, int):
36
- return BAR_PERIOD_ENUM_TO_NAME.get(value, str(value))
37
- return str(value)
38
-
39
-
40
- def _get_secmaster_path() -> str:
41
- return os.environ.get("SECMASTER_DB_PATH", "secmaster.db")
42
-
43
-
44
- def _get_available_symbols(bar_period: str | None = None) -> list[str]:
45
- db_path = _get_secmaster_path()
46
- if not os.path.exists(db_path):
47
- return []
48
-
49
- conn = sqlite3.connect(db_path)
50
- cursor = conn.cursor()
51
- if bar_period and bar_period in BAR_PERIOD_TO_RTYPE:
52
- rtype = BAR_PERIOD_TO_RTYPE[bar_period]
53
- cursor.execute(
54
- "SELECT DISTINCT symbol FROM symbol_coverage WHERE rtype = ? ORDER BY symbol",
55
- (rtype,),
56
- )
57
- else:
58
- cursor.execute("SELECT DISTINCT symbol FROM symbol_coverage ORDER BY symbol")
59
- symbols = [row[0] for row in cursor.fetchall()]
60
- conn.close()
61
- return symbols
62
-
63
-
64
- def _get_symbols_with_coverage(bar_period: str) -> list[dict]:
65
- db_path = _get_secmaster_path()
66
- if not os.path.exists(db_path) or bar_period not in BAR_PERIOD_TO_RTYPE:
67
- return []
68
-
69
- rtype = BAR_PERIOD_TO_RTYPE[bar_period]
70
- conn = sqlite3.connect(db_path)
71
- cursor = conn.cursor()
72
- cursor.execute(
73
- "SELECT symbol, min_ts, max_ts FROM symbol_coverage WHERE rtype = ? ORDER BY symbol",
74
- (rtype,),
75
- )
76
- symbols = [
77
- {"symbol": row[0], "min_ts": row[1], "max_ts": row[2]}
78
- for row in cursor.fetchall()
79
- ]
80
- conn.close()
81
- return symbols
82
-
83
-
84
- def _get_available_bar_periods() -> list[str]:
85
- db_path = _get_secmaster_path()
86
- if not os.path.exists(db_path):
87
- return []
88
-
89
- conn = sqlite3.connect(db_path)
90
- cursor = conn.cursor()
91
- cursor.execute("SELECT DISTINCT rtype FROM symbol_coverage ORDER BY rtype")
92
- periods = [
93
- RTYPE_TO_BAR_PERIOD[row[0]]
94
- for row in cursor.fetchall()
95
- if row[0] in RTYPE_TO_BAR_PERIOD
96
- ]
97
- conn.close()
98
- return periods
99
-
100
-
101
- BASE_STYLE = """
102
- * { margin: 0; padding: 0; box-sizing: border-box; }
103
- body {
104
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
105
- background: #0d1117;
106
- color: #e6edf3;
107
- min-height: 100vh;
108
- display: flex;
109
- }
110
- .sidebar {
111
- width: 220px;
112
- background: #161b22;
113
- border-right: 1px solid #30363d;
114
- min-height: 100vh;
115
- position: fixed;
116
- left: 0;
117
- top: 0;
118
- display: flex;
119
- flex-direction: column;
120
- }
121
- .sidebar-header {
122
- padding: 20px 16px;
123
- border-bottom: 1px solid #30363d;
124
- }
125
- .sidebar-header h1 {
126
- font-size: 16px;
127
- font-weight: 600;
128
- color: #e6edf3;
129
- }
130
- .sidebar-nav {
131
- padding: 12px 8px;
132
- flex: 1;
133
- }
134
- .sidebar-nav a {
135
- display: flex;
136
- align-items: center;
137
- gap: 10px;
138
- padding: 10px 12px;
139
- color: #8b949e;
140
- text-decoration: none;
141
- font-size: 14px;
142
- border-radius: 6px;
143
- margin-bottom: 2px;
144
- }
145
- .sidebar-nav a:hover {
146
- background: #21262d;
147
- color: #e6edf3;
148
- }
149
- .sidebar-nav a.active {
150
- background: #21262d;
151
- color: #e6edf3;
152
- }
153
- .sidebar-nav svg {
154
- width: 16px;
155
- height: 16px;
156
- flex-shrink: 0;
157
- }
158
- .main-content {
159
- margin-left: 220px;
160
- flex: 1;
161
- min-height: 100vh;
162
- }
163
- .container {
164
- max-width: 1200px;
165
- margin: 0 auto;
166
- padding: 32px 24px;
167
- }
168
- .card {
169
- background: #161b22;
170
- border: 1px solid #30363d;
171
- border-radius: 8px;
172
- padding: 24px;
173
- margin-bottom: 16px;
174
- }
175
- .card h2 {
176
- font-size: 16px;
177
- font-weight: 600;
178
- margin-bottom: 16px;
179
- color: #e6edf3;
180
- }
181
- .empty-state {
182
- text-align: center;
183
- padding: 48px;
184
- color: #8b949e;
185
- }
186
- .empty-state p { margin-top: 8px; font-size: 14px; }
187
- .badge {
188
- display: inline-block;
189
- padding: 4px 10px;
190
- border-radius: 12px;
191
- font-size: 12px;
192
- font-weight: 500;
193
- }
194
- .badge-green { background: #238636; color: #fff; }
195
- .badge-yellow { background: #9e6a03; color: #fff; }
196
- .badge-red { background: #da3633; color: #fff; }
197
- """
198
-
199
-
200
- SIDEBAR_HTML = """
201
- <aside class="sidebar">
202
- <div class="sidebar-header">
203
- <h1>OneSecondTrader</h1>
204
- </div>
205
- <nav class="sidebar-nav">
206
- <a href="/" class="{runs_active}">
207
- <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>
208
- Runs
209
- </a>
210
- <a href="/backtest" class="{backtest_active}">
211
- <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>
212
- Backtest
213
- </a>
214
- <a href="/secmaster" class="{secmaster_active}">
215
- <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>
216
- Securities Master
217
- </a>
218
- </nav>
219
- </aside>
220
- """
221
-
222
-
223
- RUNS_STYLE = """
224
- .runs-table { width: 100%; border-collapse: collapse; }
225
- .runs-table th, .runs-table td { padding: 12px; text-align: left; border-bottom: 1px solid #30363d; }
226
- .runs-table th { font-size: 12px; color: #8b949e; text-transform: uppercase; font-weight: 500; }
227
- .runs-table tbody tr { cursor: pointer; }
228
- .runs-table tbody tr:hover { background: #21262d; }
229
- .runs-table .symbols { font-family: monospace; font-size: 12px; color: #8b949e; }
230
- .runs-table .date-range { font-size: 12px; color: #8b949e; }
231
- .runs-table .delete-btn { background: none; border: none; cursor: pointer; padding: 4px 8px; color: #8b949e; font-size: 16px; border-radius: 4px; }
232
- .runs-table .delete-btn:hover { background: #f8514926; color: #f85149; }
233
- .runs-table .actions { text-align: center; width: 50px; }
234
- """
235
-
236
- RUNS_SCRIPT = """
237
- async function deleteRun(runId, event) {
238
- event.stopPropagation();
239
- if (!confirm('Delete this run? This cannot be undone.')) return;
240
- const res = await fetch(`/api/run/${runId}`, { method: 'DELETE' });
241
- if (res.ok) loadRuns();
242
- else alert('Failed to delete run');
243
- }
244
- async function loadRuns() {
245
- const container = document.getElementById('runs-content');
246
- const res = await fetch('/api/runs');
247
- const data = await res.json();
248
- if (!data.runs || data.runs.length === 0) {
249
- container.innerHTML = `
250
- <div class="empty-state">
251
- <p>No runs yet.</p>
252
- <p>Run a strategy with RunRecorder to see results here.</p>
253
- </div>
254
- `;
255
- return;
256
- }
257
- const rows = data.runs.map(r => {
258
- const statusClass = r.status === 'completed' ? 'badge-green' : r.status === 'running' ? 'badge-yellow' : 'badge-red';
259
- const createdAt = r.created_at ? r.created_at.split('T')[0] + ' ' + r.created_at.split('T')[1].split('.')[0] : '-';
260
- const dateRange = r.first_bar && r.last_bar
261
- ? r.first_bar.split('T')[0] + ' → ' + r.last_bar.split('T')[0]
262
- : '-';
263
- return `<tr onclick="window.location='/run/${r.run_id}'">
264
- <td>${createdAt}</td>
265
- <td>${r.strategy}</td>
266
- <td class="symbols">${r.symbols.length}</td>
267
- <td>${r.bar_period || '-'}</td>
268
- <td class="date-range">${dateRange}</td>
269
- <td><span class="badge ${statusClass}">${r.status}</span></td>
270
- <td class="actions"><button class="delete-btn" onclick="deleteRun('${r.run_id}', event)" title="Delete run">🗑</button></td>
271
- </tr>`;
272
- }).join('');
273
- container.innerHTML = `
274
- <table class="runs-table">
275
- <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>
276
- <tbody>${rows}</tbody>
277
- </table>
278
- `;
279
- }
280
- document.addEventListener('DOMContentLoaded', loadRuns);
281
- """
282
-
283
-
284
- @app.get("/", response_class=HTMLResponse)
285
- async def index():
286
- sidebar = SIDEBAR_HTML.format(
287
- runs_active="active", backtest_active="", secmaster_active=""
288
- )
289
- return f"""
290
- <!DOCTYPE html>
291
- <html>
292
- <head>
293
- <title>OneSecondTrader Dashboard</title>
294
- <style>{BASE_STYLE}{RUNS_STYLE}</style>
295
- </head>
296
- <body>
297
- {sidebar}
298
- <main class="main-content">
299
- <div class="container">
300
- <div class="card">
301
- <h2>Recent Runs</h2>
302
- <div id="runs-content">
303
- <p style="color: #8b949e;">Loading...</p>
304
- </div>
305
- </div>
306
- </div>
307
- </main>
308
- <script>{RUNS_SCRIPT}</script>
309
- </body>
310
- </html>
311
- """
312
-
313
-
314
- RUN_DETAIL_STYLE = """
315
- .run-header { margin-bottom: 24px; }
316
- .run-header h2 { margin-bottom: 8px; }
317
- .run-meta { display: flex; gap: 24px; flex-wrap: wrap; color: #8b949e; font-size: 14px; }
318
- .run-meta span { display: flex; align-items: center; gap: 6px; }
319
- .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-bottom: 24px; }
320
- .stat-card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 16px; }
321
- .stat-card .label { font-size: 12px; color: #8b949e; margin-bottom: 4px; }
322
- .stat-card .value { font-size: 24px; font-weight: 600; }
323
- .back-link { color: #58a6ff; text-decoration: none; font-size: 14px; display: inline-block; margin-bottom: 16px; }
324
- .back-link:hover { text-decoration: underline; }
325
- .positions-table { width: 100%; border-collapse: collapse; margin-top: 16px; font-size: 14px; }
326
- .positions-table th { text-align: left; padding: 12px 8px; border-bottom: 1px solid #30363d; color: #8b949e; font-weight: 500; }
327
- .positions-table td { padding: 10px 8px; border-bottom: 1px solid #21262d; }
328
- .positions-table tr:hover { background: #161b22; }
329
- .pnl-positive { color: #3fb950; }
330
- .pnl-negative { color: #f85149; }
331
- .side-long { color: #3fb950; }
332
- .side-short { color: #f85149; }
333
- .positions-summary { display: flex; gap: 24px; margin-bottom: 16px; padding: 12px; background: #161b22; border-radius: 6px; }
334
- .positions-summary .item { display: flex; flex-direction: column; }
335
- .positions-summary .item-label { font-size: 11px; color: #8b949e; text-transform: uppercase; }
336
- .positions-summary .item-value { font-size: 18px; font-weight: 600; }
337
- .positions-filter { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
338
- .positions-filter select { background: #21262d; border: 1px solid #30363d; border-radius: 6px; padding: 8px 12px; color: #c9d1d9; font-size: 14px; min-width: 150px; }
339
- .positions-filter select:focus { outline: none; border-color: #58a6ff; }
340
- .positions-filter label { color: #8b949e; font-size: 14px; }
341
- """
342
-
343
- RUN_DETAIL_SCRIPT = """
344
- let allPositions = [];
345
- function formatTime(ts) {
346
- if (!ts) return '-';
347
- return ts.split('T')[0] + ' ' + ts.split('T')[1].split('.')[0];
348
- }
349
- function formatPnl(pnl) {
350
- const cls = pnl >= 0 ? 'pnl-positive' : 'pnl-negative';
351
- const sign = pnl >= 0 ? '+' : '';
352
- return `<span class="${cls}">${sign}$${pnl.toFixed(2)}</span>`;
353
- }
354
- function computeSummary(positions) {
355
- const totalPnl = positions.reduce((sum, p) => sum + p.pnl, 0);
356
- const winners = positions.filter(p => p.pnl > 0).length;
357
- const losers = positions.filter(p => p.pnl < 0).length;
358
- const winRate = positions.length > 0 ? winners / positions.length : 0;
359
- return { total_positions: positions.length, total_pnl: totalPnl, winners, losers, win_rate: winRate };
360
- }
361
- function renderPositions(positions, selectedSymbol) {
362
- const filtered = selectedSymbol ? positions.filter(p => p.symbol === selectedSymbol) : positions;
363
- const s = computeSummary(filtered);
364
- const winRate = s.win_rate ? (s.win_rate * 100).toFixed(1) + '%' : '-';
365
- const summaryPnlClass = s.total_pnl >= 0 ? 'pnl-positive' : 'pnl-negative';
366
- document.getElementById('summary-pnl').className = 'item-value ' + summaryPnlClass;
367
- document.getElementById('summary-pnl').textContent = (s.total_pnl >= 0 ? '+' : '') + '$' + s.total_pnl.toFixed(2);
368
- document.getElementById('summary-positions').textContent = s.total_positions;
369
- document.getElementById('summary-winners').textContent = s.winners;
370
- document.getElementById('summary-losers').textContent = s.losers;
371
- document.getElementById('summary-winrate').textContent = winRate;
372
- const tbody = document.getElementById('positions-tbody');
373
- tbody.innerHTML = filtered.map((p, i) => `
374
- <tr>
375
- <td>${i + 1}</td>
376
- <td>${p.symbol}</td>
377
- <td class="${p.side === 'LONG' ? 'side-long' : 'side-short'}">${p.side}</td>
378
- <td>${p.quantity}</td>
379
- <td>${formatTime(p.entry_time)}</td>
380
- <td>${formatTime(p.exit_time)}</td>
381
- <td>$${p.avg_entry_price.toFixed(2)}</td>
382
- <td>$${p.avg_exit_price.toFixed(2)}</td>
383
- <td>${formatPnl(p.pnl)}</td>
384
- </tr>
385
- `).join('');
386
- }
387
- function onSymbolFilterChange() {
388
- const sel = document.getElementById('symbol-filter');
389
- renderPositions(allPositions, sel.value);
390
- }
391
- async function loadRunDetail() {
392
- const runId = window.location.pathname.split('/run/')[1];
393
- const container = document.getElementById('run-content');
394
- const [runRes, posRes] = await Promise.all([
395
- fetch(`/api/run/${runId}`),
396
- fetch(`/api/run/${runId}/positions`)
397
- ]);
398
- if (!runRes.ok) {
399
- container.innerHTML = '<p style="color: #f85149;">Run not found.</p>';
400
- return;
401
- }
402
- const r = await runRes.json();
403
- const posData = await posRes.json();
404
- allPositions = posData.positions || [];
405
- const statusClass = r.status === 'completed' ? 'badge-green' : r.status === 'running' ? 'badge-yellow' : 'badge-red';
406
- const createdAt = formatTime(r.created_at);
407
- const dateRange = r.first_bar && r.last_bar
408
- ? r.first_bar.split('T')[0] + ' → ' + r.last_bar.split('T')[0]
409
- : '-';
410
- const symbols = [...new Set(allPositions.map(p => p.symbol))].sort();
411
- const s = computeSummary(allPositions);
412
- const winRate = s.win_rate ? (s.win_rate * 100).toFixed(1) + '%' : '-';
413
- let positionsHtml = '';
414
- if (allPositions.length > 0) {
415
- const summaryPnlClass = s.total_pnl >= 0 ? 'pnl-positive' : 'pnl-negative';
416
- const symbolOptions = symbols.map(sym => `<option value="${sym}">${sym}</option>`).join('');
417
- positionsHtml = `
418
- <div class="card" style="margin-top: 24px;">
419
- <h3>Positions (Round-Trip Trades)</h3>
420
- <div class="positions-filter">
421
- <label for="symbol-filter">Filter by Symbol:</label>
422
- <select id="symbol-filter" onchange="onSymbolFilterChange()">
423
- <option value="">All Symbols</option>
424
- ${symbolOptions}
425
- </select>
426
- </div>
427
- <div class="positions-summary">
428
- <div class="item">
429
- <span class="item-label">Total P&L</span>
430
- <span id="summary-pnl" class="item-value ${summaryPnlClass}">${s.total_pnl >= 0 ? '+' : ''}$${s.total_pnl.toFixed(2)}</span>
431
- </div>
432
- <div class="item">
433
- <span class="item-label">Positions</span>
434
- <span id="summary-positions" class="item-value">${s.total_positions}</span>
435
- </div>
436
- <div class="item">
437
- <span class="item-label">Winners</span>
438
- <span id="summary-winners" class="item-value pnl-positive">${s.winners}</span>
439
- </div>
440
- <div class="item">
441
- <span class="item-label">Losers</span>
442
- <span id="summary-losers" class="item-value pnl-negative">${s.losers}</span>
443
- </div>
444
- <div class="item">
445
- <span class="item-label">Win Rate</span>
446
- <span id="summary-winrate" class="item-value">${winRate}</span>
447
- </div>
448
- </div>
449
- <table class="positions-table">
450
- <thead>
451
- <tr>
452
- <th>#</th><th>Symbol</th><th>Side</th><th>Qty</th>
453
- <th>Entry Time</th><th>Exit Time</th><th>Avg Entry</th><th>Avg Exit</th><th>P&L</th>
454
- </tr>
455
- </thead>
456
- <tbody id="positions-tbody"></tbody>
457
- </table>
458
- </div>
459
- `;
460
- } else {
461
- positionsHtml = `
462
- <div class="card" style="margin-top: 24px;">
463
- <h3>Positions (Round-Trip Trades)</h3>
464
- <p style="color: #8b949e;">No completed positions.</p>
465
- </div>
466
- `;
467
- }
468
-
469
- container.innerHTML = `
470
- <a href="/" class="back-link">← Back to Runs</a>
471
- <div class="run-header">
472
- <h2>${r.strategy}</h2>
473
- <div class="run-meta">
474
- <span><span class="badge ${statusClass}">${r.status}</span></span>
475
- <span>Created: ${createdAt}</span>
476
- <span>Mode: ${r.mode}</span>
477
- </div>
478
- </div>
479
- <div class="stats-grid">
480
- <div class="stat-card">
481
- <div class="label">Symbols</div>
482
- <div class="value">${r.symbols.length}</div>
483
- </div>
484
- <div class="stat-card">
485
- <div class="label">Bar Period</div>
486
- <div class="value">${r.bar_period || '-'}</div>
487
- </div>
488
- <div class="stat-card">
489
- <div class="label">Total Bars</div>
490
- <div class="value">${r.bar_count || 0}</div>
491
- </div>
492
- <div class="stat-card">
493
- <div class="label">Date Range</div>
494
- <div class="value" style="font-size: 14px;">${dateRange}</div>
495
- </div>
496
- </div>
497
- <div class="card">
498
- <h3>Symbols</h3>
499
- <p style="color: #8b949e; font-family: monospace;">${r.symbols.join(', ') || 'None'}</p>
500
- </div>
501
- ${positionsHtml}
502
- `;
503
- if (allPositions.length > 0) {
504
- renderPositions(allPositions, '');
505
- }
506
- }
507
- document.addEventListener('DOMContentLoaded', loadRunDetail);
508
- """
509
-
510
-
511
- @app.get("/run/{run_id}", response_class=HTMLResponse)
512
- async def run_detail_page(run_id: str):
513
- sidebar = SIDEBAR_HTML.format(
514
- runs_active="active", backtest_active="", secmaster_active=""
515
- )
516
- return f"""
517
- <!DOCTYPE html>
518
- <html>
519
- <head>
520
- <title>Run Detail - OneSecondTrader</title>
521
- <style>{BASE_STYLE}{RUN_DETAIL_STYLE}</style>
522
- </head>
523
- <body>
524
- {sidebar}
525
- <main class="main-content">
526
- <div class="container">
527
- <div id="run-content">
528
- <p style="color: #8b949e;">Loading...</p>
529
- </div>
530
- </div>
531
- </main>
532
- <script>{RUN_DETAIL_SCRIPT}</script>
533
- </body>
534
- </html>
535
- """
536
-
537
-
538
- def _get_runs_db_path() -> str:
539
- return os.environ.get("RUNS_DB_PATH", "runs.db")
540
-
541
-
542
- def _get_recent_runs(limit: int = 50) -> list[dict]:
543
- import json
544
-
545
- db_path = _get_runs_db_path()
546
- if not os.path.exists(db_path):
547
- return []
548
-
549
- conn = sqlite3.connect(db_path)
550
- cursor = conn.cursor()
551
- cursor.execute(
552
- """
553
- SELECT
554
- r.run_id,
555
- r.strategy,
556
- r.symbols,
557
- r.bar_period,
558
- r.mode,
559
- r.status,
560
- r.created_at,
561
- r.completed_at,
562
- COUNT(b.id) as bar_count,
563
- MIN(b.ts_event) as first_bar,
564
- MAX(b.ts_event) as last_bar
565
- FROM runs r
566
- LEFT JOIN bars b ON r.run_id = b.run_id
567
- GROUP BY r.run_id
568
- ORDER BY r.created_at DESC
569
- LIMIT ?
570
- """,
571
- (limit,),
572
- )
573
- runs = []
574
- for row in cursor.fetchall():
575
- runs.append(
576
- {
577
- "run_id": row[0],
578
- "strategy": row[1],
579
- "symbols": json.loads(row[2]) if row[2] else [],
580
- "bar_period": _normalize_bar_period(row[3]),
581
- "mode": row[4],
582
- "status": row[5],
583
- "created_at": row[6],
584
- "completed_at": row[7],
585
- "bar_count": row[8],
586
- "first_bar": row[9],
587
- "last_bar": row[10],
588
- }
589
- )
590
- conn.close()
591
- return runs
592
-
593
-
594
- @app.get("/api/runs")
595
- async def list_runs(limit: int = 50):
596
- return {"runs": _get_recent_runs(limit)}
597
-
598
-
599
- def _get_run_by_id(run_id: str) -> dict | None:
600
- import json
601
-
602
- db_path = _get_runs_db_path()
603
- if not os.path.exists(db_path):
604
- return None
605
-
606
- conn = sqlite3.connect(db_path)
607
- cursor = conn.cursor()
608
- cursor.execute(
609
- """
610
- SELECT
611
- r.run_id,
612
- r.strategy,
613
- r.symbols,
614
- r.bar_period,
615
- r.mode,
616
- r.status,
617
- r.created_at,
618
- r.completed_at,
619
- COUNT(b.id) as bar_count,
620
- MIN(b.ts_event) as first_bar,
621
- MAX(b.ts_event) as last_bar
622
- FROM runs r
623
- LEFT JOIN bars b ON r.run_id = b.run_id
624
- WHERE r.run_id = ?
625
- GROUP BY r.run_id
626
- """,
627
- (run_id,),
628
- )
629
- row = cursor.fetchone()
630
- conn.close()
631
- if not row:
632
- return None
633
- return {
634
- "run_id": row[0],
635
- "strategy": row[1],
636
- "symbols": json.loads(row[2]) if row[2] else [],
637
- "bar_period": _normalize_bar_period(row[3]),
638
- "mode": row[4],
639
- "status": row[5],
640
- "created_at": row[6],
641
- "completed_at": row[7],
642
- "bar_count": row[8],
643
- "first_bar": row[9],
644
- "last_bar": row[10],
645
- }
646
-
647
-
648
- @app.get("/api/run/{run_id}")
649
- async def get_run(run_id: str):
650
- run = _get_run_by_id(run_id)
651
- if not run:
652
- from fastapi import HTTPException
653
-
654
- raise HTTPException(status_code=404, detail="Run not found")
655
- return run
656
-
657
-
658
- @app.delete("/api/run/{run_id}")
659
- async def delete_run(run_id: str):
660
- db_path = _get_runs_db_path()
661
- if not os.path.exists(db_path):
662
- from fastapi import HTTPException
663
-
664
- raise HTTPException(status_code=404, detail="Run not found")
665
- conn = sqlite3.connect(db_path)
666
- cursor = conn.cursor()
667
- cursor.execute("DELETE FROM bars WHERE run_id = ?", (run_id,))
668
- cursor.execute("DELETE FROM order_requests WHERE run_id = ?", (run_id,))
669
- cursor.execute("DELETE FROM order_responses WHERE run_id = ?", (run_id,))
670
- cursor.execute("DELETE FROM fills WHERE run_id = ?", (run_id,))
671
- cursor.execute("DELETE FROM runs WHERE run_id = ?", (run_id,))
672
- conn.commit()
673
- conn.close()
674
- return {"status": "deleted"}
675
-
676
-
677
- def _normalize_side(side) -> str:
678
- if side in ("BUY", 1, "1"):
679
- return "BUY"
680
- return "SELL"
681
-
682
-
683
- def _extract_positions(fills: list[dict]) -> list[dict]:
684
- if not fills:
685
- return []
686
- for f in fills:
687
- f["side"] = _normalize_side(f["side"])
688
- positions = []
689
- by_symbol: dict[str, list[dict]] = {}
690
- for f in fills:
691
- by_symbol.setdefault(f["symbol"], []).append(f)
692
- for symbol, symbol_fills in by_symbol.items():
693
- symbol_fills.sort(key=lambda x: x["ts_event"])
694
- position = 0.0
695
- position_fills: list[dict] = []
696
- position_id = 0
697
- for fill in symbol_fills:
698
- qty = fill["quantity"]
699
- signed_qty = qty if fill["side"] == "BUY" else -qty
700
- new_position = position + signed_qty
701
- position_fills.append(fill)
702
- if new_position == 0.0 and position != 0.0:
703
- position_id += 1
704
- pnl = 0.0
705
- total_commission = 0.0
706
- for pf in position_fills:
707
- value = pf["price"] * pf["quantity"]
708
- commission = pf.get("commission") or 0.0
709
- total_commission += commission
710
- if pf["side"] == "BUY":
711
- pnl -= value
712
- else:
713
- pnl += value
714
- pnl -= total_commission
715
- entry_fill = position_fills[0]
716
- exit_fill = position_fills[-1]
717
- entry_side = entry_fill["side"]
718
- entry_qty = sum(
719
- pf["quantity"] for pf in position_fills if pf["side"] == entry_side
720
- )
721
- entry_value = sum(
722
- pf["quantity"] * pf["price"]
723
- for pf in position_fills
724
- if pf["side"] == entry_side
725
- )
726
- avg_entry = entry_value / entry_qty if entry_qty else 0
727
- exit_side = "SELL" if entry_side == "BUY" else "BUY"
728
- exit_qty = sum(
729
- pf["quantity"] for pf in position_fills if pf["side"] == exit_side
730
- )
731
- exit_value = sum(
732
- pf["quantity"] * pf["price"]
733
- for pf in position_fills
734
- if pf["side"] == exit_side
735
- )
736
- avg_exit = exit_value / exit_qty if exit_qty else 0
737
- positions.append(
738
- {
739
- "position_id": position_id,
740
- "symbol": symbol,
741
- "side": "LONG" if entry_side == "BUY" else "SHORT",
742
- "quantity": entry_qty,
743
- "entry_time": entry_fill["ts_event"],
744
- "exit_time": exit_fill["ts_event"],
745
- "avg_entry_price": avg_entry,
746
- "avg_exit_price": avg_exit,
747
- "pnl": pnl,
748
- "commission": total_commission,
749
- "num_fills": len(position_fills),
750
- }
751
- )
752
- position_fills = []
753
- position = new_position
754
- positions.sort(key=lambda x: x["entry_time"])
755
- return positions
756
-
757
-
758
- @app.get("/api/run/{run_id}/positions")
759
- async def get_run_positions(run_id: str):
760
- db_path = _get_runs_db_path()
761
- if not os.path.exists(db_path):
762
- return {"positions": []}
763
- conn = sqlite3.connect(db_path)
764
- cursor = conn.cursor()
765
- cursor.execute(
766
- """
767
- SELECT ts_event, symbol, side, quantity, price, commission
768
- FROM fills WHERE run_id = ? ORDER BY ts_event
769
- """,
770
- (run_id,),
771
- )
772
- rows = cursor.fetchall()
773
- conn.close()
774
- fills = [
775
- {
776
- "ts_event": row[0],
777
- "symbol": row[1],
778
- "side": row[2],
779
- "quantity": row[3],
780
- "price": row[4],
781
- "commission": row[5],
782
- }
783
- for row in rows
784
- ]
785
- positions = _extract_positions(fills)
786
- total_pnl = sum(p["pnl"] for p in positions)
787
- winners = sum(1 for p in positions if p["pnl"] > 0)
788
- losers = sum(1 for p in positions if p["pnl"] < 0)
789
- return {
790
- "positions": positions,
791
- "summary": {
792
- "total_positions": len(positions),
793
- "total_pnl": total_pnl,
794
- "winners": winners,
795
- "losers": losers,
796
- "win_rate": winners / len(positions) if positions else 0,
797
- },
798
- }
799
-
800
-
801
- @app.get("/health")
802
- async def health():
803
- return {"status": "ok"}
804
-
805
-
806
- @app.get("/api/strategies")
807
- async def list_strategies():
808
- strategies = []
809
- for class_name, cls in registry.get_strategies().items():
810
- display_name = getattr(cls, "name", "") or class_name
811
- strategies.append({"class_name": class_name, "display_name": display_name})
812
- return {"strategies": strategies}
813
-
814
-
815
- @app.get("/api/brokers")
816
- async def list_brokers():
817
- return {"brokers": list(registry.get_brokers().keys())}
818
-
819
-
820
- @app.get("/api/datafeeds")
821
- async def list_datafeeds():
822
- return {"datafeeds": list(registry.get_datafeeds().keys())}
823
-
824
-
825
- @app.get("/api/strategies/{name}")
826
- async def get_strategy(name: str):
827
- schema = registry.get_strategy_schema(name)
828
- if schema is None:
829
- return {"error": "Strategy not found"}
830
- return schema
831
-
832
-
833
- @app.get("/api/brokers/{name}")
834
- async def get_broker(name: str):
835
- schema = registry.get_broker_schema(name)
836
- if schema is None:
837
- return {"error": "Broker not found"}
838
- return schema
839
-
840
-
841
- @app.get("/api/datafeeds/{name}")
842
- async def get_datafeed(name: str):
843
- schema = registry.get_datafeed_schema(name)
844
- if schema is None:
845
- return {"error": "Datafeed not found"}
846
- return schema
847
-
848
-
849
- @app.get("/api/secmaster/symbols")
850
- async def get_symbols(bar_period: str | None = None):
851
- return {"symbols": _get_available_symbols(bar_period)}
852
-
853
-
854
- @app.get("/api/secmaster/symbols_coverage")
855
- async def get_symbols_coverage(bar_period: str):
856
- return {"symbols": _get_symbols_with_coverage(bar_period)}
857
-
858
-
859
- @app.get("/api/secmaster/bar_periods")
860
- async def get_bar_periods():
861
- return {"bar_periods": _get_available_bar_periods()}
862
-
863
-
864
- @app.get("/api/presets")
865
- async def list_presets():
866
- db_path = _get_secmaster_path()
867
- if not os.path.exists(db_path):
868
- return {"presets": []}
869
- conn = sqlite3.connect(db_path)
870
- cursor = conn.cursor()
871
- cursor.execute("SELECT name FROM symbol_presets ORDER BY name")
872
- presets = [row[0] for row in cursor.fetchall()]
873
- conn.close()
874
- return {"presets": presets}
875
-
876
-
877
- @app.get("/api/presets/{name}")
878
- async def get_preset(name: str):
879
- import json
880
-
881
- db_path = _get_secmaster_path()
882
- if not os.path.exists(db_path):
883
- return {"error": "Preset not found"}
884
- conn = sqlite3.connect(db_path)
885
- cursor = conn.cursor()
886
- cursor.execute("SELECT symbols FROM symbol_presets WHERE name = ?", (name,))
887
- row = cursor.fetchone()
888
- conn.close()
889
- if row is None:
890
- return {"error": "Preset not found"}
891
- return {"name": name, "symbols": json.loads(row[0])}
892
-
893
-
894
- class PresetRequest(BaseModel):
895
- name: str
896
- symbols: list[str]
897
-
898
-
899
- @app.post("/api/presets")
900
- async def create_preset(request: PresetRequest):
901
- import json
902
-
903
- db_path = _get_secmaster_path()
904
- conn = sqlite3.connect(db_path)
905
- cursor = conn.cursor()
906
- cursor.execute(
907
- "INSERT INTO symbol_presets (name, symbols) VALUES (?, ?)",
908
- (request.name, json.dumps(request.symbols)),
909
- )
910
- conn.commit()
911
- conn.close()
912
- return {"status": "created", "name": request.name}
913
-
914
-
915
- @app.put("/api/presets/{name}")
916
- async def update_preset(name: str, request: PresetRequest):
917
- import json
918
-
919
- db_path = _get_secmaster_path()
920
- conn = sqlite3.connect(db_path)
921
- cursor = conn.cursor()
922
- cursor.execute(
923
- "UPDATE symbol_presets SET symbols = ? WHERE name = ?",
924
- (json.dumps(request.symbols), name),
925
- )
926
- conn.commit()
927
- conn.close()
928
- return {"status": "updated", "name": name}
929
-
930
-
931
- @app.delete("/api/presets/{name}")
932
- async def delete_preset(name: str):
933
- db_path = _get_secmaster_path()
934
- conn = sqlite3.connect(db_path)
935
- cursor = conn.cursor()
936
- cursor.execute("DELETE FROM symbol_presets WHERE name = ?", (name,))
937
- conn.commit()
938
- conn.close()
939
- return {"status": "deleted", "name": name}
940
-
941
-
942
- class BacktestRequest(BaseModel):
943
- strategy: str
944
- strategy_params: dict
945
- symbols: list[str]
946
- start_date: str | None = None
947
- end_date: str | None = None
948
-
949
-
950
- def _run_backtest(request: BacktestRequest, run_id: str) -> None:
951
- import pandas as pd
952
-
953
- try:
954
- _running_jobs[run_id] = "running"
955
-
956
- strategy_cls = registry.get_strategies().get(request.strategy)
957
- if not strategy_cls:
958
- _running_jobs[run_id] = "error: invalid strategy"
959
- return
960
-
961
- strategy_params = _deserialize_params(
962
- request.strategy_params, getattr(strategy_cls, "parameters", {})
963
- )
964
-
965
- updated_parameters = {}
966
- for name, spec in strategy_cls.parameters.items():
967
- if name in strategy_params:
968
- updated_parameters[name] = type(spec)(
969
- default=strategy_params[name],
970
- **{k: v for k, v in spec.__dict__.items() if k != "default"},
971
- )
972
- else:
973
- updated_parameters[name] = spec
974
-
975
- configured_strategy = type(
976
- f"Configured{request.strategy}",
977
- (strategy_cls,),
978
- {"symbols": request.symbols, "parameters": updated_parameters},
979
- )
980
-
981
- datafeed_attrs = {}
982
- if request.start_date:
983
- datafeed_attrs["start_ts"] = int(
984
- pd.Timestamp(request.start_date, tz="UTC").value
985
- )
986
- if request.end_date:
987
- end_dt = (
988
- pd.Timestamp(request.end_date, tz="UTC")
989
- + pd.Timedelta(days=1)
990
- - pd.Timedelta(1, unit="ns")
991
- )
992
- datafeed_attrs["end_ts"] = int(end_dt.value)
993
-
994
- configured_datafeed = type(
995
- "ConfiguredSimulatedDatafeed",
996
- (SimulatedDatafeed,),
997
- datafeed_attrs,
998
- )
999
-
1000
- configured_orchestrator = type(
1001
- "ConfiguredOrchestrator",
1002
- (Orchestrator,),
1003
- {"mode": "backtest"},
1004
- )
1005
-
1006
- orchestrator = configured_orchestrator(
1007
- strategies=[configured_strategy],
1008
- broker=SimulatedBroker,
1009
- datafeed=configured_datafeed,
1010
- )
1011
-
1012
- orchestrator.run()
1013
- _running_jobs[run_id] = "completed"
1014
- except Exception as e:
1015
- _running_jobs[run_id] = f"error: {e}"
1016
-
1017
-
1018
- def _deserialize_params(params: dict, param_specs: dict) -> dict:
1019
- result = {}
1020
- for name, value in params.items():
1021
- spec = param_specs.get(name)
1022
- if spec is None:
1023
- result[name] = value
1024
- continue
1025
- if isinstance(spec.default, enum.Enum):
1026
- enum_cls = type(spec.default)
1027
- result[name] = enum_cls[value]
1028
- else:
1029
- result[name] = value
1030
- return result
1031
-
1032
-
1033
- @app.post("/api/backtest/run")
1034
- async def run_backtest(request: BacktestRequest, background_tasks: BackgroundTasks):
1035
- import uuid
1036
-
1037
- run_id = str(uuid.uuid4())[:8]
1038
- background_tasks.add_task(_run_backtest, request, run_id)
1039
- return {"run_id": run_id, "status": "started"}
1040
-
1041
-
1042
- @app.get("/api/backtest/status/{run_id}")
1043
- async def backtest_status(run_id: str):
1044
- status = _running_jobs.get(run_id, "not found")
1045
- return {"run_id": run_id, "status": status}
1046
-
1047
-
1048
- BACKTEST_FORM_STYLE = """
1049
- .form-group { margin-bottom: 16px; }
1050
- .form-group label { display: block; margin-bottom: 6px; font-size: 14px; color: #8b949e; }
1051
- .form-group select, .form-group input {
1052
- width: 100%; padding: 8px 12px; background: #0d1117; border: 1px solid #30363d;
1053
- border-radius: 6px; color: #e6edf3; font-size: 14px;
1054
- }
1055
- .form-group select:focus, .form-group input:focus { outline: none; border-color: #58a6ff; }
1056
- .params-container { margin-top: 12px; padding: 12px; background: #0d1117; border-radius: 6px; }
1057
- .param-row { display: flex; gap: 12px; margin-bottom: 8px; align-items: center; }
1058
- .param-row label { min-width: 120px; font-size: 13px; }
1059
- .param-row input, .param-row select { flex: 1; }
1060
- .btn { padding: 10px 20px; background: #238636; border: none; border-radius: 6px;
1061
- color: #fff; font-size: 14px; cursor: pointer; }
1062
- .btn:hover { background: #2ea043; }
1063
- .btn:disabled { background: #30363d; cursor: not-allowed; }
1064
- .btn-sm { padding: 6px 12px; font-size: 12px; }
1065
- .btn-secondary { background: #30363d; }
1066
- .btn-secondary:hover { background: #484f58; }
1067
- .btn-danger { background: #da3633; }
1068
- .btn-danger:hover { background: #f85149; }
1069
- #status { margin-top: 16px; padding: 12px; border-radius: 6px; display: none; }
1070
- .status-running { background: #9e6a03; }
1071
- .status-completed { background: #238636; }
1072
- .status-error { background: #da3633; }
1073
- .date-row { display: flex; gap: 12px; }
1074
- .date-row .form-group { flex: 1; margin-bottom: 0; }
1075
- .symbol-section { background: #0d1117; border-radius: 6px; padding: 12px; }
1076
- .preset-row { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
1077
- .preset-row select { flex: 1; }
1078
- .preset-row input { flex: 1; }
1079
- .search-row { display: flex; gap: 8px; margin-bottom: 8px; }
1080
- .search-row input { flex: 1; }
1081
- .search-results { max-height: 150px; overflow-y: auto; border: 1px solid #30363d; border-radius: 4px; margin-bottom: 12px; }
1082
- .search-results:empty { display: none; }
1083
- .search-result { display: flex; justify-content: space-between; align-items: center; padding: 6px 10px; border-bottom: 1px solid #21262d; }
1084
- .search-result:last-child { border-bottom: none; }
1085
- .search-result:hover { background: #161b22; }
1086
- .search-result .symbol { font-family: monospace; }
1087
- .selected-symbols { display: flex; flex-wrap: wrap; gap: 6px; min-height: 32px; max-height: 150px; overflow-y: auto; }
1088
- .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; }
1089
- .selected-tag .remove { cursor: pointer; opacity: 0.7; }
1090
- .selected-tag .remove:hover { opacity: 1; }
1091
- .selected-label { font-size: 13px; color: #8b949e; margin-bottom: 6px; }
1092
- .section-header { font-size: 14px; color: #8b949e; margin-bottom: 6px; margin-top: 8px; }
1093
- """
1094
-
1095
- BACKTEST_FORM_SCRIPT = """
1096
- let strategyParams = [];
1097
- let symbolCoverage = {};
1098
- let allSymbols = [];
1099
- let selectedSymbols = [];
1100
- let presets = [];
1101
- let barPeriods = [];
1102
- let currentBarPeriod = '';
1103
- let globalMinTs = null;
1104
- let globalMaxTs = null;
1105
- let globalMinDate = null;
1106
- let globalMaxDate = null;
1107
-
1108
- async function loadStrategies() {
1109
- const res = await fetch('/api/strategies');
1110
- const data = await res.json();
1111
- const sel = document.getElementById('strategy');
1112
- data.strategies.forEach(s => sel.innerHTML += `<option value="${s.class_name}">${s.display_name}</option>`);
1113
- if (data.strategies.length > 0) { sel.value = data.strategies[0].class_name; loadStrategyParams(); }
1114
- }
1115
-
1116
- async function loadBarPeriods() {
1117
- const res = await fetch('/api/secmaster/bar_periods');
1118
- const data = await res.json();
1119
- barPeriods = data.bar_periods || [];
1120
- const sel = document.getElementById('bar-period');
1121
- sel.innerHTML = '';
1122
- barPeriods.forEach(p => sel.innerHTML += `<option value="${p}">${p}</option>`);
1123
- if (barPeriods.length > 0) {
1124
- currentBarPeriod = barPeriods[0];
1125
- sel.value = currentBarPeriod;
1126
- await loadSymbolsForPeriod();
1127
- }
1128
- }
1129
-
1130
- async function onBarPeriodChange() {
1131
- currentBarPeriod = document.getElementById('bar-period').value;
1132
- selectedSymbols = [];
1133
- renderSelectedSymbols();
1134
- await loadSymbolsForPeriod();
1135
- updateDateRange();
1136
- }
1137
-
1138
- async function loadSymbolsForPeriod() {
1139
- const res = await fetch(`/api/secmaster/symbols_coverage?bar_period=${currentBarPeriod}`);
1140
- const data = await res.json();
1141
- symbolCoverage = {};
1142
- allSymbols = [];
1143
- (data.symbols || []).forEach(s => {
1144
- symbolCoverage[s.symbol] = {min_ts: s.min_ts, max_ts: s.max_ts};
1145
- allSymbols.push(s.symbol);
1146
- });
1147
- }
1148
-
1149
- async function loadPresets() {
1150
- const res = await fetch('/api/presets');
1151
- const data = await res.json();
1152
- presets = data.presets || [];
1153
- renderPresetSelect();
1154
- }
1155
-
1156
- function renderPresetSelect() {
1157
- const sel = document.getElementById('preset-select');
1158
- sel.innerHTML = '<option value="">-- Select Preset --</option>';
1159
- presets.forEach(p => sel.innerHTML += `<option value="${p}">${p}</option>`);
1160
- }
1161
-
1162
- async function loadPreset() {
1163
- const name = document.getElementById('preset-select').value;
1164
- if (!name) return;
1165
- const res = await fetch(`/api/presets/${encodeURIComponent(name)}`);
1166
- const data = await res.json();
1167
- if (data.symbols) {
1168
- selectedSymbols = data.symbols.filter(s => allSymbols.includes(s));
1169
- renderSelectedSymbols();
1170
- updateDateRange();
1171
- }
1172
- }
1173
-
1174
- async function savePreset() {
1175
- const nameInput = document.getElementById('preset-name');
1176
- const name = nameInput.value.trim();
1177
- if (!name) { alert('Enter a preset name'); return; }
1178
- if (selectedSymbols.length === 0) { alert('Select at least one symbol'); return; }
1179
- const exists = presets.includes(name);
1180
- const method = exists ? 'PUT' : 'POST';
1181
- const url = exists ? `/api/presets/${encodeURIComponent(name)}` : '/api/presets';
1182
- await fetch(url, {
1183
- method, headers: {'Content-Type': 'application/json'},
1184
- body: JSON.stringify({name, symbols: selectedSymbols})
1185
- });
1186
- nameInput.value = '';
1187
- await loadPresets();
1188
- document.getElementById('preset-select').value = name;
1189
- }
1190
-
1191
- async function deletePreset() {
1192
- const name = document.getElementById('preset-select').value;
1193
- if (!name) { alert('Select a preset to delete'); return; }
1194
- if (!confirm(`Delete preset "${name}"?`)) return;
1195
- await fetch(`/api/presets/${encodeURIComponent(name)}`, {method: 'DELETE'});
1196
- await loadPresets();
1197
- selectedSymbols = [];
1198
- renderSelectedSymbols();
1199
- updateDateRange();
1200
- }
1201
-
1202
- function searchSymbols() {
1203
- const query = document.getElementById('symbol-search').value.toUpperCase();
1204
- const container = document.getElementById('search-results');
1205
- if (query.length < 1) { container.innerHTML = ''; return; }
1206
- const matches = allSymbols.filter(s => s.toUpperCase().includes(query) && !selectedSymbols.includes(s)).slice(0, 20);
1207
- 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('');
1208
- }
1209
-
1210
- function addSymbol(symbol) {
1211
- if (!selectedSymbols.includes(symbol)) {
1212
- selectedSymbols.push(symbol);
1213
- selectedSymbols.sort();
1214
- renderSelectedSymbols();
1215
- searchSymbols();
1216
- updateDateRange();
1217
- }
1218
- }
1219
-
1220
- function removeSymbol(symbol) {
1221
- selectedSymbols = selectedSymbols.filter(s => s !== symbol);
1222
- renderSelectedSymbols();
1223
- searchSymbols();
1224
- updateDateRange();
1225
- }
1226
-
1227
- function renderSelectedSymbols() {
1228
- const container = document.getElementById('selected-symbols');
1229
- const label = document.getElementById('selected-label');
1230
- label.textContent = `Selected (${selectedSymbols.length}):`;
1231
- container.innerHTML = selectedSymbols.map(s => `<span class="selected-tag">${s}<span class="remove" onclick="removeSymbol('${s}')">&times;</span></span>`).join('');
1232
- }
1233
-
1234
- function updateDateRange() {
1235
- if (selectedSymbols.length === 0) {
1236
- globalMinTs = null;
1237
- globalMaxTs = null;
1238
- document.getElementById('start-date').value = '';
1239
- document.getElementById('end-date').value = '';
1240
- document.getElementById('range-slider-container').style.display = 'none';
1241
- return;
1242
- }
1243
- let minTs = Infinity, maxTs = -Infinity;
1244
- selectedSymbols.forEach(s => {
1245
- const cov = symbolCoverage[s];
1246
- if (cov) {
1247
- if (cov.min_ts < minTs) minTs = cov.min_ts;
1248
- if (cov.max_ts > maxTs) maxTs = cov.max_ts;
1249
- }
1250
- });
1251
- if (minTs === Infinity) return;
1252
- globalMinTs = minTs;
1253
- globalMaxTs = maxTs;
1254
- globalMinDate = tsToDate(minTs);
1255
- globalMaxDate = tsToDate(maxTs);
1256
- const startInput = document.getElementById('start-date');
1257
- const endInput = document.getElementById('end-date');
1258
- startInput.min = globalMinDate;
1259
- startInput.max = globalMaxDate;
1260
- startInput.value = globalMinDate;
1261
- endInput.min = globalMinDate;
1262
- endInput.max = globalMaxDate;
1263
- endInput.value = globalMaxDate;
1264
- }
1265
-
1266
- function tsToDate(ts) {
1267
- return new Date(ts / 1000000).toISOString().split('T')[0];
1268
- }
1269
-
1270
- function clampDate(inputId) {
1271
- if (!globalMinDate || !globalMaxDate) return;
1272
- const input = document.getElementById(inputId);
1273
- if (input.value < globalMinDate) input.value = globalMinDate;
1274
- if (input.value > globalMaxDate) input.value = globalMaxDate;
1275
- }
1276
-
1277
- async function loadStrategyParams() {
1278
- const name = document.getElementById('strategy').value;
1279
- const res = await fetch(`/api/strategies/${name}`);
1280
- const data = await res.json();
1281
- strategyParams = (data.parameters || []).filter(p => p.name !== 'bar_period');
1282
- renderParams('strategy-params', strategyParams, 'sp_');
1283
- }
1284
-
1285
- function formatParamName(name) {
1286
- return name.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
1287
- }
1288
-
1289
- function renderParams(containerId, params, prefix) {
1290
- const container = document.getElementById(containerId);
1291
- container.innerHTML = '';
1292
- params.forEach(p => {
1293
- const row = document.createElement('div');
1294
- row.className = 'param-row';
1295
- let input;
1296
- if (p.choices) {
1297
- input = `<select id="${prefix}${p.name}">` +
1298
- p.choices.map(c => `<option value="${c}" ${c===p.default?'selected':''}>${c}</option>`).join('') +
1299
- '</select>';
1300
- } else if (p.type === 'bool') {
1301
- input = `<select id="${prefix}${p.name}"><option value="true" ${p.default?'selected':''}>true</option><option value="false" ${!p.default?'selected':''}>false</option></select>`;
1302
- } else {
1303
- const attrs = [];
1304
- if (p.min !== undefined) attrs.push(`min="${p.min}"`);
1305
- if (p.max !== undefined) attrs.push(`max="${p.max}"`);
1306
- if (p.step !== undefined) attrs.push(`step="${p.step}"`);
1307
- input = `<input type="${p.type==='int'||p.type==='float'?'number':'text'}" id="${prefix}${p.name}" value="${p.default}" ${attrs.join(' ')}>`;
1308
- }
1309
- row.innerHTML = `<label>${formatParamName(p.name)}</label>${input}`;
1310
- container.appendChild(row);
1311
- });
1312
- }
1313
-
1314
- function collectParams(params, prefix) {
1315
- const result = {};
1316
- params.forEach(p => {
1317
- const el = document.getElementById(prefix + p.name);
1318
- let val = el.value;
1319
- if (p.type === 'int') val = parseInt(val);
1320
- else if (p.type === 'float') val = parseFloat(val);
1321
- else if (p.type === 'bool') val = val === 'true';
1322
- result[p.name] = val;
1323
- });
1324
- return result;
1325
- }
1326
-
1327
- async function runBacktest() {
1328
- const btn = document.getElementById('run-btn');
1329
- const status = document.getElementById('status');
1330
- if (selectedSymbols.length === 0) {
1331
- alert('Please select at least one symbol');
1332
- return;
1333
- }
1334
- btn.disabled = true;
1335
- status.style.display = 'block';
1336
- status.className = 'status-running';
1337
- status.textContent = 'Starting backtest...';
1338
-
1339
- const params = collectParams(strategyParams, 'sp_');
1340
- params.bar_period = currentBarPeriod;
1341
- const payload = {
1342
- strategy: document.getElementById('strategy').value,
1343
- strategy_params: params,
1344
- symbols: selectedSymbols,
1345
- start_date: document.getElementById('start-date').value || null,
1346
- end_date: document.getElementById('end-date').value || null
1347
- };
1348
-
1349
- const res = await fetch('/api/backtest/run', {
1350
- method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)
1351
- });
1352
- const data = await res.json();
1353
- const runId = data.run_id;
1354
- status.textContent = `Backtest ${runId} running...`;
1355
-
1356
- const poll = setInterval(async () => {
1357
- const r = await fetch(`/api/backtest/status/${runId}`);
1358
- const d = await r.json();
1359
- if (d.status === 'completed') {
1360
- clearInterval(poll);
1361
- status.className = 'status-completed';
1362
- status.textContent = `Backtest ${runId} completed!`;
1363
- btn.disabled = false;
1364
- } else if (d.status.startsWith('error')) {
1365
- clearInterval(poll);
1366
- status.className = 'status-error';
1367
- status.textContent = `Backtest ${runId}: ${d.status}`;
1368
- btn.disabled = false;
1369
- }
1370
- }, 1000);
1371
- }
1372
-
1373
- document.addEventListener('DOMContentLoaded', () => {
1374
- loadStrategies();
1375
- loadBarPeriods();
1376
- loadPresets();
1377
- });
1378
- """
1379
-
1380
-
1381
- @app.get("/backtest", response_class=HTMLResponse)
1382
- async def backtest_form():
1383
- sidebar = SIDEBAR_HTML.format(
1384
- runs_active="", backtest_active="active", secmaster_active=""
1385
- )
1386
- return f"""
1387
- <!DOCTYPE html>
1388
- <html>
1389
- <head>
1390
- <title>Backtest - OneSecondTrader</title>
1391
- <style>{BASE_STYLE}{BACKTEST_FORM_STYLE}</style>
1392
- </head>
1393
- <body>
1394
- {sidebar}
1395
- <main class="main-content">
1396
- <div class="container">
1397
- <div class="card">
1398
- <h2>Run Backtest</h2>
1399
- <div class="form-group">
1400
- <label>Strategy</label>
1401
- <select id="strategy" onchange="loadStrategyParams()"></select>
1402
- <div class="section-header">Parameters</div>
1403
- <div id="strategy-params" class="params-container"></div>
1404
- </div>
1405
- <div class="form-group">
1406
- <label>Bar Period</label>
1407
- <select id="bar-period" onchange="onBarPeriodChange()"></select>
1408
- </div>
1409
- <div class="form-group">
1410
- <label>Symbols</label>
1411
- <div class="symbol-section">
1412
- <div class="preset-row">
1413
- <select id="preset-select" onchange="loadPreset()"></select>
1414
- <input type="text" id="preset-name" placeholder="New preset name...">
1415
- <button class="btn btn-sm btn-secondary" onclick="savePreset()">Save</button>
1416
- <button class="btn btn-sm btn-danger" onclick="deletePreset()">Delete</button>
1417
- </div>
1418
- <div class="search-row">
1419
- <input type="text" id="symbol-search" placeholder="Search symbols..." oninput="searchSymbols()">
1420
- </div>
1421
- <div id="search-results" class="search-results"></div>
1422
- <div id="selected-label" class="selected-label">Selected (0):</div>
1423
- <div id="selected-symbols" class="selected-symbols"></div>
1424
- </div>
1425
- </div>
1426
- <div class="date-row">
1427
- <div class="form-group">
1428
- <label>Start Date</label>
1429
- <input type="date" id="start-date" onchange="clampDate('start-date')">
1430
- </div>
1431
- <div class="form-group">
1432
- <label>End Date</label>
1433
- <input type="date" id="end-date" onchange="clampDate('end-date')">
1434
- </div>
1435
- </div>
1436
- <div class="form-group" style="margin-top: 16px;">
1437
- <button id="run-btn" class="btn" onclick="runBacktest()">Run Backtest</button>
1438
- </div>
1439
- <div id="status"></div>
1440
- </div>
1441
- </div>
1442
- </main>
1443
- <script>{BACKTEST_FORM_SCRIPT}</script>
1444
- </body>
1445
- </html>
1446
- """
1447
-
1448
-
1449
- def _get_secmaster_summary() -> dict:
1450
- db_path = _get_secmaster_path()
1451
- if not os.path.exists(db_path):
1452
- return {"exists": False}
1453
-
1454
- conn = sqlite3.connect(db_path)
1455
- cursor = conn.cursor()
1456
-
1457
- cursor.execute("SELECT key, value FROM meta")
1458
- meta = {row[0]: row[1] for row in cursor.fetchall()}
1459
- conn.close()
1460
-
1461
- if not meta:
1462
- return {"exists": True, "db_path": db_path, "needs_reindex": True}
1463
-
1464
- rtypes_str = meta.get("ohlcv_schemas", "")
1465
- rtypes = [RTYPE_TO_BAR_PERIOD.get(int(r), r) for r in rtypes_str.split(",") if r]
1466
-
1467
- db_size_bytes = os.path.getsize(db_path)
1468
-
1469
- return {
1470
- "exists": True,
1471
- "db_path": db_path,
1472
- "db_size_mb": round(db_size_bytes / (1024 * 1024), 1),
1473
- "symbol_count": int(meta.get("symbol_count", 0)),
1474
- "ohlcv_record_count": int(meta.get("ohlcv_record_count", 0)),
1475
- "min_ts": int(meta.get("ohlcv_min_ts", 0)),
1476
- "max_ts": int(meta.get("ohlcv_max_ts", 0)),
1477
- "schemas": rtypes,
1478
- }
1479
-
1480
-
1481
- def _search_symbol(query: str) -> list[dict]:
1482
- db_path = _get_secmaster_path()
1483
- if not os.path.exists(db_path):
1484
- return []
1485
-
1486
- conn = sqlite3.connect(db_path)
1487
- cursor = conn.cursor()
1488
-
1489
- cursor.execute(
1490
- """
1491
- SELECT symbol, rtype, min_ts, max_ts, record_count
1492
- FROM symbol_coverage
1493
- WHERE symbol LIKE ?
1494
- ORDER BY symbol
1495
- LIMIT 100
1496
- """,
1497
- (f"%{query}%",),
1498
- )
1499
-
1500
- results = []
1501
- for row in cursor.fetchall():
1502
- results.append(
1503
- {
1504
- "symbol": row[0],
1505
- "min_ts": row[2],
1506
- "max_ts": row[3],
1507
- "record_count": row[4],
1508
- "schema": (
1509
- RTYPE_TO_BAR_PERIOD.get(row[1], str(row[1])) if row[1] else None
1510
- ),
1511
- }
1512
- )
1513
-
1514
- conn.close()
1515
- return results
1516
-
1517
-
1518
- @app.get("/api/secmaster/summary")
1519
- async def get_secmaster_summary():
1520
- return _get_secmaster_summary()
1521
-
1522
-
1523
- @app.get("/api/secmaster/search")
1524
- async def search_secmaster(q: str = ""):
1525
- if len(q) < 1:
1526
- return {"results": []}
1527
- return {"results": _search_symbol(q)}
1528
-
1529
-
1530
- SECMASTER_STYLE = """
1531
- .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
1532
- .stat-card { background: #0d1117; border-radius: 8px; padding: 20px; }
1533
- .stat-card .label { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; }
1534
- .stat-card .value { font-size: 28px; font-weight: 600; margin-top: 4px; }
1535
- .stat-card .sub { font-size: 12px; color: #8b949e; margin-top: 4px; }
1536
- .asset-table { width: 100%; border-collapse: collapse; }
1537
- .asset-table th, .asset-table td { padding: 12px; text-align: left; border-bottom: 1px solid #30363d; }
1538
- .asset-table th { font-size: 12px; color: #8b949e; text-transform: uppercase; font-weight: 500; }
1539
- .asset-table tr:hover { background: #21262d; }
1540
- .search-box { width: 100%; padding: 12px 16px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e6edf3; font-size: 14px; margin-bottom: 16px; }
1541
- .search-box:focus { outline: none; border-color: #58a6ff; }
1542
- .search-results { max-height: 400px; overflow-y: auto; }
1543
- .no-db { text-align: center; padding: 48px; color: #8b949e; }
1544
- .no-db code { background: #21262d; padding: 2px 6px; border-radius: 4px; }
1545
- .loading { display: flex; align-items: center; gap: 12px; color: #8b949e; padding: 24px; }
1546
- .spinner { width: 20px; height: 20px; border: 2px solid #30363d; border-top-color: #58a6ff; border-radius: 50%; animation: spin 0.8s linear infinite; }
1547
- @keyframes spin { to { transform: rotate(360deg); } }
1548
- """
1549
-
1550
- SECMASTER_SCRIPT = """
1551
- function showLoading(elementId, message) {
1552
- document.getElementById(elementId).innerHTML = `<div class="loading"><div class="spinner"></div><span>${message}</span></div>`;
1553
- }
1554
-
1555
- async function loadSummary() {
1556
- showLoading('content', 'Loading database summary...');
1557
- const res = await fetch('/api/secmaster/summary');
1558
- const data = await res.json();
1559
-
1560
- if (!data.exists) {
1561
- document.getElementById('content').innerHTML = `
1562
- <div class="no-db">
1563
- <p>No securities master database found.</p>
1564
- <p style="margin-top: 8px;">Set <code>SECMASTER_DB_PATH</code> environment variable or place <code>secmaster.db</code> in the working directory.</p>
1565
- </div>
1566
- `;
1567
- return;
1568
- }
1569
-
1570
- if (data.needs_reindex) {
1571
- document.getElementById('content').innerHTML = `
1572
- <div class="no-db">
1573
- <p>Database metadata not found.</p>
1574
- <p style="margin-top: 8px;">Run <code>from onesecondtrader.secmaster.utils import update_meta; update_meta(path)</code> to rebuild stats.</p>
1575
- </div>
1576
- `;
1577
- return;
1578
- }
1579
-
1580
- const minDate = new Date(data.min_ts / 1000000).toISOString().split('T')[0];
1581
- const maxDate = new Date(data.max_ts / 1000000).toISOString().split('T')[0];
1582
-
1583
- document.getElementById('content').innerHTML = `
1584
- <div class="stats-grid">
1585
- <div class="stat-card">
1586
- <div class="label">Symbols</div>
1587
- <div class="value">${data.symbol_count.toLocaleString()}</div>
1588
- </div>
1589
- <div class="stat-card">
1590
- <div class="label">OHLCV Records</div>
1591
- <div class="value">${data.ohlcv_record_count.toLocaleString()}</div>
1592
- </div>
1593
- <div class="stat-card">
1594
- <div class="label">Date Range</div>
1595
- <div class="value" style="font-size: 18px;">${minDate}</div>
1596
- <div class="sub">to ${maxDate}</div>
1597
- </div>
1598
- <div class="stat-card">
1599
- <div class="label">Schemas</div>
1600
- <div class="value" style="font-size: 18px;">${data.schemas.join(', ')}</div>
1601
- </div>
1602
- <div class="stat-card">
1603
- <div class="label">Database Size</div>
1604
- <div class="value">${data.db_size_mb} MB</div>
1605
- <div class="sub">${data.db_path}</div>
1606
- </div>
1607
- </div>
1608
- <div class="card">
1609
- <h2>Symbol Search</h2>
1610
- <input type="text" class="search-box" id="search-input" placeholder="Search symbols..." oninput="searchSymbols()">
1611
- <div id="search-results" class="search-results"></div>
1612
- </div>
1613
- `;
1614
- }
1615
-
1616
- let searchTimeout;
1617
- async function searchSymbols() {
1618
- const q = document.getElementById('search-input').value;
1619
- clearTimeout(searchTimeout);
1620
- if (q.length < 1) {
1621
- document.getElementById('search-results').innerHTML = '';
1622
- return;
1623
- }
1624
- showLoading('search-results', 'Searching...');
1625
- searchTimeout = setTimeout(async () => {
1626
- const res = await fetch('/api/secmaster/search?q=' + encodeURIComponent(q));
1627
- const data = await res.json();
1628
- if (data.results.length === 0) {
1629
- document.getElementById('search-results').innerHTML = '<p style="color: #8b949e;">No results found.</p>';
1630
- return;
1631
- }
1632
- let rows = data.results.map(r => {
1633
- const minDate = r.min_ts ? new Date(r.min_ts / 1000000).toISOString().split('T')[0] : '-';
1634
- const maxDate = r.max_ts ? new Date(r.max_ts / 1000000).toISOString().split('T')[0] : '-';
1635
- return `<tr>
1636
- <td>${r.symbol}</td>
1637
- <td>${r.schema || '-'}</td>
1638
- <td>${minDate} → ${maxDate}</td>
1639
- <td>${r.record_count ? r.record_count.toLocaleString() : '-'}</td>
1640
- </tr>`;
1641
- }).join('');
1642
- document.getElementById('search-results').innerHTML = `
1643
- <table class="asset-table">
1644
- <thead><tr><th>Symbol</th><th>Schema</th><th>Date Range</th><th>Records</th></tr></thead>
1645
- <tbody>${rows}</tbody>
1646
- </table>
1647
- `;
1648
- }, 300);
1649
- }
1650
-
1651
- document.addEventListener('DOMContentLoaded', loadSummary);
1652
- """
1653
-
1654
-
1655
- @app.get("/secmaster", response_class=HTMLResponse)
1656
- async def secmaster_page():
1657
- sidebar = SIDEBAR_HTML.format(
1658
- runs_active="", backtest_active="", secmaster_active="active"
1659
- )
1660
- return f"""
1661
- <!DOCTYPE html>
1662
- <html>
1663
- <head>
1664
- <title>Securities Master - OneSecondTrader</title>
1665
- <style>{BASE_STYLE}{SECMASTER_STYLE}</style>
1666
- </head>
1667
- <body>
1668
- {sidebar}
1669
- <main class="main-content">
1670
- <div class="container" id="content">
1671
- <p style="color: #8b949e;">Loading...</p>
1672
- </div>
1673
- </main>
1674
- <script>{SECMASTER_SCRIPT}</script>
1675
- </body>
1676
- </html>
1677
- """