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.
@@ -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", backtest_active="", secmaster_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} &bull; ${orders.length} order(s)</div>
778
+ </div>
779
+ <table class="sv-table">
780
+ <thead>
781
+ <tr>
782
+ <th>Time</th>
783
+ <th>Symbol</th>
784
+ <th>Side</th>
785
+ <th>Type</th>
786
+ <th>Qty</th>
787
+ <th>Price</th>
788
+ <th>Status</th>
789
+ </tr>
790
+ </thead>
791
+ <tbody>
792
+ ${orders.map(o => `
793
+ <tr onclick="showSignalChart('${o.order_id}', '${o.symbol}')">
794
+ <td>${formatTime(o.ts_event)}</td>
795
+ <td style="font-family: monospace;">${o.symbol}</td>
796
+ <td class="sv-side-${o.side.toLowerCase()}">${o.side}</td>
797
+ <td>${o.order_type || '-'}</td>
798
+ <td>${o.quantity || '-'}</td>
799
+ <td>${o.limit_price ? '$' + o.limit_price.toFixed(2) : (o.stop_price ? 'Stop $' + o.stop_price.toFixed(2) : 'MKT')}</td>
800
+ <td><span class="sv-status sv-status-${o.status.toLowerCase()}">${o.status}</span></td>
801
+ </tr>
802
+ `).join('')}
803
+ </tbody>
804
+ </table>
805
+ `;
806
+ }
807
+
808
+ function showSignalChart(orderId, symbol) {
809
+ const modal = document.getElementById('chart-modal');
810
+ const modalTitle = document.getElementById('chart-modal-title');
811
+ const modalBody = document.getElementById('chart-modal-body');
812
+ modalTitle.textContent = `Signal Chart - ${symbol}`;
813
+ modalBody.innerHTML = '<div style="text-align: center; padding: 40px;"><p>Generating chart...</p></div>';
814
+ modal.style.display = 'flex';
815
+ fetch(`/api/signals/${currentRun}/chart/${orderId}`)
816
+ .then(response => {
817
+ if (!response.ok) {
818
+ return response.json().then(data => { throw new Error(data.detail || 'Failed to load chart'); });
819
+ }
820
+ return response.blob();
821
+ })
822
+ .then(blob => {
823
+ const url = URL.createObjectURL(blob);
824
+ modalBody.innerHTML = `<img src="${url}" style="max-width: 100%; max-height: 80vh;">`;
825
+ })
826
+ .catch(error => {
827
+ modalBody.innerHTML = `<div style="color: #f85149; padding: 20px;">${error.message}</div>`;
828
+ });
829
+ }
830
+
831
+ function closeChartModal() {
832
+ document.getElementById('chart-modal').style.display = 'none';
833
+ }
834
+
835
+ document.addEventListener('DOMContentLoaded', loadRuns);
836
+ """
837
+
838
+
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", backtest_active="", secmaster_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()">&times;</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="", backtest_active="active", secmaster_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="", backtest_active="", secmaster_active="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()">&times;</button>
2932
+ </div>
2933
+ <div id="chart-modal-body" class="chart-modal-body"></div>
2934
+ </div>
2935
+ </div>
2936
+ <script>{SIGNAL_VALIDATION_SCRIPT}</script>
2937
+ </body>
2938
+ </html>
2939
+ """
2940
+
2941
+
2942
+ @app.get("/pipeline/sub2", response_class=HTMLResponse)
2943
+ async def pipeline_sub2():
2944
+ sidebar = SIDEBAR_HTML.format(
2945
+ runs_active="",
2946
+ backtest_active="",
2947
+ secmaster_active="",
2948
+ pipeline_active="active",
2949
+ pipeline_open="open",
2950
+ pipeline_sub1_active="",
2951
+ pipeline_sub2_active="active",
2952
+ )
2953
+ return f"""
2954
+ <!DOCTYPE html>
2955
+ <html>
2956
+ <head>
2957
+ <title>Sub Tab 2 - OneSecondTrader</title>
2958
+ <style>{BASE_STYLE}</style>
2959
+ </head>
2960
+ <body>
2961
+ {sidebar}
2962
+ <main class="main-content">
2963
+ <div class="container">
2964
+ <div class="card">
2965
+ <h2>Sub Tab 2</h2>
2966
+ <p style="color: #8b949e;">This is Sub Tab 2 of the Dev Pipeline.</p>
2967
+ </div>
2968
+ </div>
2969
+ </main>
2970
+ </body>
2971
+ </html>
2972
+ """