新增实例数据统计页:复盘 KPI 上、资金曲线下,系统设置可关。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1581,6 +1581,9 @@ def init_db():
|
|||||||
from lib.options.options_db import init_options_tables
|
from lib.options.options_db import init_options_tables
|
||||||
|
|
||||||
init_options_tables(conn)
|
init_options_tables(conn)
|
||||||
|
from lib.instance.instance_cash_sample_lib import init_cash_sample_table
|
||||||
|
|
||||||
|
init_cash_sample_table(conn)
|
||||||
from lib.sim.db_lib import init_sim_tables
|
from lib.sim.db_lib import init_sim_tables
|
||||||
init_sim_tables(conn)
|
init_sim_tables(conn)
|
||||||
from lib.trade.account_risk_lib import ensure_account_risk_schema
|
from lib.trade.account_risk_lib import ensure_account_risk_schema
|
||||||
@@ -4277,7 +4280,10 @@ def records_page():
|
|||||||
@app.route("/stats")
|
@app.route("/stats")
|
||||||
@login_required
|
@login_required
|
||||||
def stats_page():
|
def stats_page():
|
||||||
return redirect("/options")
|
redir = redirect_to_embed_shell_if_enabled("stats")
|
||||||
|
if redir is not None:
|
||||||
|
return redir
|
||||||
|
return render_main_page("stats")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/dashboard")
|
@app.route("/dashboard")
|
||||||
@@ -5730,6 +5736,10 @@ from lib.options.options_review_register import install_options_review
|
|||||||
|
|
||||||
install_options_review(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
install_options_review(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
|
|
||||||
|
from lib.instance.instance_data_stats_register import install_instance_data_stats
|
||||||
|
|
||||||
|
install_instance_data_stats(app, sys.modules[__name__])
|
||||||
|
|
||||||
from lib.hub.hub_bridge_register import register_hub_bridge_routes
|
from lib.hub.hub_bridge_register import register_hub_bridge_routes
|
||||||
|
|
||||||
register_hub_bridge_routes(app, sys.modules[__name__])
|
register_hub_bridge_routes(app, sys.modules[__name__])
|
||||||
|
|||||||
@@ -0,0 +1,345 @@
|
|||||||
|
/**
|
||||||
|
* 实例数据统计:复盘 KPI + 资金曲线.
|
||||||
|
*/
|
||||||
|
(function (global) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var currentRange = "7d";
|
||||||
|
var booted = false;
|
||||||
|
|
||||||
|
function $(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function root() {
|
||||||
|
return $("options-data-stats");
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s == null ? "" : s)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtPnl(v) {
|
||||||
|
if (v == null || v === "") return "—";
|
||||||
|
var n = Number(v);
|
||||||
|
if (Number.isNaN(n)) return "—";
|
||||||
|
return (n >= 0 ? "+" : "") + n.toFixed(2) + "U";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtHold(sec) {
|
||||||
|
if (sec == null) return "—";
|
||||||
|
var s = Math.max(0, Number(sec) || 0);
|
||||||
|
if (s < 3600) return Math.round(s / 60) + "m";
|
||||||
|
if (s < 86400) return (s / 3600).toFixed(1) + "h";
|
||||||
|
return (s / 86400).toFixed(1) + "d";
|
||||||
|
}
|
||||||
|
|
||||||
|
function pnlClass(v) {
|
||||||
|
var n = Number(v);
|
||||||
|
if (Number.isNaN(n) || n === 0) return "";
|
||||||
|
return n > 0 ? "ods-pnl-pos" : "ods-pnl-neg";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeReasonLabel(r) {
|
||||||
|
var map = {
|
||||||
|
perp_tp: "永续止盈",
|
||||||
|
perp_sl: "永续止损",
|
||||||
|
oo_expiry_loss: "期期到期亏损",
|
||||||
|
oo_expiry_win: "期期到期盈利",
|
||||||
|
target_win_leg: "期期平盈利腿",
|
||||||
|
profit_rr_win_leg: "期期盈亏比达标·平盈利腿",
|
||||||
|
};
|
||||||
|
return map[r] || String(r || "未填");
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceType() {
|
||||||
|
var el = root();
|
||||||
|
return (el && el.getAttribute("data-source-type")) || "option_spot";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(text) {
|
||||||
|
var el = $("ods-status");
|
||||||
|
if (el) el.textContent = text || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGroup(title, items) {
|
||||||
|
if (!items || !items.length) return "";
|
||||||
|
var lines = items
|
||||||
|
.slice(0, 8)
|
||||||
|
.map(function (g) {
|
||||||
|
var keyLabel =
|
||||||
|
title === "对冲结束原因" ? closeReasonLabel(g.key) : String(g.key || "");
|
||||||
|
return (
|
||||||
|
'<div class="ods-stat-row">' +
|
||||||
|
'<span class="ods-stat-key">' +
|
||||||
|
escapeHtml(keyLabel) +
|
||||||
|
" · " +
|
||||||
|
g.count +
|
||||||
|
"笔</span>" +
|
||||||
|
'<span class="ods-stat-val">' +
|
||||||
|
fmtPnl(g.pnl_sum) +
|
||||||
|
" / 胜" +
|
||||||
|
(g.win_rate || 0) +
|
||||||
|
"%</span>" +
|
||||||
|
"</div>"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
return (
|
||||||
|
'<div class="ods-stat-card"><div class="ods-stat-card-title">' +
|
||||||
|
title +
|
||||||
|
"</div>" +
|
||||||
|
lines +
|
||||||
|
"</div>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadReviewStats() {
|
||||||
|
var kpi = $("ods-kpi");
|
||||||
|
var groups = $("ods-stats-groups");
|
||||||
|
if (!kpi || !groups) return Promise.resolve();
|
||||||
|
var qs = new URLSearchParams();
|
||||||
|
qs.set("source_type", sourceType());
|
||||||
|
qs.set("sync", "0");
|
||||||
|
return fetch("/api/options/review/stats?" + qs.toString(), {
|
||||||
|
credentials: "same-origin",
|
||||||
|
})
|
||||||
|
.then(function (r) {
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(function (data) {
|
||||||
|
if (!data || !data.ok) {
|
||||||
|
kpi.innerHTML = '<div class="muted" style="font-size:.76rem">统计加载失败</div>';
|
||||||
|
groups.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var k = data.kpi || {};
|
||||||
|
kpi.innerHTML = [
|
||||||
|
["笔数", k.total],
|
||||||
|
["已复盘率", (k.review_rate || 0) + "%"],
|
||||||
|
["胜率", (k.win_rate || 0) + "%"],
|
||||||
|
["累计盈亏", fmtPnl(k.pnl_sum)],
|
||||||
|
["平均盈亏", fmtPnl(k.avg_pnl)],
|
||||||
|
["平均持有", fmtHold(k.avg_hold_sec)],
|
||||||
|
]
|
||||||
|
.map(function (pair) {
|
||||||
|
var cls = "";
|
||||||
|
if (pair[0] === "累计盈亏") cls = pnlClass(k.pnl_sum);
|
||||||
|
if (pair[0] === "平均盈亏") cls = pnlClass(k.avg_pnl);
|
||||||
|
return (
|
||||||
|
'<div class="ods-kpi-tile"><div class="ods-kpi-label">' +
|
||||||
|
pair[0] +
|
||||||
|
'</div><div class="ods-kpi-value' +
|
||||||
|
(cls ? " " + cls : "") +
|
||||||
|
'">' +
|
||||||
|
pair[1] +
|
||||||
|
"</div></div>"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
var html = [
|
||||||
|
renderGroup("按类型", data.by_source_type),
|
||||||
|
renderGroup("按标的", data.by_underlying),
|
||||||
|
renderGroup("按策略", data.by_strategy),
|
||||||
|
renderGroup("对冲结束原因", data.by_close_reason),
|
||||||
|
renderGroup("持有周期", data.by_hold_bucket),
|
||||||
|
renderGroup("Call/Put", data.by_opt_type),
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("");
|
||||||
|
groups.innerHTML =
|
||||||
|
html || '<div class="muted" style="font-size:.76rem">暂无分组数据</div>';
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
kpi.innerHTML = '<div class="muted" style="font-size:.76rem">统计加载失败</div>';
|
||||||
|
groups.innerHTML = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtCash(v) {
|
||||||
|
if (v == null || Number.isNaN(Number(v))) return "—";
|
||||||
|
return Number(v).toFixed(2) + "U";
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCurve(points) {
|
||||||
|
var svg = $("ods-curve-svg");
|
||||||
|
if (!svg) return;
|
||||||
|
var W = 640;
|
||||||
|
var H = 240;
|
||||||
|
var padL = 48;
|
||||||
|
var padR = 12;
|
||||||
|
var padT = 16;
|
||||||
|
var padB = 28;
|
||||||
|
var stroke = getComputedStyle(document.body).getPropertyValue("--accent") || "#5aa0ff";
|
||||||
|
stroke = (stroke || "").trim() || "#5aa0ff";
|
||||||
|
var grid = "rgba(127,127,127,.22)";
|
||||||
|
var muted = "rgba(160,160,180,.75)";
|
||||||
|
|
||||||
|
if (!points || !points.length) {
|
||||||
|
svg.innerHTML =
|
||||||
|
'<text x="320" y="120" text-anchor="middle" fill="' +
|
||||||
|
muted +
|
||||||
|
'" font-size="13">暂无采样数据,请稍后再看</text>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var vals = points.map(function (p) {
|
||||||
|
return Number(p.v);
|
||||||
|
});
|
||||||
|
var ts = points.map(function (p) {
|
||||||
|
return Number(p.t);
|
||||||
|
});
|
||||||
|
var minV = Math.min.apply(null, vals);
|
||||||
|
var maxV = Math.max.apply(null, vals);
|
||||||
|
if (maxV - minV < 1e-6) {
|
||||||
|
minV -= 1;
|
||||||
|
maxV += 1;
|
||||||
|
}
|
||||||
|
var minT = ts[0];
|
||||||
|
var maxT = ts[ts.length - 1];
|
||||||
|
if (maxT <= minT) maxT = minT + 1;
|
||||||
|
|
||||||
|
function xOf(t) {
|
||||||
|
return padL + ((t - minT) / (maxT - minT)) * (W - padL - padR);
|
||||||
|
}
|
||||||
|
function yOf(v) {
|
||||||
|
return padT + (1 - (v - minV) / (maxV - minV)) * (H - padT - padB);
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = points
|
||||||
|
.map(function (p, i) {
|
||||||
|
var x = xOf(p.t);
|
||||||
|
var y = yOf(p.v);
|
||||||
|
return (i ? "L" : "M") + x.toFixed(1) + "," + y.toFixed(1);
|
||||||
|
})
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
var yTicks = [minV, (minV + maxV) / 2, maxV];
|
||||||
|
var gridHtml = yTicks
|
||||||
|
.map(function (v) {
|
||||||
|
var y = yOf(v);
|
||||||
|
return (
|
||||||
|
'<line x1="' +
|
||||||
|
padL +
|
||||||
|
'" y1="' +
|
||||||
|
y.toFixed(1) +
|
||||||
|
'" x2="' +
|
||||||
|
(W - padR) +
|
||||||
|
'" y2="' +
|
||||||
|
y.toFixed(1) +
|
||||||
|
'" stroke="' +
|
||||||
|
grid +
|
||||||
|
'" stroke-width="1"/>' +
|
||||||
|
'<text x="' +
|
||||||
|
(padL - 6) +
|
||||||
|
'" y="' +
|
||||||
|
(y + 4).toFixed(1) +
|
||||||
|
'" text-anchor="end" fill="' +
|
||||||
|
muted +
|
||||||
|
'" font-size="10">' +
|
||||||
|
Number(v).toFixed(0) +
|
||||||
|
"</text>"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
var last = points[points.length - 1];
|
||||||
|
svg.innerHTML =
|
||||||
|
gridHtml +
|
||||||
|
'<path d="' +
|
||||||
|
path +
|
||||||
|
'" fill="none" stroke="' +
|
||||||
|
stroke +
|
||||||
|
'" stroke-width="2" vector-effect="non-scaling-stroke"/>' +
|
||||||
|
'<circle cx="' +
|
||||||
|
xOf(last.t).toFixed(1) +
|
||||||
|
'" cy="' +
|
||||||
|
yOf(last.v).toFixed(1) +
|
||||||
|
'" r="3.5" fill="' +
|
||||||
|
stroke +
|
||||||
|
'"/>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCashCurve() {
|
||||||
|
var meta = $("ods-curve-meta");
|
||||||
|
return fetch("/api/instance/cash_curve?range=" + encodeURIComponent(currentRange), {
|
||||||
|
credentials: "same-origin",
|
||||||
|
})
|
||||||
|
.then(function (r) {
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(function (data) {
|
||||||
|
if (!data || !data.ok) {
|
||||||
|
if (meta) meta.textContent = "曲线加载失败";
|
||||||
|
drawCurve([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var pts = data.points || [];
|
||||||
|
drawCurve(pts);
|
||||||
|
if (meta) {
|
||||||
|
meta.textContent =
|
||||||
|
"采样 " +
|
||||||
|
pts.length +
|
||||||
|
" 点 · 最新 " +
|
||||||
|
fmtCash(data.latest != null ? data.latest : pts.length ? pts[pts.length - 1].v : null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
if (meta) meta.textContent = "曲线加载失败";
|
||||||
|
drawCurve([]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadAll() {
|
||||||
|
setStatus("加载中…");
|
||||||
|
return Promise.all([loadReviewStats(), loadCashCurve()]).then(function () {
|
||||||
|
setStatus("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindOnce() {
|
||||||
|
if (booted || !root()) return;
|
||||||
|
booted = true;
|
||||||
|
var btn = $("ods-reload-btn");
|
||||||
|
if (btn) {
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
reloadAll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.querySelectorAll("#options-data-stats .ods-range-btn").forEach(function (b) {
|
||||||
|
b.addEventListener("click", function () {
|
||||||
|
currentRange = b.getAttribute("data-range") || "7d";
|
||||||
|
document.querySelectorAll("#options-data-stats .ods-range-btn").forEach(function (x) {
|
||||||
|
x.classList.toggle("active", x === b);
|
||||||
|
});
|
||||||
|
loadCashCurve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 默认 7 日更有曲线感;若用户点今日再切
|
||||||
|
var def7 = document.querySelector('#options-data-stats .ods-range-btn[data-range="7d"]');
|
||||||
|
if (def7) {
|
||||||
|
document.querySelectorAll("#options-data-stats .ods-range-btn").forEach(function (x) {
|
||||||
|
x.classList.toggle("active", x === def7);
|
||||||
|
});
|
||||||
|
currentRange = "7d";
|
||||||
|
}
|
||||||
|
reloadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bootIfPresent() {
|
||||||
|
if (root()) bindOnce();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", bootIfPresent);
|
||||||
|
} else {
|
||||||
|
bootIfPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// embed 软切 tab 时脚本可能再次插入
|
||||||
|
global.__OPTIONS_DATA_STATS_BOOT__ = bootIfPresent;
|
||||||
|
})(typeof window !== "undefined" ? window : this);
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
"""实例资金曲线采样:现金余额(不含权利金/浮盈),供数据统计页使用."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
|
_lock = threading.Lock()
|
||||||
|
_last_sample_at = 0.0
|
||||||
|
_thread: threading.Thread | None = None
|
||||||
|
_stop = threading.Event()
|
||||||
|
|
||||||
|
|
||||||
|
def cash_sample_interval_sec() -> float:
|
||||||
|
try:
|
||||||
|
return max(30.0, float(os.getenv("INSTANCE_CASH_SAMPLE_SEC") or 60))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 60.0
|
||||||
|
|
||||||
|
|
||||||
|
def cash_sample_keep_days() -> int:
|
||||||
|
try:
|
||||||
|
return max(7, int(float(os.getenv("INSTANCE_CASH_SAMPLE_KEEP_DAYS") or 90)))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 90
|
||||||
|
|
||||||
|
|
||||||
|
def init_cash_sample_table(conn) -> None:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS instance_cash_samples (
|
||||||
|
ts_ms INTEGER PRIMARY KEY,
|
||||||
|
cash_usdt REAL NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_float(v: Any) -> Optional[float]:
|
||||||
|
if v is None or v == "":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def cash_usdt_from_balances(bal: dict[str, Any] | None) -> Optional[float]:
|
||||||
|
"""现金 = 交易账户 + 资金账户(优先 USDT,否则 USDC);不含权利金/浮盈."""
|
||||||
|
if not isinstance(bal, dict):
|
||||||
|
return None
|
||||||
|
parts = [_safe_float(bal.get("trading_usdt")), _safe_float(bal.get("funding_usdt"))]
|
||||||
|
if all(x is None for x in parts):
|
||||||
|
parts = [_safe_float(bal.get("trading_usdc")), _safe_float(bal.get("funding_usdc"))]
|
||||||
|
known = [x for x in parts if x is not None]
|
||||||
|
if not known:
|
||||||
|
return None
|
||||||
|
return round(sum(known), 4)
|
||||||
|
|
||||||
|
|
||||||
|
def record_cash_sample(conn, cash_usdt: float, *, ts_ms: int | None = None) -> None:
|
||||||
|
init_cash_sample_table(conn)
|
||||||
|
t = int(ts_ms if ts_ms is not None else time.time() * 1000)
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO instance_cash_samples(ts_ms, cash_usdt)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT(ts_ms) DO UPDATE SET cash_usdt=excluded.cash_usdt
|
||||||
|
""",
|
||||||
|
(t, float(cash_usdt)),
|
||||||
|
)
|
||||||
|
keep_ms = cash_sample_keep_days() * 86400 * 1000
|
||||||
|
cutoff = t - keep_ms
|
||||||
|
conn.execute("DELETE FROM instance_cash_samples WHERE ts_ms < ?", (cutoff,))
|
||||||
|
|
||||||
|
|
||||||
|
def _range_start_ms(range_key: str) -> tuple[str, int]:
|
||||||
|
rk = (range_key or "7d").strip().lower()
|
||||||
|
now_ms = int(time.time() * 1000)
|
||||||
|
if rk in ("today", "1d"):
|
||||||
|
start = dt.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
return "today", int(start.timestamp() * 1000)
|
||||||
|
if rk in ("30d", "30"):
|
||||||
|
return "30d", now_ms - 30 * 86400 * 1000
|
||||||
|
if rk in ("all", "0"):
|
||||||
|
return "all", 0
|
||||||
|
return "7d", now_ms - 7 * 86400 * 1000
|
||||||
|
|
||||||
|
|
||||||
|
def query_cash_series_with_db(conn, *, range_key: str = "7d") -> dict[str, Any]:
|
||||||
|
rk, start_ms = _range_start_ms(range_key)
|
||||||
|
now_ms = int(time.time() * 1000)
|
||||||
|
init_cash_sample_table(conn)
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT ts_ms, cash_usdt
|
||||||
|
FROM instance_cash_samples
|
||||||
|
WHERE ts_ms >= ?
|
||||||
|
ORDER BY ts_ms ASC
|
||||||
|
""",
|
||||||
|
(start_ms,),
|
||||||
|
).fetchall()
|
||||||
|
points = [{"t": int(r["ts_ms"]), "v": float(r["cash_usdt"])} for r in rows]
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"range": rk,
|
||||||
|
"points": points,
|
||||||
|
"start_ms": start_ms,
|
||||||
|
"end_ms": now_ms,
|
||||||
|
"latest": points[-1]["v"] if points else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sample_once(get_db: Callable, fetch_balances: Callable) -> bool:
|
||||||
|
"""拉取余额并写入采样;成功返回 True."""
|
||||||
|
global _last_sample_at
|
||||||
|
try:
|
||||||
|
bal = fetch_balances()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
cash = cash_usdt_from_balances(bal if isinstance(bal, dict) else None)
|
||||||
|
if cash is None:
|
||||||
|
return False
|
||||||
|
with _lock:
|
||||||
|
conn = get_db()
|
||||||
|
try:
|
||||||
|
record_cash_sample(conn, cash)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
_last_sample_at = time.time()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def start_cash_sample_loop(get_db: Callable, fetch_balances: Callable) -> None:
|
||||||
|
global _thread
|
||||||
|
if _thread and _thread.is_alive():
|
||||||
|
return
|
||||||
|
_stop.clear()
|
||||||
|
|
||||||
|
def _loop() -> None:
|
||||||
|
# 启动后立刻采一笔,避免曲线长期空白
|
||||||
|
try:
|
||||||
|
sample_once(get_db, fetch_balances)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
while not _stop.is_set():
|
||||||
|
if _stop.wait(cash_sample_interval_sec()):
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
sample_once(get_db, fetch_balances)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
_thread = threading.Thread(target=_loop, daemon=True, name="instance-cash-sample")
|
||||||
|
_thread.start()
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""实例数据统计:复盘 KPI + 资金曲线 API,以及后台现金采样."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from flask import Flask, jsonify, request
|
||||||
|
|
||||||
|
|
||||||
|
def install_instance_data_stats(app: Flask, app_module: Any) -> None:
|
||||||
|
get_db: Callable = app_module.get_db
|
||||||
|
login_required = app_module.login_required
|
||||||
|
|
||||||
|
def _fetch_balances() -> dict[str, Any]:
|
||||||
|
ex = getattr(app_module, "exchange_options", None)
|
||||||
|
if ex is None:
|
||||||
|
return {}
|
||||||
|
from lib.exchange.okx_options_lib import fetch_options_balances
|
||||||
|
|
||||||
|
return fetch_options_balances(ex, force=False) or {}
|
||||||
|
|
||||||
|
@app.route("/api/instance/cash_curve")
|
||||||
|
@login_required
|
||||||
|
def api_instance_cash_curve():
|
||||||
|
from lib.instance.instance_cash_sample_lib import (
|
||||||
|
query_cash_series_with_db,
|
||||||
|
sample_once,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 打开页面时尽量补一笔最新点(失败忽略)
|
||||||
|
try:
|
||||||
|
sample_once(get_db, _fetch_balances)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
range_key = (request.args.get("range") or "7d").strip()
|
||||||
|
conn = get_db()
|
||||||
|
try:
|
||||||
|
return jsonify(query_cash_series_with_db(conn, range_key=range_key))
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"ok": False, "msg": f"加载资金曲线失败: {e}"}), 500
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 后台采样(daemon);无密钥时会空转跳过
|
||||||
|
try:
|
||||||
|
from lib.instance.instance_cash_sample_lib import start_cash_sample_loop
|
||||||
|
|
||||||
|
start_cash_sample_loop(get_db, _fetch_balances)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[instance_cash_sample] start skipped: {e}", flush=True)
|
||||||
@@ -15,7 +15,7 @@ DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
|
|||||||
"show_nav_strategy": False,
|
"show_nav_strategy": False,
|
||||||
"show_nav_strategy_records": False,
|
"show_nav_strategy_records": False,
|
||||||
"show_nav_records": False, # 永续交易记录与复盘已移除
|
"show_nav_records": False, # 永续交易记录与复盘已移除
|
||||||
"show_nav_stats": False,
|
"show_nav_stats": True, # 数据统计(复盘 KPI + 资金曲线);可在系统设置关闭
|
||||||
"show_nav_risk_policy": True,
|
"show_nav_risk_policy": True,
|
||||||
"show_nav_live_guide": True,
|
"show_nav_live_guide": True,
|
||||||
"show_nav_system_guide": False,
|
"show_nav_system_guide": False,
|
||||||
@@ -37,7 +37,7 @@ DISPLAY_LABELS: dict[str, str] = {
|
|||||||
"show_nav_strategy": "策略交易",
|
"show_nav_strategy": "策略交易",
|
||||||
"show_nav_strategy_records": "策略交易记录",
|
"show_nav_strategy_records": "策略交易记录",
|
||||||
"show_nav_records": "交易记录与复盘",
|
"show_nav_records": "交易记录与复盘",
|
||||||
"show_nav_stats": "统计分析",
|
"show_nav_stats": "数据统计",
|
||||||
"show_nav_risk_policy": "风控说明",
|
"show_nav_risk_policy": "风控说明",
|
||||||
"show_nav_live_guide": "实盘说明",
|
"show_nav_live_guide": "实盘说明",
|
||||||
"show_nav_system_guide": "系统说明",
|
"show_nav_system_guide": "系统说明",
|
||||||
@@ -111,8 +111,8 @@ def display_prefs_template_context(get_db: Callable) -> dict[str, Any]:
|
|||||||
def tab_allowed(tab: str, display: Optional[dict[str, bool]] = None) -> bool:
|
def tab_allowed(tab: str, display: Optional[dict[str, bool]] = None) -> bool:
|
||||||
prefs = normalize_display_prefs(display or {})
|
prefs = normalize_display_prefs(display or {})
|
||||||
t = (tab or "").strip()
|
t = (tab or "").strip()
|
||||||
if t in ("trade", "records", "stats", "hedge_plan"):
|
if t in ("trade", "records", "hedge_plan"):
|
||||||
return False # 永续下单/记录/统计已下线;关键位监控保留
|
return False # 永续下单/记录已下线;关键位监控保留
|
||||||
key = NAV_TAB_ALLOWED.get(t)
|
key = NAV_TAB_ALLOWED.get(t)
|
||||||
if not key:
|
if not key:
|
||||||
return True
|
return True
|
||||||
@@ -124,6 +124,7 @@ def display_meta_for_ui() -> list[dict[str, Any]]:
|
|||||||
"show_nav_dashboard",
|
"show_nav_dashboard",
|
||||||
"show_nav_account_ledger",
|
"show_nav_account_ledger",
|
||||||
"show_nav_key_monitor",
|
"show_nav_key_monitor",
|
||||||
|
"show_nav_stats",
|
||||||
"show_nav_risk_policy",
|
"show_nav_risk_policy",
|
||||||
"show_nav_live_guide",
|
"show_nav_live_guide",
|
||||||
"show_nav_system_guide",
|
"show_nav_system_guide",
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def embed_render_plan(page: str, embed_mode: str | None) -> EmbedRenderPlan:
|
|||||||
key_history=page == "key_monitor",
|
key_history=page == "key_monitor",
|
||||||
key_list=page in ("key_monitor", "trade") or is_strategy,
|
key_list=page in ("key_monitor", "trade") or is_strategy,
|
||||||
orders=page == "trade" or is_strategy,
|
orders=page == "trade" or is_strategy,
|
||||||
stats_bundle=page == "stats",
|
stats_bundle=False, # 旧永续统计已弃用;数据统计页走复盘 API + 资金曲线
|
||||||
strategy=is_strategy,
|
strategy=is_strategy,
|
||||||
orphan_live=page == "trade" and is_shell,
|
orphan_live=page == "trade" and is_shell,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ EMBED_TABS: tuple[str, ...] = (
|
|||||||
"key_monitor",
|
"key_monitor",
|
||||||
"options",
|
"options",
|
||||||
"options_review",
|
"options_review",
|
||||||
|
"stats",
|
||||||
"risk_policy",
|
"risk_policy",
|
||||||
"live_guide",
|
"live_guide",
|
||||||
"system_guide",
|
"system_guide",
|
||||||
@@ -27,7 +28,7 @@ PATH_TO_EMBED_TAB: dict[str, str] = {
|
|||||||
"/": "options",
|
"/": "options",
|
||||||
"/trade": "options", # 实盘下单界面已移除
|
"/trade": "options", # 实盘下单界面已移除
|
||||||
"/records": "options_review", # 永续交易记录与复盘已移除
|
"/records": "options_review", # 永续交易记录与复盘已移除
|
||||||
"/stats": "options",
|
"/stats": "stats",
|
||||||
"/dashboard": "dashboard",
|
"/dashboard": "dashboard",
|
||||||
"/account_ledger": "account_ledger",
|
"/account_ledger": "account_ledger",
|
||||||
"/key_monitor": "key_monitor",
|
"/key_monitor": "key_monitor",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ _COMMON_STATIC_ASSETS: dict[str, str] = {
|
|||||||
"options_settings.js": "application/javascript; charset=utf-8",
|
"options_settings.js": "application/javascript; charset=utf-8",
|
||||||
"options_position_cards.js": "application/javascript; charset=utf-8",
|
"options_position_cards.js": "application/javascript; charset=utf-8",
|
||||||
"options_review.js": "application/javascript; charset=utf-8",
|
"options_review.js": "application/javascript; charset=utf-8",
|
||||||
|
"options_data_stats.js": "application/javascript; charset=utf-8",
|
||||||
"order_entry_model.js": "application/javascript; charset=utf-8",
|
"order_entry_model.js": "application/javascript; charset=utf-8",
|
||||||
"focus_chart_page.js": "application/javascript; charset=utf-8",
|
"focus_chart_page.js": "application/javascript; charset=utf-8",
|
||||||
"focus_chart_page.css": "text/css; charset=utf-8",
|
"focus_chart_page.css": "text/css; charset=utf-8",
|
||||||
|
|||||||
@@ -127,6 +127,8 @@
|
|||||||
{% include 'options_panel.html' %}
|
{% include 'options_panel.html' %}
|
||||||
{% elif page == 'options_review' %}
|
{% elif page == 'options_review' %}
|
||||||
{% include 'options_review_panel.html' %}
|
{% include 'options_review_panel.html' %}
|
||||||
|
{% elif page == 'stats' %}
|
||||||
|
{% include 'options_data_stats_panel.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
{% if options_nav_visible and display.show_nav_options_review %}
|
{% if options_nav_visible and display.show_nav_options_review %}
|
||||||
<a href="/options/review" data-embed-tab="options_review" class="{% if initial_tab == 'options_review' %}active{% endif %}">期权复盘</a>
|
<a href="/options/review" data-embed-tab="options_review" class="{% if initial_tab == 'options_review' %}active{% endif %}">期权复盘</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a href="/stats" data-embed-tab="stats" class="{% if initial_tab == 'stats' %}active{% endif %}"{% if not display.show_nav_stats %} style="display:none"{% endif %}>数据统计</a>
|
||||||
{% if display.show_nav_risk_policy %}
|
{% if display.show_nav_risk_policy %}
|
||||||
<a href="/risk_policy" data-embed-tab="risk_policy" class="{% if initial_tab == 'risk_policy' %}active{% endif %}">风控说明</a>
|
<a href="/risk_policy" data-embed-tab="risk_policy" class="{% if initial_tab == 'risk_policy' %}active{% endif %}">风控说明</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -66,7 +67,7 @@
|
|||||||
<div id="embed-flash" class="flash" style="display:none" role="status"></div>
|
<div id="embed-flash" class="flash" style="display:none" role="status"></div>
|
||||||
|
|
||||||
{% include 'instance_header_panel.html' %}
|
{% include 'instance_header_panel.html' %}
|
||||||
{% if initial_tab not in ('settings', 'risk_policy', 'live_guide', 'system_guide', 'env_config', 'options_review') and include_transfer_block %}
|
{% if initial_tab not in ('settings', 'risk_policy', 'live_guide', 'system_guide', 'env_config', 'options_review', 'stats') and include_transfer_block %}
|
||||||
{% include 'instance_top_bar.html' %}
|
{% include 'instance_top_bar.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -113,6 +114,7 @@
|
|||||||
{% if options_nav_visible and display.show_nav_options_review %}
|
{% if options_nav_visible and display.show_nav_options_review %}
|
||||||
<a href="/options/review" data-embed-tab="options_review">期权复盘</a>
|
<a href="/options/review" data-embed-tab="options_review">期权复盘</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a href="/stats" data-embed-tab="stats"{% if not display.show_nav_stats %} style="display:none"{% endif %}>数据统计</a>
|
||||||
{% if display.show_nav_risk_policy %}
|
{% if display.show_nav_risk_policy %}
|
||||||
<a href="/risk_policy" data-embed-tab="risk_policy">风控说明</a>
|
<a href="/risk_policy" data-embed-tab="risk_policy">风控说明</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -168,15 +168,13 @@
|
|||||||
{% if display.show_nav_records %}
|
{% if display.show_nav_records %}
|
||||||
<a href="/records" data-embed-tab="records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
|
<a href="/records" data-embed-tab="records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if display.show_nav_stats %}
|
|
||||||
<a href="/stats" data-embed-tab="stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if options_nav_visible and display.show_nav_options %}
|
{% if options_nav_visible and display.show_nav_options %}
|
||||||
<a href="/options" data-embed-tab="options" class="{% if page == 'options' %}active{% endif %}">期权</a>
|
<a href="/options" data-embed-tab="options" class="{% if page == 'options' %}active{% endif %}">期权</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if options_nav_visible and display.show_nav_options_review %}
|
{% if options_nav_visible and display.show_nav_options_review %}
|
||||||
<a href="/options/review" data-embed-tab="options_review" class="{% if page == 'options_review' %}active{% endif %}">期权复盘</a>
|
<a href="/options/review" data-embed-tab="options_review" class="{% if page == 'options_review' %}active{% endif %}">期权复盘</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a href="/stats" data-embed-tab="stats" class="{% if page == 'stats' %}active{% endif %}"{% if not display.show_nav_stats %} style="display:none"{% endif %}>数据统计</a>
|
||||||
{% if display.show_nav_risk_policy %}
|
{% if display.show_nav_risk_policy %}
|
||||||
<a href="/risk_policy" data-embed-tab="risk_policy" class="{% if page == 'risk_policy' %}active{% endif %}">风控说明</a>
|
<a href="/risk_policy" data-embed-tab="risk_policy" class="{% if page == 'risk_policy' %}active{% endif %}">风控说明</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -194,7 +192,7 @@
|
|||||||
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
|
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
|
||||||
|
|
||||||
{% include 'instance_header_panel.html' %}
|
{% include 'instance_header_panel.html' %}
|
||||||
{% if page not in ('settings', 'risk_policy', 'live_guide', 'system_guide', 'env_config', 'options', 'options_review') %}
|
{% if page not in ('settings', 'risk_policy', 'live_guide', 'system_guide', 'env_config', 'options', 'options_review', 'stats') %}
|
||||||
{% include 'instance_top_bar.html' %}
|
{% include 'instance_top_bar.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -359,6 +357,8 @@
|
|||||||
{% include 'options_panel.html' %}
|
{% include 'options_panel.html' %}
|
||||||
{% elif page == 'options_review' %}
|
{% elif page == 'options_review' %}
|
||||||
{% include 'options_review_panel.html' %}
|
{% include 'options_review_panel.html' %}
|
||||||
|
{% elif page == 'stats' %}
|
||||||
|
{% include 'options_data_stats_panel.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
@@ -387,44 +387,6 @@
|
|||||||
{% include 'settings_panel.html' %}
|
{% include 'settings_panel.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if page == 'stats' %}
|
|
||||||
<div class="card stats-card full" id="stats-card">
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">
|
|
||||||
<h2 style="margin-bottom:0">数据统计</h2>
|
|
||||||
<button type="button" class="stats-toggle" id="stats-toggle-btn" onclick="toggleStatsCard()">折叠</button>
|
|
||||||
</div>
|
|
||||||
<div class="stats-content" id="stats-content">
|
|
||||||
<div class="sub" style="margin-bottom:12px;color:#8892b0;font-size:.82rem">
|
|
||||||
统计分析按<strong>北京时间 {{ stats_bundle.stats_reset_hour }}:00</strong>切日计入(与顶栏 UTC 列表窗无关).历史总开仓(累计):
|
|
||||||
<strong style="color:#cfd3ef">{{ stats_bundle.total_opens_all }}</strong> 次
|
|
||||||
</div>
|
|
||||||
<div class="form-row" style="margin-bottom:14px;align-items:center">
|
|
||||||
<label style="display:flex;align-items:center;gap:8px;font-size:.88rem;color:#cfd3ef">
|
|
||||||
统计品类
|
|
||||||
<select id="stats-segment-select" onchange="switchStatsSegment()" style="min-width:200px">
|
|
||||||
{% for seg in stats_bundle.segments %}
|
|
||||||
<option value="{{ seg.key }}">{{ seg.title }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{% for seg in stats_bundle.segments %}
|
|
||||||
<div class="stats-segment-block stats-segment-panel" data-stats-segment="{{ seg.key }}"{% if not loop.first %} style="display:none"{% endif %}>
|
|
||||||
<div class="stats-period-tabs" role="tablist" aria-label="统计周期">
|
|
||||||
<button type="button" class="stats-period-tab active" data-stats-period="day" role="tab" aria-selected="true" onclick="switchStatsPeriod('day')">日统计</button>
|
|
||||||
<button type="button" class="stats-period-tab" data-stats-period="week" role="tab" aria-selected="false" onclick="switchStatsPeriod('week')">周统计</button>
|
|
||||||
<button type="button" class="stats-period-tab" data-stats-period="month" role="tab" aria-selected="false" onclick="switchStatsPeriod('month')">月统计</button>
|
|
||||||
<button type="button" class="stats-period-tab" data-stats-period="all" role="tab" aria-selected="false" onclick="switchStatsPeriod('all')">全部统计</button>
|
|
||||||
</div>
|
|
||||||
{{ period_stats_pane("day", seg.day) }}
|
|
||||||
{{ period_stats_pane("week", seg.week) }}
|
|
||||||
{{ period_stats_pane("month", seg.month) }}
|
|
||||||
{{ period_stats_pane("all", seg.all) }}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal" id="imgModal" onclick="closeModal()">
|
<div class="modal" id="imgModal" onclick="closeModal()">
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
{# 实例数据统计:上方期权复盘 KPI/分组,下方资金曲线 #}
|
||||||
|
<div class="card ods-page full" id="options-data-stats" data-source-type="option_spot">
|
||||||
|
<style>
|
||||||
|
.ods-page{grid-column:1/-1}
|
||||||
|
.ods-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:14px}
|
||||||
|
.ods-head h2{margin:0;font-size:1.05rem}
|
||||||
|
.ods-section{margin-bottom:22px}
|
||||||
|
.ods-section-title{font-weight:650;font-size:.9rem;margin-bottom:4px}
|
||||||
|
.ods-section-desc{font-size:.72rem;opacity:.7;margin:0 0 12px}
|
||||||
|
.ods-kpi-row{
|
||||||
|
display:grid;grid-template-columns:repeat(6,minmax(0,1fr));
|
||||||
|
gap:8px;margin-bottom:12px;
|
||||||
|
}
|
||||||
|
.ods-kpi-tile{
|
||||||
|
border:1px solid rgba(127,127,127,.22);border-radius:10px;
|
||||||
|
padding:10px 12px;background:rgba(0,0,0,.2);min-width:0;
|
||||||
|
}
|
||||||
|
.ods-kpi-label{font-size:.7rem;opacity:.7;margin-bottom:4px}
|
||||||
|
.ods-kpi-value{font-size:.95rem;font-weight:650;letter-spacing:.01em;word-break:break-all}
|
||||||
|
.ods-stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}
|
||||||
|
.ods-stat-card{
|
||||||
|
border:1px solid rgba(127,127,127,.22);border-radius:10px;
|
||||||
|
padding:10px 12px;background:rgba(0,0,0,.16);font-size:.76rem;
|
||||||
|
}
|
||||||
|
.ods-stat-card-title{
|
||||||
|
font-weight:650;margin-bottom:8px;font-size:.78rem;
|
||||||
|
padding-bottom:6px;border-bottom:1px solid rgba(127,127,127,.18);
|
||||||
|
}
|
||||||
|
.ods-stat-row{
|
||||||
|
display:flex;justify-content:space-between;align-items:baseline;gap:10px;
|
||||||
|
padding:5px 0;border-bottom:1px solid rgba(127,127,127,.1);
|
||||||
|
}
|
||||||
|
.ods-stat-row:last-child{border-bottom:none;padding-bottom:0}
|
||||||
|
.ods-stat-key{opacity:.9;min-width:0;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.ods-stat-val{flex-shrink:0;font-variant-numeric:tabular-nums;opacity:.85}
|
||||||
|
.ods-curve-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px}
|
||||||
|
.ods-range-btn{
|
||||||
|
font-size:.76rem;padding:4px 10px;border-radius:6px;cursor:pointer;
|
||||||
|
border:1px solid rgba(127,127,127,.3);background:transparent;color:inherit;
|
||||||
|
}
|
||||||
|
.ods-range-btn.active{border-color:rgba(90,160,255,.7);background:rgba(90,160,255,.12)}
|
||||||
|
.ods-curve-meta{font-size:.72rem;opacity:.75;margin-left:auto}
|
||||||
|
.ods-curve-wrap{
|
||||||
|
border:1px solid rgba(127,127,127,.22);border-radius:10px;
|
||||||
|
padding:12px 10px 8px;background:rgba(0,0,0,.14);
|
||||||
|
}
|
||||||
|
.ods-curve-svg{width:100%;height:240px;display:block}
|
||||||
|
.ods-pnl-pos{color:#3dd68c}
|
||||||
|
.ods-pnl-neg{color:#f07178}
|
||||||
|
@media (max-width:900px){.ods-kpi-row{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
||||||
|
@media (max-width:640px){
|
||||||
|
.ods-kpi-row{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||||
|
.ods-curve-svg{height:200px}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="ods-head">
|
||||||
|
<h2>数据统计</h2>
|
||||||
|
<span class="muted" id="ods-status" style="font-size:.72rem"></span>
|
||||||
|
<button type="button" class="btn-secondary" id="ods-reload-btn" style="font-size:.76rem;padding:4px 10px">刷新</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="ods-section" aria-labelledby="ods-review-heading">
|
||||||
|
<div class="ods-section-title" id="ods-review-heading">期权复盘统计</div>
|
||||||
|
<p class="ods-section-desc">与「期权复盘」页统计同源;复盘页内统计仍保留。</p>
|
||||||
|
<div id="ods-kpi" class="ods-kpi-row"></div>
|
||||||
|
<div id="ods-stats-groups" class="ods-stats-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="ods-section" aria-labelledby="ods-curve-heading">
|
||||||
|
<div class="ods-section-title" id="ods-curve-heading">资金曲线</div>
|
||||||
|
<p class="ods-section-desc">现金余额(交易账户+资金账户,不含权利金与浮盈);后台定时采样。</p>
|
||||||
|
<div class="ods-curve-toolbar">
|
||||||
|
<button type="button" class="ods-range-btn active" data-range="today">今日</button>
|
||||||
|
<button type="button" class="ods-range-btn" data-range="7d">7日</button>
|
||||||
|
<button type="button" class="ods-range-btn" data-range="30d">30日</button>
|
||||||
|
<button type="button" class="ods-range-btn" data-range="all">全部</button>
|
||||||
|
<span class="ods-curve-meta" id="ods-curve-meta">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="ods-curve-wrap">
|
||||||
|
<svg id="ods-curve-svg" class="ods-curve-svg" viewBox="0 0 640 240" preserveAspectRatio="none" role="img" aria-label="资金曲线"></svg>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<script src="/static/options_data_stats.js?v=1"></script>
|
||||||
Reference in New Issue
Block a user