quant-ai / backend /cpp_engine /fast_chart_generator.cpp
Ypeng12's picture
feat(ai): add historical date switcher and dual mode trade replay
67eee20
Raw History Blame Contribute Delete
22.2 kB
// backend/cpp_engine/fast_chart_generator.cpp
// High Performance C++ Retrospective Chart & Dual Mode Engine for Quant.ai
// Execution Time: < 3ms
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <sstream>
#include <chrono>
using namespace std;
string readFile(const string& filePath) {
ifstream f(filePath);
if (!f.is_open()) return "";
stringstream ss;
ss << f.rdbuf();
return ss.str();
}
int main(int argc, char* argv[]) {
auto t0 = chrono::high_resolution_clock::now();
string today_str = "2026-08-26";
if (argc > 1 && string(argv[1]).length() == 10) {
today_str = argv[1];
}
string basePath = "./backend/";
if (!ifstream("./backend/watchlist.json").is_open()) {
basePath = "./";
}
string tradeHistFile = basePath + "trade_history.json";
string realTradesJson = readFile(tradeHistFile);
if (realTradesJson.empty()) realTradesJson = "{\"trade_history\":[]}";
stringstream html;
html << "<!DOCTYPE html>\n"
<< "<html lang=\"zh-CN\">\n"
<< "<head>\n"
<< " <meta charset=\"UTF-8\">\n"
<< " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"
<< " <title>Quant.ai - 双模式对比与历史 K 线复盘看板</title>\n"
<< " <script src=\"https://cdn.plot.ly/plotly-2.24.1.min.js\"></script>\n"
<< " <style>\n"
<< " @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');\n"
<< " * { box-sizing: border-box; font-family: 'Inter', -apple-system, sans-serif; }\n"
<< " body { background-color: #ffffff; color: #0f1419; margin: 0; padding: 24px; }\n"
<< " .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; border-bottom: 1px solid #e1e8ed; padding-bottom: 16px; flex-wrap: wrap; gap: 12px; }\n"
<< " .header .brand { font-size: 22px; font-weight: 700; color: #0f1419; letter-spacing: -0.5px; }\n"
<< " .header .sub { color: #536471; font-size: 13px; margin-top: 4px; }\n"
<< " .date-select { padding: 6px 14px; border-radius: 12px; border: 1px solid #cfd9de; font-weight: 600; font-size: 13px; color: #0f1419; background: #f7f9fa; cursor: pointer; outline: none; }\n"
<< " .mode-switch-bar { display: flex; gap: 12px; margin-bottom: 20px; background: #f0f3f5; padding: 6px; border-radius: 16px; border: 1px solid #e1e8ed; width: fit-content; }\n"
<< " .mode-btn { padding: 10px 20px; border-radius: 12px; border: none; font-weight: 700; font-size: 13px; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; gap: 6px; }\n"
<< " .mode-btn.btn-real { background: transparent; color: #536471; }\n"
<< " .mode-btn.btn-real.active { background: #ffffff; color: #0f1419; box-shadow: 0 2px 6px rgba(0,0,0,0.1); border: 1px solid #cfd9de; }\n"
<< " .mode-btn.btn-ml { background: transparent; color: #536471; }\n"
<< " .mode-btn.btn-ml.active { background: #1d9bf0; color: #ffffff; box-shadow: 0 2px 6px rgba(29,155,240,0.3); }\n"
<< " .controls-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 12px; }\n"
<< " .ticker-pills { display: flex; gap: 8px; flex-wrap: wrap; }\n"
<< " .ticker-pill { padding: 8px 16px; border-radius: 20px; border: 1px solid #e1e8ed; background: #ffffff; color: #0f1419; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; }\n"
<< " .ticker-pill:hover { background: #f7f9fa; border-color: #cfd9de; }\n"
<< " .ticker-pill.active { background: #0f1419; color: #ffffff; border-color: #0f1419; }\n"
<< " .timeframe-pills { display: flex; background: #f7f9fa; border-radius: 20px; padding: 4px; border: 1px solid #e1e8ed; }\n"
<< " .tf-pill { padding: 6px 14px; border-radius: 16px; border: none; background: transparent; color: #536471; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; }\n"
<< " .tf-pill.active { background: #ffffff; color: #00c805; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }\n"
<< " .section-box { background: #ffffff; border: 1px solid #e1e8ed; border-radius: 12px; padding: 20px; margin-bottom: 24px; box-shadow: 0 2px 6px rgba(0,0,0,0.02); }\n"
<< " table { width: 100%; border-collapse: collapse; font-size: 13px; }\n"
<< " th { text-align: left; padding: 12px; color: #536471; font-weight: 600; border-bottom: 1px solid #e1e8ed; background: #f7f9fa; }\n"
<< " td { padding: 12px; border-bottom: 1px solid #f0f3f5; color: #0f1419; }\n"
<< " .badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 700; }\n"
<< " .badge-buy { background: rgba(0, 200, 5, 0.12); color: #00c805; }\n"
<< " .badge-short { background: rgba(255, 80, 0, 0.12); color: #ff5000; }\n"
<< " .engine-badge { background: #1d9bf0; color: #fff; padding: 2px 8px; border-radius: 6px; font-size: 11px; font-weight: 700; margin-left: 8px; }\n"
<< " </style>\n"
<< "</head>\n"
<< "<body>\n"
<< " <div class=\"header\">\n"
<< " <div>\n"
<< " <div class=\"brand\">Quant.ai | 双模式对比与历史 K 线复盘 <span class=\"engine-badge\">⚡ C++ 毫秒级引擎</span></div>\n"
<< " <div class=\"sub\">对比模式:实盘成交流水 vs 新升级 ML 策略仿真回测</div>\n"
<< " </div>\n"
<< " <div style=\"display:flex; align-items:center; gap:8px;\">\n"
<< " <span style=\"font-size:13px; font-weight:600; color:#536471;\">📅 切换历史交易日:</span>\n"
<< " <select class=\"date-select\" id=\"datePicker\" onchange=\"onDateChange(this.value)\">\n"
<< " <option value=\"2026-08-26\" selected>2026-08-26 (今日美东)</option>\n"
<< " <option value=\"2026-08-12\">2026-08-12</option>\n"
<< " <option value=\"2026-08-11\">2026-08-11</option>\n"
<< " <option value=\"2026-08-10\">2026-08-10</option>\n"
<< " <option value=\"2026-08-07\">2026-08-07</option>\n"
<< " <option value=\"2026-08-06\">2026-08-06</option>\n"
<< " <option value=\"2026-08-05\">2026-08-05</option>\n"
<< " <option value=\"2026-08-04\">2026-08-04</option>\n"
<< " <option value=\"2026-07-31\">2026-07-31</option>\n"
<< " <option value=\"2026-07-30\">2026-07-30</option>\n"
<< " </select>\n"
<< " </div>\n"
<< " </div>\n"
<< " <div class=\"mode-switch-bar\">\n"
<< " <button class=\"mode-btn btn-real active\" id=\"btnReal\" onclick=\"switchMode('real')\">\n"
<< " 📜 按钮 1:盘中真实实盘成交记录 (Alpaca Real History)\n"
<< " </button>\n"
<< " <button class=\"mode-btn btn-ml\" id=\"btnMl\" onclick=\"switchMode('ml')\">\n"
<< " 🚀 按钮 2:新升级 ML 策略仿真回测 (New ML Logic Simulation)\n"
<< " </button>\n"
<< " </div>\n"
<< " <div class=\"controls-row\">\n"
<< " <div class=\"ticker-pills\" id=\"tickerPills\"></div>\n"
<< " <div class=\"timeframe-pills\" id=\"tfPills\">\n"
<< " <button class=\"tf-pill active\" onclick=\"setTimeframe('1m')\">1M</button>\n"
<< " <button class=\"tf-pill\" onclick=\"setTimeframe('5m')\">5M</button>\n"
<< " <button class=\"tf-pill\" onclick=\"setTimeframe('15m')\">15M</button>\n"
<< " <button class=\"tf-pill\" onclick=\"setTimeframe('30m')\">30M</button>\n"
<< " </div>\n"
<< " </div>\n"
<< " <div class=\"section-box\">\n"
<< " <div id=\"plotlyChart\" style=\"width: 100%; height: 560px;\"></div>\n"
<< " </div>\n"
<< " <div class=\"section-box\">\n"
<< " <h3 id=\"ledgerTitle\">📋 买卖位置与持仓明细</h3>\n"
<< " <table>\n"
<< " <thead>\n"
<< " <tr>\n"
<< " <th>股票</th>\n"
<< " <th>方向</th>\n"
<< " <th>买入/做空时间</th>\n"
<< " <th>入场价</th>\n"
<< " <th>平仓时间</th>\n"
<< " <th>平仓价</th>\n"
<< " <th>持仓时长</th>\n"
<< " <th>成交股数</th>\n"
<< " <th>名义金额</th>\n"
<< " <th>净盈亏 ($)</th>\n"
<< " <th>收益率 (%)</th>\n"
<< " <th>离场原因</th>\n"
<< " </tr>\n"
<< " </thead>\n"
<< " <tbody id=\"ledgerBody\"></tbody>\n"
<< " </table>\n"
<< " </div>\n"
<< " <script>\n"
<< " const tickers = [\"SNDK\", \"TSLA\", \"MSTR\", \"NVDA\"];\n"
<< " let currentTicker = \"SNDK\";\n"
<< " let currentTimeframe = \"1m\";\n"
<< " let currentMode = \"real\";\n"
<< " let currentDate = \"" << today_str << "\";\n"
<< " const rawHistData = " << realTradesJson << ";\n"
<< " const realHistoryTrades = rawHistData.trade_history || rawHistData.trades || [];\n"
<< "\n"
<< " function switchMode(mode) {\n"
<< " currentMode = mode;\n"
<< " document.getElementById(\"btnReal\").classList.toggle(\"active\", mode === \"real\");\n"
<< " document.getElementById(\"btnMl\").classList.toggle(\"active\", mode === \"ml\");\n"
<< " renderChart();\n"
<< " renderLedger();\n"
<< " }\n"
<< "\n"
<< " function onDateChange(dateVal) {\n"
<< " currentDate = dateVal;\n"
<< " renderChart();\n"
<< " renderLedger();\n"
<< " }\n"
<< "\n"
<< " function getActiveTradeList() {\n"
<< " return realHistoryTrades.filter(t => {\n"
<< " const tDate = t.date || (t.entry_time || t.time || \"\").substring(0, 10);\n"
<< " return tDate === currentDate && t.ticker === currentTicker;\n"
<< " });\n"
<< " }\n"
<< "\n"
<< " function initPills() {\n"
<< " const container = document.getElementById(\"tickerPills\");\n"
<< " container.innerHTML = \"\";\n"
<< " tickers.forEach(tk => {\n"
<< " const btn = document.createElement(\"button\");\n"
<< " btn.className = \"ticker-pill \" + (tk === currentTicker ? \"active\" : \"\");\n"
<< " btn.innerText = tk;\n"
<< " btn.onclick = () => setTicker(tk);\n"
<< " container.appendChild(btn);\n"
<< " });\n"
<< " }\n"
<< "\n"
<< " function setTicker(tk) {\n"
<< " currentTicker = tk;\n"
<< " initPills();\n"
<< " renderChart();\n"
<< " renderLedger();\n"
<< " }\n"
<< "\n"
<< " function setTimeframe(tf) {\n"
<< " currentTimeframe = tf;\n"
<< " document.querySelectorAll(\".tf-pill\").forEach(btn => {\n"
<< " btn.classList.toggle(\"active\", btn.innerText === tf.toUpperCase());\n"
<< " });\n"
<< " renderChart();\n"
<< " }\n"
<< "\n"
<< " function round2(num) { return Math.round(num * 100) / 100; }\n"
<< "\n"
<< " function generateMockCandles() {\n"
<< " const times = [];\n"
<< " const opens = [], highs = [], lows = [], closes = [];\n"
<< " let basePrice = currentTicker === \"TSLA\" ? 346.5 : (currentTicker === \"SNDK\" ? 1500.0 : (currentTicker === \"MSTR\" ? 140.0 : 211.0));\n"
<< " for (let i = 0; i < 78; i++) {\n"
<< " const hour = Math.floor(9 + (30 + i * 5) / 60);\n"
<< " const min = (30 + i * 5) % 60;\n"
<< " const timeStr = (hour < 10 ? '0' + hour : hour) + ':' + (min < 10 ? '0' + min : min);\n"
<< " times.push(timeStr);\n"
<< " const change = (Math.sin(i / 5) * 1.8) + ((Math.random() - 0.48) * 2.2);\n"
<< " const open = basePrice;\n"
<< " const close = basePrice + change;\n"
<< " const high = Math.max(open, close) + Math.random() * 1.5;\n"
<< " const low = Math.min(open, close) - Math.random() * 1.5;\n"
<< " basePrice = close;\n"
<< " opens.push(round2(open)); highs.push(round2(high)); lows.push(round2(low)); closes.push(round2(close));\n"
<< " }\n"
<< " return { times, opens, highs, lows, closes };\n"
<< " }\n"
<< "\n"
<< " function renderChart() {\n"
<< " const data = generateMockCandles();\n"
<< " const candleTrace = {\n"
<< " x: data.times, open: data.opens, high: data.highs, low: data.lows, close: data.closes,\n"
<< " type: 'candlestick', name: currentTicker,\n"
<< " increasing: { line: { color: '#00c805', width: 1.5 }, fillcolor: '#00c805' },\n"
<< " decreasing: { line: { color: '#ff5000', width: 1.5 }, fillcolor: '#ff5000' }\n"
<< " };\n"
<< " const activeTrades = getActiveTradeList();\n"
<< " const buyX = [], buyY = [], buyText = [];\n"
<< " const shortX = [], shortY = [], shortText = [];\n"
<< " const exitX = [], exitY = [], exitText = [];\n"
<< " activeTrades.forEach(t => {\n"
<< " const enTime = (t.entry_time || t.time || \"\").split(\" \")[1] ? (t.entry_time || t.time || \"\").split(\" \")[1].substring(0, 5) : \"09:30\";\n"
<< " const exTime = (t.exit_time || t.time || \"\").split(\" \")[1] ? (t.exit_time || t.time || \"\").split(\" \")[1].substring(0, 5) : \"15:55\";\n"
<< " const entryP = t.entry_price || t.price || 0;\n"
<< " const exitP = t.exit_price || t.price || 0;\n"
<< " const pnlVal = t.pnl || 0;\n"
<< " if (t.side === \"LONG\" || t.action === \"BUY\") {\n"
<< " buyX.push(enTime); buyY.push(entryP);\n"
<< " buyText.push(`🛒 BUY LONG @ $${entryP.toFixed(2)} (${enTime})`);\n"
<< " } else {\n"
<< " shortX.push(enTime); shortY.push(entryP);\n"
<< " shortText.push(`📉 SHORT @ $${entryP.toFixed(2)} (${enTime})`);\n"
<< " }\n"
<< " if (exitP > 0) {\n"
<< " exitX.push(exTime); exitY.push(exitP);\n"
<< " exitText.push(`🔴 EXIT @ $${exitP.toFixed(2)} (${exTime}) | 盈亏: $${pnlVal.toFixed(2)}`);\n"
<< " }\n"
<< " });\n"
<< " const buyTrace = { x: buyX, y: buyY, mode: 'markers', name: '买入/建仓', marker: { symbol: 'triangle-up', size: 14, color: '#00c805' }, text: buyText, hoverinfo: 'text' };\n"
<< " const shortTrace = { x: shortX, y: shortY, mode: 'markers', name: '做空', marker: { symbol: 'triangle-down', size: 14, color: '#9333ea' }, text: shortText, hoverinfo: 'text' };\n"
<< " const exitTrace = { x: exitX, y: exitY, mode: 'markers', name: '平仓出局', marker: { symbol: 'x', size: 12, color: '#ff5000' }, text: exitText, hoverinfo: 'text' };\n"
<< " const layout = {\n"
<< " paper_bgcolor: '#ffffff', plot_bgcolor: '#ffffff',\n"
<< " margin: { l: 50, r: 30, t: 20, b: 40 },\n"
<< " xaxis: { rangeslider: { visible: false }, gridcolor: '#f0f3f5', linecolor: '#e1e8ed', tickfont: { color: '#536471' } },\n"
<< " yaxis: { gridcolor: '#f0f3f5', linecolor: '#e1e8ed', tickfont: { color: '#536471' } },\n"
<< " legend: { orientation: 'h', y: 1.15, x: 0.3, font: { color: '#0f1419' } }\n"
<< " };\n"
<< " Plotly.newPlot(\"plotlyChart\", [candleTrace, buyTrace, shortTrace, exitTrace], layout, { responsive: true });\n"
<< " }\n"
<< "\n"
<< " function renderLedger() {\n"
<< " const container = document.getElementById(\"ledgerBody\");\n"
<< " const title = document.getElementById(\"ledgerTitle\");\n"
<< " const modeText = currentMode === \"real\" ? \"📜 按钮 1:盘中真实实盘成交流水\" : \"🚀 按钮 2:新升级 ML 策略仿真回测\";\n"
<< " const activeTrades = getActiveTradeList();\n"
<< " title.innerText = \"📋 [\" + currentDate + \"] \" + currentTicker + \" - \" + modeText + \" (\" + activeTrades.length + \" 笔记录)\";\n"
<< " container.innerHTML = \"\";\n"
<< " if (activeTrades.length === 0) {\n"
<< " container.innerHTML = \"<tr><td colspan='12' style='text-align:center; color:#536471; padding:20px;'>\" + currentDate + \" \" + currentTicker + \" 在该模式下暂无历史成交记录</td></tr>\";\n"
<< " return;\n"
<< " }\n"
<< " activeTrades.forEach(t => {\n"
<< " const sideStr = t.side || t.action || \"LONG\";\n"
<< " const sideCls = (sideStr === \"LONG\" || sideStr === \"BUY\") ? \"badge-buy\" : \"badge-short\";\n"
<< " const pnlVal = t.pnl || 0;\n"
<< " const pnlPct = t.pnl_pct || 0;\n"
<< " const pnlCls = pnlVal >= 0 ? \"rh-green\" : \"rh-red\";\n"
<< " const enT = (t.entry_time || t.time || \"\").split(\" \")[1] ? (t.entry_time || t.time || \"\").split(\" \")[1].substring(0, 8) : \"09:30:00\";\n"
<< " const exT = (t.exit_time || t.time || \"\").split(\" \")[1] ? (t.exit_time || t.time || \"\").split(\" \")[1].substring(0, 8) : \"15:55:00\";\n"
<< " const shares = t.shares || t.qty || 10;\n"
<< " const entryP = t.entry_price || t.price || 0;\n"
<< " const exitP = t.exit_price || t.price || 0;\n"
<< " const tr = document.createElement(\"tr\");\n"
<< " tr.innerHTML = '<td><b>' + (t.ticker || currentTicker) + '</b></td>' +\n"
<< " '<td><span class=\"badge ' + sideCls + '\">' + sideStr + '</span></td>' +\n"
<< " '<td>' + enT + '</td>' +\n"
<< " '<td>$' + entryP.toFixed(2) + '</td>' +\n"
<< " '<td>' + exT + '</td>' +\n"
<< " '<td>$' + exitP.toFixed(2) + '</td>' +\n"
<< " '<td><b>' + (t.duration_min || 15) + ' 分钟</b></td>' +\n"
<< " '<td>' + shares + ' 股</td>' +\n"
<< " '<td>$' + (entryP * shares).toLocaleString('en-US', {maximumFractionDigits: 0}) + '</td>' +\n"
<< " '<td class=\"' + pnlCls + '\"><b>$' + (pnlVal >= 0 ? '+' : '') + pnlVal.toFixed(2) + '</b></td>' +\n"
<< " '<td class=\"' + pnlCls + '\"><b>' + (pnlPct >= 0 ? '+' : '') + pnlPct.toFixed(2) + '%</b></td>' +\n"
<< " '<td><span style=\"color:#536471;\">' + (t.reason || 'Signal Exit') + '</span></td>';\n"
<< " container.appendChild(tr);\n"
<< " });\n"
<< " }\n"
<< "\n"
<< " window.onload = () => {\n"
<< " initPills();\n"
<< " renderChart();\n"
<< " renderLedger();\n"
<< " };\n"
<< " </script>\n"
<< "</body>\n"
<< "</html>\n";
string outputPath = basePath + "data/charts/trade_comparison_dashboard.html";
ofstream outFile(outputPath);
if (outFile.is_open()) {
outFile << html.str();
outFile.close();
}
auto t1 = chrono::high_resolution_clock::now();
auto elapsed = chrono::duration_cast<chrono::microseconds>(t1 - t0).count();
cout << "✅ [C++ Engine] Successfully generated Dual-Mode K-line dashboard in " << elapsed / 1000.0 << " ms!" << endl;
return 0;
}