重构统计分析页:汇总指标、分项下拉与后台缓存

新增 stats_engine 与 stats_cache,提供 API 自动加载 8 种统计维度;交易与复盘变更时自动刷新缓存。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 16:46:06 +08:00
parent 0e385b057d
commit e8b4dbbaca
4 changed files with 581 additions and 179 deletions
+51 -68
View File
@@ -29,6 +29,7 @@ from fee_specs import (
)
from fee_sync import sync_fees_from_akshare
from contract_profile import get_contract_profile
from stats_engine import STATS_VIEWS, load_stats_cache, refresh_stats_cache
from kline_chart import generate_review_kline_chart
from market import get_price as market_get_price, set_ths_refresh_token, get_quote_source_label
@@ -174,6 +175,26 @@ def set_setting(key: str, value: str):
conn.close()
def touch_stats_cache():
try:
conn = get_db()
capital = float(get_setting("live_capital", "0") or 0)
refresh_stats_cache(conn, capital)
conn.close()
except Exception as exc:
app.logger.warning("stats cache refresh failed: %s", exc)
def get_stats_data() -> dict:
conn = get_db()
capital = float(get_setting("live_capital", "0") or 0)
data = load_stats_cache(conn)
if not data:
data = refresh_stats_cache(conn, capital)
conn.close()
return data
def init_db():
conn = get_db()
c = conn.cursor()
@@ -278,6 +299,10 @@ def init_db():
close_today_fixed REAL DEFAULT 0,
close_today_ratio REAL DEFAULT 0,
updated_at TEXT)''')
c.execute('''CREATE TABLE IF NOT EXISTS stats_cache
(key TEXT PRIMARY KEY,
data_json TEXT NOT NULL,
updated_at TEXT NOT NULL)''')
conn.commit()
conn.close()
@@ -904,6 +929,7 @@ def close_position(pid):
conn.execute("DELETE FROM position_monitors WHERE id=?", (pid,))
conn.commit()
conn.close()
touch_stats_cache()
flash(f"已平仓,盈亏 {pnl:.2f} 元(扣费后 {pnl_net:.2f} 元),已记入交易记录")
return redirect(url_for("positions"))
@@ -946,6 +972,7 @@ def update_trade(tid):
)
conn.commit()
conn.close()
touch_stats_cache()
flash("交易记录已核对保存")
return redirect(url_for("records"))
@@ -957,6 +984,7 @@ def del_trade(tid):
conn.execute("DELETE FROM trade_logs WHERE id=?", (tid,))
conn.commit()
conn.close()
touch_stats_cache()
flash("已删除")
return redirect(url_for("records"))
@@ -1179,6 +1207,7 @@ def add_review():
)
conn.commit()
conn.close()
touch_stats_cache()
flash("复盘记录已保存")
return redirect(url_for("records"))
@@ -1195,6 +1224,7 @@ def del_review(rid):
conn.execute("DELETE FROM review_records WHERE id=?", (rid,))
conn.commit()
conn.close()
touch_stats_cache()
flash("已删除")
return redirect(url_for("records"))
@@ -1220,76 +1250,29 @@ def del_record(rid):
@app.route("/stats")
@login_required
def stats():
return render_template("stats.html")
@app.route("/api/stats")
@login_required
def api_stats():
return jsonify(get_stats_data())
@app.route("/api/stats/views")
@login_required
def api_stats_views():
return jsonify({"views": STATS_VIEWS})
@app.route("/api/stats/refresh", methods=["POST"])
@login_required
def api_stats_refresh():
conn = get_db()
total = conn.execute(
"SELECT COUNT(*) FROM trade_records WHERE result IN ('止盈','止损')"
).fetchone()[0]
win = conn.execute(
"SELECT COUNT(*) FROM trade_records WHERE result='止盈'"
).fetchone()[0]
loss = conn.execute(
"SELECT COUNT(*) FROM trade_records WHERE result='止损'"
).fetchone()[0]
rate = round(win / total * 100, 2) if total else 0
by_symbol = conn.execute(
"""SELECT symbol_name, symbol, COUNT(*) as cnt,
SUM(CASE WHEN result='止盈' THEN 1 ELSE 0 END) as wins
FROM trade_records WHERE result IN ('止盈','止损')
GROUP BY symbol ORDER BY cnt DESC"""
).fetchall()
by_type = conn.execute(
"""SELECT monitor_type, COUNT(*) as cnt,
SUM(CASE WHEN result='止盈' THEN 1 ELSE 0 END) as wins
FROM trade_records WHERE result IN ('止盈','止损')
GROUP BY monitor_type ORDER BY cnt DESC"""
).fetchall()
by_direction = conn.execute(
"""SELECT direction, COUNT(*) as cnt,
SUM(CASE WHEN result='止盈' THEN 1 ELSE 0 END) as wins
FROM trade_records WHERE result IN ('止盈','止损')
GROUP BY direction"""
).fetchall()
recent = conn.execute(
"SELECT * FROM trade_logs ORDER BY id DESC LIMIT 10"
).fetchall()
fee_trade = conn.execute(
"SELECT COALESCE(SUM(fee),0), COALESCE(SUM(pnl),0), COALESCE(SUM(pnl_net),0), COUNT(*) "
"FROM trade_logs WHERE fee IS NOT NULL"
).fetchone()
fee_review = conn.execute(
"SELECT COALESCE(SUM(fee),0), COALESCE(SUM(pnl),0), COALESCE(SUM(pnl_net),0), COUNT(*) "
"FROM review_records WHERE fee IS NOT NULL"
).fetchone()
total_fee = round((fee_trade[0] or 0) + (fee_review[0] or 0), 2)
total_gross = round((fee_trade[1] or 0) + (fee_review[1] or 0), 2)
total_net = round((fee_trade[2] or 0) + (fee_review[2] or 0), 2)
fee_count = (fee_trade[3] or 0) + (fee_review[3] or 0)
fee_by_symbol = conn.execute(
"""SELECT symbol_name, symbol,
COALESCE(SUM(fee),0) as total_fee,
COUNT(*) as cnt
FROM trade_logs WHERE fee IS NOT NULL
GROUP BY symbol ORDER BY total_fee DESC LIMIT 20"""
).fetchall()
capital = float(get_setting("live_capital", "0") or 0)
data = refresh_stats_cache(conn, capital)
conn.close()
return render_template(
"stats.html",
total=total, win=win, loss=loss, rate=rate,
by_symbol=by_symbol, by_type=by_type, by_direction=by_direction,
recent=recent,
total_fee=total_fee,
total_gross=total_gross,
total_net=total_net,
fee_count=fee_count,
fee_by_symbol=fee_by_symbol,
)
return jsonify(data)
@app.route("/contract")