onesecondtrader 0.41.0__py3-none-any.whl → 0.43.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.
- onesecondtrader/__init__.py +2 -0
- onesecondtrader/connectors/datafeeds/simulated.py +42 -11
- onesecondtrader/core/events/requests.py +2 -0
- onesecondtrader/core/models/__init__.py +2 -1
- onesecondtrader/core/models/orders.py +12 -0
- onesecondtrader/core/models/records.py +2 -0
- onesecondtrader/core/strategies/base.py +8 -1
- onesecondtrader/core/strategies/examples.py +4 -0
- onesecondtrader/dashboard/app.py +1301 -6
- onesecondtrader/orchestrator/recorder.py +5 -2
- onesecondtrader/orchestrator/schema.sql +5 -1
- {onesecondtrader-0.41.0.dist-info → onesecondtrader-0.43.0.dist-info}/METADATA +1 -1
- {onesecondtrader-0.41.0.dist-info → onesecondtrader-0.43.0.dist-info}/RECORD +15 -15
- {onesecondtrader-0.41.0.dist-info → onesecondtrader-0.43.0.dist-info}/WHEEL +0 -0
- {onesecondtrader-0.41.0.dist-info → onesecondtrader-0.43.0.dist-info}/licenses/LICENSE +0 -0
onesecondtrader/dashboard/app.py
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import enum
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
import shutil
|
|
5
8
|
import sqlite3
|
|
6
9
|
|
|
7
10
|
from fastapi import FastAPI, BackgroundTasks
|
|
8
|
-
from fastapi.responses import HTMLResponse
|
|
11
|
+
from fastapi.responses import HTMLResponse, FileResponse
|
|
9
12
|
from pydantic import BaseModel
|
|
10
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
|
+
|
|
11
22
|
from onesecondtrader.orchestrator import Orchestrator
|
|
12
23
|
from onesecondtrader.connectors.brokers import SimulatedBroker
|
|
13
24
|
from onesecondtrader.connectors.datafeeds import SimulatedDatafeed
|
|
25
|
+
from onesecondtrader.core.models.orders import ActionType
|
|
14
26
|
from . import registry
|
|
15
27
|
|
|
28
|
+
CHARTS_DIR = pathlib.Path(os.environ.get("CHARTS_DIR", "charts"))
|
|
29
|
+
|
|
16
30
|
app = FastAPI(title="OneSecondTrader Dashboard")
|
|
17
31
|
|
|
18
32
|
_running_jobs: dict[str, str] = {}
|
|
@@ -155,6 +169,49 @@ body {
|
|
|
155
169
|
height: 16px;
|
|
156
170
|
flex-shrink: 0;
|
|
157
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
|
+
}
|
|
158
215
|
.main-content {
|
|
159
216
|
margin-left: 220px;
|
|
160
217
|
flex: 1;
|
|
@@ -215,6 +272,17 @@ SIDEBAR_HTML = """
|
|
|
215
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>
|
|
216
273
|
Securities Master
|
|
217
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>
|
|
218
286
|
</nav>
|
|
219
287
|
</aside>
|
|
220
288
|
"""
|
|
@@ -284,7 +352,13 @@ document.addEventListener('DOMContentLoaded', loadRuns);
|
|
|
284
352
|
@app.get("/", response_class=HTMLResponse)
|
|
285
353
|
async def index():
|
|
286
354
|
sidebar = SIDEBAR_HTML.format(
|
|
287
|
-
runs_active="active",
|
|
355
|
+
runs_active="active",
|
|
356
|
+
backtest_active="",
|
|
357
|
+
secmaster_active="",
|
|
358
|
+
pipeline_active="",
|
|
359
|
+
pipeline_open="",
|
|
360
|
+
pipeline_sub1_active="",
|
|
361
|
+
pipeline_sub2_active="",
|
|
288
362
|
)
|
|
289
363
|
return f"""
|
|
290
364
|
<!DOCTYPE html>
|
|
@@ -338,6 +412,15 @@ RUN_DETAIL_STYLE = """
|
|
|
338
412
|
.positions-filter select { background: #21262d; border: 1px solid #30363d; border-radius: 6px; padding: 8px 12px; color: #c9d1d9; font-size: 14px; min-width: 150px; }
|
|
339
413
|
.positions-filter select:focus { outline: none; border-color: #58a6ff; }
|
|
340
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; }
|
|
341
424
|
"""
|
|
342
425
|
|
|
343
426
|
RUN_DETAIL_SCRIPT = """
|
|
@@ -371,7 +454,7 @@ function renderPositions(positions, selectedSymbol) {
|
|
|
371
454
|
document.getElementById('summary-winrate').textContent = winRate;
|
|
372
455
|
const tbody = document.getElementById('positions-tbody');
|
|
373
456
|
tbody.innerHTML = filtered.map((p, i) => `
|
|
374
|
-
<tr>
|
|
457
|
+
<tr class="position-row" data-symbol="${p.symbol}" data-position-id="${p.position_id}">
|
|
375
458
|
<td>${i + 1}</td>
|
|
376
459
|
<td>${p.symbol}</td>
|
|
377
460
|
<td class="${p.side === 'LONG' ? 'side-long' : 'side-short'}">${p.side}</td>
|
|
@@ -383,6 +466,13 @@ function renderPositions(positions, selectedSymbol) {
|
|
|
383
466
|
<td>${formatPnl(p.pnl)}</td>
|
|
384
467
|
</tr>
|
|
385
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
|
+
});
|
|
386
476
|
}
|
|
387
477
|
function onSymbolFilterChange() {
|
|
388
478
|
const sel = document.getElementById('symbol-filter');
|
|
@@ -504,14 +594,258 @@ async function loadRunDetail() {
|
|
|
504
594
|
renderPositions(allPositions, '');
|
|
505
595
|
}
|
|
506
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
|
+
}
|
|
507
625
|
document.addEventListener('DOMContentLoaded', loadRunDetail);
|
|
508
626
|
"""
|
|
509
627
|
|
|
510
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} • ${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
|
+
|
|
511
839
|
@app.get("/run/{run_id}", response_class=HTMLResponse)
|
|
512
840
|
async def run_detail_page(run_id: str):
|
|
513
841
|
sidebar = SIDEBAR_HTML.format(
|
|
514
|
-
runs_active="active",
|
|
842
|
+
runs_active="active",
|
|
843
|
+
backtest_active="",
|
|
844
|
+
secmaster_active="",
|
|
845
|
+
pipeline_active="",
|
|
846
|
+
pipeline_open="",
|
|
847
|
+
pipeline_sub1_active="",
|
|
848
|
+
pipeline_sub2_active="",
|
|
515
849
|
)
|
|
516
850
|
return f"""
|
|
517
851
|
<!DOCTYPE html>
|
|
@@ -529,6 +863,15 @@ async def run_detail_page(run_id: str):
|
|
|
529
863
|
</div>
|
|
530
864
|
</div>
|
|
531
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()">×</button>
|
|
871
|
+
</div>
|
|
872
|
+
<div id="chart-modal-body" class="chart-modal-body"></div>
|
|
873
|
+
</div>
|
|
874
|
+
</div>
|
|
532
875
|
<script>{RUN_DETAIL_SCRIPT}</script>
|
|
533
876
|
</body>
|
|
534
877
|
</html>
|
|
@@ -671,6 +1014,9 @@ async def delete_run(run_id: str):
|
|
|
671
1014
|
cursor.execute("DELETE FROM runs WHERE run_id = ?", (run_id,))
|
|
672
1015
|
conn.commit()
|
|
673
1016
|
conn.close()
|
|
1017
|
+
charts_path = CHARTS_DIR / run_id
|
|
1018
|
+
if charts_path.exists():
|
|
1019
|
+
shutil.rmtree(charts_path)
|
|
674
1020
|
return {"status": "deleted"}
|
|
675
1021
|
|
|
676
1022
|
|
|
@@ -798,6 +1144,505 @@ async def get_run_positions(run_id: str):
|
|
|
798
1144
|
}
|
|
799
1145
|
|
|
800
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
|
+
|
|
801
1646
|
@app.get("/health")
|
|
802
1647
|
async def health():
|
|
803
1648
|
return {"status": "ok"}
|
|
@@ -1381,7 +2226,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
1381
2226
|
@app.get("/backtest", response_class=HTMLResponse)
|
|
1382
2227
|
async def backtest_form():
|
|
1383
2228
|
sidebar = SIDEBAR_HTML.format(
|
|
1384
|
-
runs_active="",
|
|
2229
|
+
runs_active="",
|
|
2230
|
+
backtest_active="active",
|
|
2231
|
+
secmaster_active="",
|
|
2232
|
+
pipeline_active="",
|
|
2233
|
+
pipeline_open="",
|
|
2234
|
+
pipeline_sub1_active="",
|
|
2235
|
+
pipeline_sub2_active="",
|
|
1385
2236
|
)
|
|
1386
2237
|
return f"""
|
|
1387
2238
|
<!DOCTYPE html>
|
|
@@ -1655,7 +2506,13 @@ document.addEventListener('DOMContentLoaded', loadSummary);
|
|
|
1655
2506
|
@app.get("/secmaster", response_class=HTMLResponse)
|
|
1656
2507
|
async def secmaster_page():
|
|
1657
2508
|
sidebar = SIDEBAR_HTML.format(
|
|
1658
|
-
runs_active="",
|
|
2509
|
+
runs_active="",
|
|
2510
|
+
backtest_active="",
|
|
2511
|
+
secmaster_active="active",
|
|
2512
|
+
pipeline_active="",
|
|
2513
|
+
pipeline_open="",
|
|
2514
|
+
pipeline_sub1_active="",
|
|
2515
|
+
pipeline_sub2_active="",
|
|
1659
2516
|
)
|
|
1660
2517
|
return f"""
|
|
1661
2518
|
<!DOCTYPE html>
|
|
@@ -1675,3 +2532,441 @@ async def secmaster_page():
|
|
|
1675
2532
|
</body>
|
|
1676
2533
|
</html>
|
|
1677
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()">×</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
|
+
"""
|