本地手续费配置(标准×2),持仓/交易记录/复盘/统计展示扣费后盈亏

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 15:22:40 +08:00
parent 9ba9733523
commit bea7804d47
11 changed files with 669 additions and 20 deletions
+137 -8
View File
@@ -19,6 +19,15 @@ from werkzeug.security import check_password_hash, generate_password_hash
from symbols import search_symbols, ths_to_codes from symbols import search_symbols, ths_to_codes
from contract_specs import calc_position_metrics from contract_specs import calc_position_metrics
from fee_specs import (
calc_fee_breakdown,
calc_round_trip_fee,
get_fee_multiplier,
list_all_fee_rates,
load_fee_rates_from_json,
upsert_fee_rate,
)
from fee_sync import sync_fees_from_akshare
from kline_chart import generate_review_kline_chart 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 from market import get_price as market_get_price, set_ths_refresh_token, get_quote_source_label
@@ -216,6 +225,10 @@ def init_db():
"ALTER TABLE review_records ADD COLUMN symbol_name TEXT", "ALTER TABLE review_records ADD COLUMN symbol_name TEXT",
"ALTER TABLE review_records ADD COLUMN market_code TEXT", "ALTER TABLE review_records ADD COLUMN market_code TEXT",
"ALTER TABLE review_records ADD COLUMN sina_code TEXT", "ALTER TABLE review_records ADD COLUMN sina_code TEXT",
"ALTER TABLE trade_logs ADD COLUMN fee REAL",
"ALTER TABLE trade_logs ADD COLUMN pnl_net REAL",
"ALTER TABLE review_records ADD COLUMN fee REAL",
"ALTER TABLE review_records ADD COLUMN pnl_net REAL",
] ]
for sql in migrations: for sql in migrations:
try: try:
@@ -253,6 +266,17 @@ def init_db():
pnl REAL, result TEXT, pnl REAL, result TEXT,
verified INTEGER DEFAULT 0, verified INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''') created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
c.execute('''CREATE TABLE IF NOT EXISTS fee_rates
(product TEXT PRIMARY KEY,
exchange TEXT,
mult INTEGER,
open_fixed REAL DEFAULT 0,
open_ratio REAL DEFAULT 0,
close_yesterday_fixed REAL DEFAULT 0,
close_yesterday_ratio REAL DEFAULT 0,
close_today_fixed REAL DEFAULT 0,
close_today_ratio REAL DEFAULT 0,
updated_at TEXT)''')
conn.commit() conn.commit()
conn.close() conn.close()
@@ -267,6 +291,14 @@ def init_db():
os.makedirs(UPLOAD_DIR, exist_ok=True) os.makedirs(UPLOAD_DIR, exist_ok=True)
expire_old_plans() expire_old_plans()
if not get_setting("fee_multiplier"):
set_setting("fee_multiplier", "2")
conn = get_db()
fee_cnt = conn.execute("SELECT COUNT(*) FROM fee_rates").fetchone()[0]
conn.close()
if fee_cnt == 0:
load_fee_rates_from_json()
def sync_admin_from_env(): def sync_admin_from_env():
""" """
@@ -616,6 +648,13 @@ def api_position_live():
direction, entry, sl, tp, lots, mark, capital, sym, direction, entry, sl, tp, lots, mark, capital, sym,
) )
holding = calc_holding_duration(r["open_time"] or "", now_iso) holding = calc_holding_duration(r["open_time"] or "", now_iso)
close_est = mark if mark is not None else entry
fee_info = calc_fee_breakdown(
sym, entry, close_est, lots, r["open_time"] or "", now_iso,
)
est_net = None
if metrics.get("float_pnl") is not None:
est_net = round(metrics["float_pnl"] - fee_info["total_fee"], 2)
out.append({ out.append({
"id": r["id"], "id": r["id"],
"symbol": r["symbol_name"] or sym, "symbol": r["symbol_name"] or sym,
@@ -628,6 +667,11 @@ def api_position_live():
"open_time": r["open_time"], "open_time": r["open_time"],
"mark_price": mark, "mark_price": mark,
"holding_duration": holding, "holding_duration": holding,
"est_fee": fee_info["total_fee"],
"est_fee_open": fee_info["open_fee"],
"est_fee_close": fee_info["close_fee"],
"est_fee_close_type": fee_info["close_type"],
"est_pnl_net": est_net,
**metrics, **metrics,
}) })
return jsonify(out) return jsonify(out)
@@ -840,24 +884,26 @@ def close_position(pid):
capital = float(get_setting("live_capital", "0") or 0) capital = float(get_setting("live_capital", "0") or 0)
metrics = calc_position_metrics(direction, entry, sl, tp, lots, close_price, capital, sym) metrics = calc_position_metrics(direction, entry, sl, tp, lots, close_price, capital, sym)
pnl = metrics.get("float_pnl") or 0.0 pnl = metrics.get("float_pnl") or 0.0
fee = calc_round_trip_fee(sym, entry, close_price, lots, open_time, close_time)
pnl_net = round(pnl - fee, 2)
result = classify_close_result(direction, close_price, sl, tp) result = classify_close_result(direction, close_price, sl, tp)
minutes = holding_to_minutes(open_time, close_time) minutes = holding_to_minutes(open_time, close_time)
conn.execute( conn.execute(
"""INSERT INTO trade_logs """INSERT INTO trade_logs
(symbol, symbol_name, market_code, sina_code, monitor_type, direction, (symbol, symbol_name, market_code, sina_code, monitor_type, direction,
entry_price, stop_loss, take_profit, close_price, lots, margin, entry_price, stop_loss, take_profit, close_price, lots, margin,
holding_minutes, open_time, close_time, pnl, result) holding_minutes, open_time, close_time, pnl, fee, pnl_net, result)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
( (
sym, row["symbol_name"], market, sina, "持仓监控", direction, sym, row["symbol_name"], market, sina, "持仓监控", direction,
entry, sl, tp, close_price, lots, metrics["margin"], entry, sl, tp, close_price, lots, metrics["margin"],
minutes, open_time, close_time, pnl, result, minutes, open_time, close_time, pnl, fee, pnl_net, result,
), ),
) )
conn.execute("DELETE FROM position_monitors WHERE id=?", (pid,)) conn.execute("DELETE FROM position_monitors WHERE id=?", (pid,))
conn.commit() conn.commit()
conn.close() conn.close()
flash(f"已平仓,盈亏 {pnl:.2f} 元,已记入交易记录") flash(f"已平仓,盈亏 {pnl:.2f}(扣费后 {pnl_net:.2f} 元),已记入交易记录")
return redirect(url_for("positions")) return redirect(url_for("positions"))
@@ -1060,6 +1106,18 @@ def add_review():
initial_pnl = calc_rr_ratio(direction, entry_price, stop_loss, take_profit) initial_pnl = calc_rr_ratio(direction, entry_price, stop_loss, take_profit)
actual_pnl = calc_rr_ratio(direction, entry_price, stop_loss, close_price) actual_pnl = calc_rr_ratio(direction, entry_price, stop_loss, close_price)
gross_pnl = num("pnl")
if gross_pnl is None and entry_price and close_price:
spec_mult = calc_position_metrics(
direction, entry_price, stop_loss, take_profit,
lots, close_price, 0, symbol,
)
gross_pnl = spec_mult.get("float_pnl")
fee = calc_round_trip_fee(
symbol, entry_price or 0, close_price or 0, lots, open_time, close_time,
)
pnl_net = round((gross_pnl or 0) - fee, 2) if gross_pnl is not None else None
auto_kline = bool(d.get("auto_kline")) auto_kline = bool(d.get("auto_kline"))
if auto_kline and not screenshot: if auto_kline and not screenshot:
try: try:
@@ -1087,19 +1145,19 @@ def add_review():
(open_time, close_time, symbol, symbol_name, market_code, sina_code, (open_time, close_time, symbol, symbol_name, market_code, sina_code,
timeframe, direction, timeframe, direction,
entry_price, stop_loss, take_profit, close_price, lots, entry_price, stop_loss, take_profit, close_price, lots,
holding_duration, initial_pnl, actual_pnl, pnl, holding_duration, initial_pnl, actual_pnl, pnl, fee, pnl_net,
open_type, expected_rr, actual_rr, exit_trigger, exit_supplement, open_type, expected_rr, actual_rr, exit_trigger, exit_supplement,
watch_after_breakeven, new_position_while_occupied, screenshot, watch_after_breakeven, new_position_while_occupied, screenshot,
auto_kline, kline_period1, kline_period2, kline_count, kline_cutoff, auto_kline, kline_period1, kline_period2, kline_count, kline_cutoff,
behavior_tags, is_emotion, notes) behavior_tags, is_emotion, notes)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
( (
open_time, close_time, open_time, close_time,
symbol, symbol_name, market_code, sina_code, symbol, symbol_name, market_code, sina_code,
d.get("timeframe", "").strip(), d.get("timeframe", "").strip(),
direction, direction,
entry_price, stop_loss, take_profit, close_price, lots, entry_price, stop_loss, take_profit, close_price, lots,
holding, initial_pnl, actual_pnl, num("pnl"), holding, initial_pnl, actual_pnl, gross_pnl, fee, pnl_net,
open_type, open_type,
None, None,
None, None,
@@ -1195,7 +1253,28 @@ def stats():
).fetchall() ).fetchall()
recent = conn.execute( recent = conn.execute(
"SELECT * FROM trade_records ORDER BY id DESC LIMIT 10" "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() ).fetchall()
conn.close() conn.close()
@@ -1204,9 +1283,59 @@ def stats():
total=total, win=win, loss=loss, rate=rate, total=total, win=win, loss=loss, rate=rate,
by_symbol=by_symbol, by_type=by_type, by_direction=by_direction, by_symbol=by_symbol, by_type=by_type, by_direction=by_direction,
recent=recent, recent=recent,
total_fee=total_fee,
total_gross=total_gross,
total_net=total_net,
fee_count=fee_count,
fee_by_symbol=fee_by_symbol,
) )
@app.route("/fees", methods=["GET", "POST"])
@login_required
def fees():
if request.method == "POST":
action = request.form.get("action")
if action == "multiplier":
try:
mult = float(request.form.get("fee_multiplier", "2"))
if mult < 0:
flash("倍率不能为负数")
else:
set_setting("fee_multiplier", str(mult))
flash(f"手续费倍率已保存:标准 × {mult}")
except ValueError:
flash("请输入有效倍率")
elif action == "sync":
mult = float(get_setting("fee_multiplier", "2") or 2)
count, msg = sync_fees_from_akshare(mult)
flash(msg if count else msg)
elif action == "reload_json":
n = load_fee_rates_from_json()
flash(f"已从本地 JSON 加载 {n} 个品种费率")
elif action == "save_row":
product = request.form.get("product", "").strip().lower()
if not product:
flash("品种代码不能为空")
else:
upsert_fee_rate(product, {
"exchange": request.form.get("exchange", "").strip(),
"mult": int(request.form.get("mult") or 10),
"open_fixed": float(request.form.get("open_fixed") or 0),
"open_ratio": float(request.form.get("open_ratio") or 0),
"close_yesterday_fixed": float(request.form.get("close_yesterday_fixed") or 0),
"close_yesterday_ratio": float(request.form.get("close_yesterday_ratio") or 0),
"close_today_fixed": float(request.form.get("close_today_fixed") or 0),
"close_today_ratio": float(request.form.get("close_today_ratio") or 0),
})
flash(f"已保存 {product} 费率")
return redirect(url_for("fees"))
rates = list_all_fee_rates()
multiplier = get_setting("fee_multiplier", "2")
return render_template("fees.html", rates=rates, multiplier=multiplier)
@app.route("/settings", methods=["GET", "POST"]) @app.route("/settings", methods=["GET", "POST"])
@login_required @login_required
def settings(): def settings():
+36
View File
@@ -0,0 +1,36 @@
{
"ag": {"exchange": "SHFE", "mult": 15, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
"au": {"exchange": "SHFE", "mult": 1000, "open_fixed": 4.0, "open_ratio": 0, "close_yesterday_fixed": 4.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"cu": {"exchange": "SHFE", "mult": 5, "open_fixed": 0.1, "open_ratio": 0, "close_yesterday_fixed": 0.1, "close_yesterday_ratio": 0, "close_today_fixed": 0.2, "close_today_ratio": 0},
"al": {"exchange": "SHFE", "mult": 5, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 6.0, "close_today_ratio": 0},
"zn": {"exchange": "SHFE", "mult": 5, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"pb": {"exchange": "SHFE", "mult": 5, "open_fixed": 0.08, "open_ratio": 0, "close_yesterday_fixed": 0.08, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"ni": {"exchange": "SHFE", "mult": 1, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 6.0, "close_today_ratio": 0},
"sn": {"exchange": "SHFE", "mult": 1, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 6.0, "close_today_ratio": 0},
"rb": {"exchange": "SHFE", "mult": 10, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
"hc": {"exchange": "SHFE", "mult": 10, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
"ru": {"exchange": "SHFE", "mult": 10, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 12.0, "close_today_ratio": 0},
"fu": {"exchange": "SHFE", "mult": 10, "open_fixed": 0.1, "open_ratio": 0, "close_yesterday_fixed": 0.1, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"bu": {"exchange": "SHFE", "mult": 10, "open_fixed": 0.1, "open_ratio": 0, "close_yesterday_fixed": 0.1, "close_yesterday_ratio": 0, "close_today_fixed": 0.2, "close_today_ratio": 0},
"sp": {"exchange": "SHFE", "mult": 10, "open_fixed": 0.1, "open_ratio": 0, "close_yesterday_fixed": 0.1, "close_yesterday_ratio": 0, "close_today_fixed": 0.1, "close_today_ratio": 0},
"sc": {"exchange": "INE", "mult": 1000, "open_fixed": 40.0, "open_ratio": 0, "close_yesterday_fixed": 40.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"i": {"exchange": "DCE", "mult": 100, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
"j": {"exchange": "DCE", "mult": 100, "open_fixed": 2.4, "open_ratio": 0, "close_yesterday_fixed": 2.4, "close_yesterday_ratio": 0, "close_today_fixed": 2.4, "close_today_ratio": 0},
"jm": {"exchange": "DCE", "mult": 60, "open_fixed": 2.4, "open_ratio": 0, "close_yesterday_fixed": 2.4, "close_yesterday_ratio": 0, "close_today_fixed": 2.4, "close_today_ratio": 0},
"m": {"exchange": "DCE", "mult": 10, "open_fixed": 2.4, "open_ratio": 0, "close_yesterday_fixed": 2.4, "close_yesterday_ratio": 0, "close_today_fixed": 2.4, "close_today_ratio": 0},
"y": {"exchange": "DCE", "mult": 10, "open_fixed": 5.0, "open_ratio": 0, "close_yesterday_fixed": 5.0, "close_yesterday_ratio": 0, "close_today_fixed": 5.0, "close_today_ratio": 0},
"p": {"exchange": "DCE", "mult": 10, "open_fixed": 5.0, "open_ratio": 0, "close_yesterday_fixed": 5.0, "close_yesterday_ratio": 0, "close_today_fixed": 5.0, "close_today_ratio": 0},
"c": {"exchange": "DCE", "mult": 10, "open_fixed": 2.4, "open_ratio": 0, "close_yesterday_fixed": 2.4, "close_yesterday_ratio": 0, "close_today_fixed": 2.4, "close_today_ratio": 0},
"l": {"exchange": "DCE", "mult": 5, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 2.0, "close_today_ratio": 0},
"pp": {"exchange": "DCE", "mult": 5, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 2.0, "close_today_ratio": 0},
"if": {"exchange": "CFFEX", "mult": 300, "open_fixed": 0, "open_ratio": 0.000092, "close_yesterday_fixed": 0, "close_yesterday_ratio": 0.000092, "close_today_fixed": 0, "close_today_ratio": 0.000276},
"ih": {"exchange": "CFFEX", "mult": 300, "open_fixed": 0, "open_ratio": 0.000092, "close_yesterday_fixed": 0, "close_yesterday_ratio": 0.000092, "close_today_fixed": 0, "close_today_ratio": 0.000276},
"ic": {"exchange": "CFFEX", "mult": 200, "open_fixed": 0, "open_ratio": 0.000092, "close_yesterday_fixed": 0, "close_yesterday_ratio": 0.000092, "close_today_fixed": 0, "close_today_ratio": 0.000276},
"im": {"exchange": "CFFEX", "mult": 200, "open_fixed": 0, "open_ratio": 0.000092, "close_yesterday_fixed": 0, "close_yesterday_ratio": 0.000092, "close_today_fixed": 0, "close_today_ratio": 0.000276},
"ma": {"exchange": "CZCE", "mult": 10, "open_fixed": 4.0, "open_ratio": 0, "close_yesterday_fixed": 4.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
"ta": {"exchange": "CZCE", "mult": 5, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"sr": {"exchange": "CZCE", "mult": 10, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"cf": {"exchange": "CZCE", "mult": 5, "open_fixed": 8.6, "open_ratio": 0, "close_yesterday_fixed": 8.6, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
"fg": {"exchange": "CZCE", "mult": 20, "open_fixed": 12.0, "open_ratio": 0, "close_yesterday_fixed": 12.0, "close_yesterday_ratio": 0, "close_today_fixed": 12.0, "close_today_ratio": 0},
"sa": {"exchange": "CZCE", "mult": 20, "open_fixed": 7.2, "open_ratio": 0, "close_yesterday_fixed": 7.2, "close_yesterday_ratio": 0, "close_today_fixed": 7.2, "close_today_ratio": 0}
}
+278
View File
@@ -0,0 +1,278 @@
"""期货手续费:本地费率表 + 开平合计估算(模拟盘参考)。"""
import json
import os
import re
import sqlite3
from datetime import datetime
from typing import Optional
from contract_specs import get_contract_spec
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "futures.db")
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
DEFAULT_JSON = os.path.join(DATA_DIR, "fee_rates.json")
# 无配置时的兜底(已为交易所标准约 2 倍)
DEFAULT_FEE = {
"open_fixed": 2.0,
"open_ratio": 0.0,
"close_yesterday_fixed": 2.0,
"close_yesterday_ratio": 0.0,
"close_today_fixed": 4.0,
"close_today_ratio": 0.0,
}
_INDEX_PRODUCTS = {"if", "ih", "ic", "im"}
def product_from_code(ths_code: str) -> str:
code = (ths_code or "").strip()
m = re.match(r"^([A-Za-z]+)", code)
return m.group(1).lower() if m else ""
def _get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def get_fee_multiplier() -> float:
conn = _get_db()
row = conn.execute(
"SELECT value FROM settings WHERE key='fee_multiplier'"
).fetchone()
conn.close()
if row and row["value"]:
try:
return max(0.0, float(row["value"]))
except ValueError:
pass
return 2.0
def get_fee_spec(ths_code: str) -> dict:
product = product_from_code(ths_code)
if not product:
spec = get_contract_spec(ths_code)
return {**DEFAULT_FEE, "mult": spec["mult"], "product": "", "exchange": ""}
conn = _get_db()
row = conn.execute(
"SELECT * FROM fee_rates WHERE product=?", (product,)
).fetchone()
conn.close()
mult = get_contract_spec(ths_code)["mult"]
if row:
return {
"product": product,
"exchange": row["exchange"] or "",
"mult": int(row["mult"] or mult),
"open_fixed": float(row["open_fixed"] or 0),
"open_ratio": float(row["open_ratio"] or 0),
"close_yesterday_fixed": float(row["close_yesterday_fixed"] or 0),
"close_yesterday_ratio": float(row["close_yesterday_ratio"] or 0),
"close_today_fixed": float(row["close_today_fixed"] or 0),
"close_today_ratio": float(row["close_today_ratio"] or 0),
}
if product in _INDEX_PRODUCTS:
return {
"product": product,
"exchange": "CFFEX",
"mult": mult,
"open_fixed": 0.0,
"open_ratio": 0.000092,
"close_yesterday_fixed": 0.0,
"close_yesterday_ratio": 0.000092,
"close_today_fixed": 0.0,
"close_today_ratio": 0.000276,
}
return {
"product": product,
"exchange": "",
"mult": mult,
**DEFAULT_FEE,
}
def calc_side_fee(
price: float,
lots: float,
mult: int,
fixed: float,
ratio: float,
) -> float:
lots = lots or 1.0
fixed = fixed or 0.0
ratio = ratio or 0.0
return fixed * lots + ratio * price * mult * lots
def is_same_day(open_time: str, close_time: str) -> bool:
if not open_time or not close_time:
return True
o = open_time.strip().replace(" ", "T")[:10]
c = close_time.strip().replace(" ", "T")[:10]
return o == c
def calc_round_trip_fee(
ths_code: str,
entry_price: float,
close_price: float,
lots: float,
open_time: str = "",
close_time: str = "",
) -> float:
if not entry_price or not close_price:
return 0.0
spec = get_fee_spec(ths_code)
mult = spec["mult"]
lots = lots or 1.0
open_fee = calc_side_fee(
entry_price, lots, mult,
spec["open_fixed"], spec["open_ratio"],
)
if is_same_day(open_time, close_time):
close_fee = calc_side_fee(
close_price, lots, mult,
spec["close_today_fixed"], spec["close_today_ratio"],
)
else:
close_fee = calc_side_fee(
close_price, lots, mult,
spec["close_yesterday_fixed"], spec["close_yesterday_ratio"],
)
return round(open_fee + close_fee, 2)
def calc_fee_breakdown(
ths_code: str,
entry_price: float,
close_price: float,
lots: float,
open_time: str = "",
close_time: str = "",
) -> dict:
spec = get_fee_spec(ths_code)
mult = spec["mult"]
lots = lots or 1.0
open_fee = calc_side_fee(
entry_price, lots, mult, spec["open_fixed"], spec["open_ratio"],
)
same_day = is_same_day(open_time, close_time)
if same_day:
close_fee = calc_side_fee(
close_price, lots, mult,
spec["close_today_fixed"], spec["close_today_ratio"],
)
close_type = "平今"
else:
close_fee = calc_side_fee(
close_price, lots, mult,
spec["close_yesterday_fixed"], spec["close_yesterday_ratio"],
)
close_type = "平昨"
total = round(open_fee + close_fee, 2)
return {
"open_fee": round(open_fee, 2),
"close_fee": round(close_fee, 2),
"close_type": close_type,
"total_fee": total,
"same_day": same_day,
}
def load_fee_rates_from_json(path: Optional[str] = None) -> int:
path = path or DEFAULT_JSON
if not os.path.isfile(path):
return 0
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
conn = _get_db()
now = datetime.now().isoformat(timespec="seconds")
count = 0
for product, item in data.items():
if not isinstance(item, dict):
continue
conn.execute(
"""INSERT INTO fee_rates
(product, exchange, mult,
open_fixed, open_ratio,
close_yesterday_fixed, close_yesterday_ratio,
close_today_fixed, close_today_ratio, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(product) DO UPDATE SET
exchange=excluded.exchange, mult=excluded.mult,
open_fixed=excluded.open_fixed, open_ratio=excluded.open_ratio,
close_yesterday_fixed=excluded.close_yesterday_fixed,
close_yesterday_ratio=excluded.close_yesterday_ratio,
close_today_fixed=excluded.close_today_fixed,
close_today_ratio=excluded.close_today_ratio,
updated_at=excluded.updated_at""",
(
product.lower(),
item.get("exchange", ""),
int(item.get("mult") or get_contract_spec(product)["mult"]),
float(item.get("open_fixed") or 0),
float(item.get("open_ratio") or 0),
float(item.get("close_yesterday_fixed") or 0),
float(item.get("close_yesterday_ratio") or 0),
float(item.get("close_today_fixed") or 0),
float(item.get("close_today_ratio") or 0),
now,
),
)
count += 1
conn.commit()
conn.close()
return count
def list_all_fee_rates() -> list:
conn = _get_db()
rows = conn.execute(
"SELECT * FROM fee_rates ORDER BY product"
).fetchall()
conn.close()
return [dict(r) for r in rows]
def upsert_fee_rate(product: str, fields: dict) -> None:
product = product.lower().strip()
conn = _get_db()
now = datetime.now().isoformat(timespec="seconds")
conn.execute(
"""INSERT INTO fee_rates
(product, exchange, mult,
open_fixed, open_ratio,
close_yesterday_fixed, close_yesterday_ratio,
close_today_fixed, close_today_ratio, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(product) DO UPDATE SET
exchange=excluded.exchange, mult=excluded.mult,
open_fixed=excluded.open_fixed, open_ratio=excluded.open_ratio,
close_yesterday_fixed=excluded.close_yesterday_fixed,
close_yesterday_ratio=excluded.close_yesterday_ratio,
close_today_fixed=excluded.close_today_fixed,
close_today_ratio=excluded.close_today_ratio,
updated_at=excluded.updated_at""",
(
product,
fields.get("exchange", ""),
int(fields.get("mult") or 10),
float(fields.get("open_fixed") or 0),
float(fields.get("open_ratio") or 0),
float(fields.get("close_yesterday_fixed") or 0),
float(fields.get("close_yesterday_ratio") or 0),
float(fields.get("close_today_fixed") or 0),
float(fields.get("close_today_ratio") or 0),
now,
),
)
conn.commit()
conn.close()
+85
View File
@@ -0,0 +1,85 @@
"""从第三方(AKShare)同步交易所参考手续费,并按倍率写入本地表。"""
import re
from typing import Any, Optional
from contract_specs import get_contract_spec
from fee_specs import get_fee_multiplier, upsert_fee_rate
def _to_float(val: Any) -> float:
if val is None:
return 0.0
s = str(val).strip().replace(",", "")
if not s or s in ("-", "None", "nan"):
return 0.0
try:
return float(s)
except ValueError:
return 0.0
def _parse_akshare_row(row: dict, multiplier: float) -> Optional[dict]:
code = str(row.get("合约代码") or row.get("代码") or "").strip()
if not code:
return None
m = re.match(r"^([A-Za-z]+)", code)
if not m:
return None
product = m.group(1).lower()
open_ratio = _to_float(row.get("手续费标准-开仓-万分之")) / 10000.0
open_fixed = _to_float(row.get("手续费标准-开仓-元"))
if open_fixed == 0 and row.get("开仓"):
open_fixed = _to_float(row.get("开仓"))
close_y_ratio = _to_float(row.get("手续费标准-平昨-万分之")) / 10000.0
close_y_fixed = _to_float(row.get("手续费标准-平昨-元"))
if close_y_fixed == 0 and row.get("平昨"):
close_y_fixed = _to_float(row.get("平昨"))
close_t_ratio = _to_float(row.get("手续费标准-平今-万分之")) / 10000.0
close_t_fixed = _to_float(row.get("手续费标准-平今-元"))
if close_t_fixed == 0 and row.get("平今"):
close_t_fixed = _to_float(row.get("平今"))
mult = int(get_contract_spec(code)["mult"])
exchange = str(row.get("交易所名称") or row.get("交易所") or "").strip()
return {
"product": product,
"exchange": exchange,
"mult": mult,
"open_fixed": round(open_fixed * multiplier, 6),
"open_ratio": round(open_ratio * multiplier, 8),
"close_yesterday_fixed": round(close_y_fixed * multiplier, 6),
"close_yesterday_ratio": round(close_y_ratio * multiplier, 8),
"close_today_fixed": round(close_t_fixed * multiplier, 6),
"close_today_ratio": round(close_t_ratio * multiplier, 8),
}
def sync_fees_from_akshare(multiplier: Optional[float] = None) -> tuple[int, str]:
multiplier = multiplier if multiplier is not None else get_fee_multiplier()
try:
import akshare as ak
except ImportError:
return 0, "未安装 akshare,请执行 pip install akshare 后重试,或使用默认费率表"
try:
df = ak.futures_comm_info(symbol="所有")
except Exception as exc:
return 0, f"拉取第三方数据失败: {exc}"
if df is None or df.empty:
return 0, "第三方返回空数据"
seen: set[str] = set()
count = 0
for _, series in df.iterrows():
row = series.to_dict()
parsed = _parse_akshare_row(row, multiplier)
if not parsed or parsed["product"] in seen:
continue
seen.add(parsed["product"])
upsert_fee_rate(parsed["product"], parsed)
count += 1
return count, f"已同步 {count} 个品种(标准费率 × {multiplier}"
+1
View File
@@ -3,3 +3,4 @@ requests==2.32.3
python-dotenv==1.0.1 python-dotenv==1.0.1
Werkzeug==3.0.3 Werkzeug==3.0.3
matplotlib==3.9.2 matplotlib==3.9.2
# 可选:第三方手续费同步 pip install akshare
+4
View File
@@ -37,6 +37,9 @@
'<div class="cell"><label>盈亏比</label><div>' + rr + '</div></div>' + '<div class="cell"><label>盈亏比</label><div>' + rr + '</div></div>' +
'<div class="cell"><label>标记价</label><div>' + (row.mark_price != null ? fmtNum(row.mark_price) : '--') + '</div></div>' + '<div class="cell"><label>标记价</label><div>' + (row.mark_price != null ? fmtNum(row.mark_price) : '--') + '</div></div>' +
'<div class="cell ' + pnlClass + '"><label>浮盈亏</label><div>' + pnlText + '</div></div>' + '<div class="cell ' + pnlClass + '"><label>浮盈亏</label><div>' + pnlText + '</div></div>' +
'<div class="cell"><label>预估手续费</label><div>' + fmtNum(row.est_fee) + '元</div></div>' +
'<div class="cell ' + (row.est_pnl_net > 0 ? 'pnl-pos' : (row.est_pnl_net < 0 ? 'pnl-neg' : '')) + '">' +
'<label>扣费后</label><div>' + (row.est_pnl_net != null ? fmtNum(row.est_pnl_net) + '元' : '--') + '</div></div>' +
'</div>' + '</div>' +
'<div class="pos-footer">' + '<div class="pos-footer">' +
'<span>保证金 ' + fmtNum(row.margin) + '元</span>' + '<span>保证金 ' + fmtNum(row.margin) + '元</span>' +
@@ -44,6 +47,7 @@
'<span>开仓 ' + (openT || '--') + '</span>' + '<span>开仓 ' + (openT || '--') + '</span>' +
'<span>持仓 ' + (row.holding_duration || '--') + '</span>' + '<span>持仓 ' + (row.holding_duration || '--') + '</span>' +
'<span>张数 ' + row.lots + '</span>' + '<span>张数 ' + row.lots + '</span>' +
'<span>手续费(估) ' + fmtNum(row.est_fee) + '元 (' + (row.est_fee_close_type || '') + ')</span>' +
'</div></div>' '</div></div>'
); );
} }
+4 -2
View File
@@ -92,7 +92,7 @@
var labels = [ var labels = [
'品种', '合约', '方向', '张数', '周期', '品种', '合约', '方向', '张数', '周期',
'成交价', '止损', '止盈', '平仓价', '成交价', '止损', '止盈', '平仓价',
'开仓时间', '平仓时间', '持仓时长', '盈利金额', '盈亏比', '开仓时间', '平仓时间', '持仓时长', '盈利金额', '手续费', '净盈亏', '盈亏比',
'开仓类型', '行为标签' '开仓类型', '行为标签'
]; ];
var values = [ var values = [
@@ -109,6 +109,8 @@
fmtTime(data.close_time), fmtTime(data.close_time),
esc(data.holding_duration), esc(data.holding_duration),
esc(data.pnl), esc(data.pnl),
esc(data.fee),
esc(data.pnl_net),
fmtRR(data), fmtRR(data),
esc(data.open_type), esc(data.open_type),
fmtTags(data) fmtTags(data)
@@ -122,7 +124,7 @@
html += '</div><div class="review-detail-values">'; html += '</div><div class="review-detail-values">';
values.forEach(function (val, i) { values.forEach(function (val, i) {
var cls = ''; var cls = '';
if (i === 15 && data.is_emotion) cls = ' class="emotion-val"'; if (i === 17 && data.is_emotion) cls = ' class="emotion-val"';
html += '<span' + cls + '>' + val + '</span>'; html += '<span' + cls + '>' + val + '</span>';
}); });
html += '</div></div>'; html += '</div></div>';
+2 -1
View File
@@ -341,7 +341,7 @@
.pos-card-head .title{font-size:1rem;font-weight:600;color:var(--text-title)} .pos-card-head .title{font-size:1rem;font-weight:600;color:var(--text-title)}
.pos-card-meta{font-size:.75rem;color:var(--text-muted);margin-bottom:.65rem} .pos-card-meta{font-size:.75rem;color:var(--text-muted);margin-bottom:.65rem}
.pos-card-meta strong{color:var(--text-primary)} .pos-card-meta strong{color:var(--text-primary)}
.pos-metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem .65rem;margin-bottom:.65rem} .pos-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem .65rem;margin-bottom:.65rem}
.pos-metrics .cell label{display:block;font-size:.68rem;color:var(--text-muted);margin-bottom:.15rem} .pos-metrics .cell label{display:block;font-size:.68rem;color:var(--text-muted);margin-bottom:.15rem}
.pos-metrics .cell div{font-size:.88rem;color:var(--text-primary)} .pos-metrics .cell div{font-size:.88rem;color:var(--text-primary)}
.pos-metrics .cell.pnl-pos div{color:var(--profit)} .pos-metrics .cell.pnl-pos div{color:var(--profit)}
@@ -405,6 +405,7 @@
<a href="{{ url_for('positions') }}" class="{% if request.endpoint == 'positions' %}active{% endif %}">持仓监控</a> <a href="{{ url_for('positions') }}" class="{% if request.endpoint == 'positions' %}active{% endif %}">持仓监控</a>
<a href="{{ url_for('records') }}" class="{% if request.endpoint in ('records', 'trades') %}active{% endif %}">交易记录与复盘</a> <a href="{{ url_for('records') }}" class="{% if request.endpoint in ('records', 'trades') %}active{% endif %}">交易记录与复盘</a>
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a> <a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
<a href="{{ url_for('fees') }}" class="{% if request.endpoint == 'fees' %}active{% endif %}">手续费配置</a>
<a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a> <a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a>
</nav> </nav>
</header> </header>
+67
View File
@@ -0,0 +1,67 @@
{% extends "base.html" %}
{% block title %}手续费配置 - 国内期货监控系统{% endblock %}
{% block content %}
<div class="card">
<h2>手续费倍率</h2>
<div class="card-body">
<form action="{{ url_for('fees') }}" method="post" class="form-row">
<input type="hidden" name="action" value="multiplier">
<label class="text-muted" style="font-size:.85rem">第三方标准费率 ×</label>
<input name="fee_multiplier" type="number" step="0.1" min="0" value="{{ multiplier }}" style="width:100px">
<button type="submit" class="btn-primary">保存倍率</button>
</form>
<p class="hint">默认 2 倍:从第三方拉取交易所参考标准后自动乘以该倍率写入本地表。模拟盘估算用,非实盘账单。</p>
<div class="form-row" style="margin-top:.75rem">
<form action="{{ url_for('fees') }}" method="post" style="display:inline">
<input type="hidden" name="action" value="sync">
<button type="submit" class="btn-primary">从第三方同步(AKShare</button>
</form>
<form action="{{ url_for('fees') }}" method="post" style="display:inline">
<input type="hidden" name="action" value="reload_json">
<button type="submit" class="btn-link" style="padding:.5rem 1rem;border:1px solid var(--card-border);border-radius:8px">重载本地 JSON 默认表</button>
</form>
</div>
</div>
</div>
<div class="card">
<h2>品种费率表(已含倍率)</h2>
<div class="card-body card-scroll">
<table class="trade-table">
<thead>
<tr>
<th>品种</th><th>交易所</th><th>乘数</th>
<th>开仓(元/手)</th><th>开仓(比例)</th>
<th>平昨(元/手)</th><th>平昨(比例)</th>
<th>平今(元/手)</th><th>平今(比例)</th>
<th>更新</th><th>操作</th>
</tr>
</thead>
<tbody>
{% for r in rates %}
<tr>
<form action="{{ url_for('fees') }}" method="post" style="display:contents">
<input type="hidden" name="action" value="save_row">
<input type="hidden" name="product" value="{{ r.product }}">
<td><strong>{{ r.product }}</strong></td>
<td><input name="exchange" value="{{ r.exchange or '' }}" style="width:72px;padding:.3rem"></td>
<td><input name="mult" type="number" value="{{ r.mult }}" style="width:56px;padding:.3rem"></td>
<td><input name="open_fixed" type="number" step="0.0001" value="{{ r.open_fixed }}" style="width:72px;padding:.3rem"></td>
<td><input name="open_ratio" type="number" step="0.0000001" value="{{ r.open_ratio }}" style="width:88px;padding:.3rem"></td>
<td><input name="close_yesterday_fixed" type="number" step="0.0001" value="{{ r.close_yesterday_fixed }}" style="width:72px;padding:.3rem"></td>
<td><input name="close_yesterday_ratio" type="number" step="0.0000001" value="{{ r.close_yesterday_ratio }}" style="width:88px;padding:.3rem"></td>
<td><input name="close_today_fixed" type="number" step="0.0001" value="{{ r.close_today_fixed }}" style="width:72px;padding:.3rem"></td>
<td><input name="close_today_ratio" type="number" step="0.0000001" value="{{ r.close_today_ratio }}" style="width:88px;padding:.3rem"></td>
<td class="text-muted" style="font-size:.72rem">{{ (r.updated_at or '')[:16] }}</td>
<td><button type="submit" class="btn-link">保存</button></td>
</form>
</tr>
{% else %}
<tr><td colspan="11" class="text-muted">暂无费率,请同步或重载 JSON</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="hint" style="margin-top:.75rem">比例按「成交价×乘数×手数×比例」计费;元/手为固定每手。开+平合计为一笔往返手续费。</p>
</div>
{% endblock %}
+17 -4
View File
@@ -16,7 +16,7 @@
<th>成交</th><th>止损(开仓)</th><th>止盈</th> <th>成交</th><th>止损(开仓)</th><th>止盈</th>
<th>基数</th><th>杠杆</th><th>持仓分钟</th> <th>基数</th><th>杠杆</th><th>持仓分钟</th>
<th>开仓时间</th><th>平仓时间</th> <th>开仓时间</th><th>平仓时间</th>
<th>盈亏(元)</th><th>结果</th><th>操作</th> <th>盈亏(元)</th><th>手续费</th><th>净盈亏</th><th>结果</th><th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -74,6 +74,12 @@
<input type="hidden" name="close_price" value="{{ t.close_price or '' }}"> <input type="hidden" name="close_price" value="{{ t.close_price or '' }}">
<input type="hidden" name="symbol_name" value="{{ t.symbol_name or t.symbol }}"> <input type="hidden" name="symbol_name" value="{{ t.symbol_name or t.symbol }}">
</td> </td>
<td><span class="cell-readonly text-muted">{{ t.fee if t.fee is not none else '-' }}</span></td>
<td>
<span class="cell-readonly {% if t.pnl_net and t.pnl_net > 0 %}text-profit{% elif t.pnl_net and t.pnl_net < 0 %}text-loss{% endif %}">
{{ t.pnl_net if t.pnl_net is not none else '-' }}
</span>
</td>
<td> <td>
<span class="cell-readonly cell-edit-hide"> <span class="cell-readonly cell-edit-hide">
{% if t.result == '止盈' %}<span class="badge profit">{{ t.result }}</span> {% if t.result == '止盈' %}<span class="badge profit">{{ t.result }}</span>
@@ -99,7 +105,7 @@
</td> </td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="14" class="text-muted">暂无交易记录</td></tr> <tr><td colspan="16" class="text-muted">暂无交易记录</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@@ -208,7 +214,7 @@
<table> <table>
<thead> <thead>
<tr> <tr>
<th>平仓</th><th>品种</th><th>方向</th><th>盈亏</th><th>情绪单</th><th>详情</th><th></th> <th>平仓</th><th>品种</th><th>方向</th><th>盈亏</th><th>手续费</th><th>净盈亏</th><th>情绪单</th><th>详情</th><th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -222,6 +228,12 @@
{% elif r.pnl and r.pnl < 0 %}<span class="badge loss">{{ r.pnl }}</span> {% elif r.pnl and r.pnl < 0 %}<span class="badge loss">{{ r.pnl }}</span>
{% else %}{{ r.actual_pnl or '-' }}{% endif %} {% else %}{{ r.actual_pnl or '-' }}{% endif %}
</td> </td>
<td class="text-muted">{{ r.fee if r.fee is not none else '-' }}</td>
<td>
{% if r.pnl_net and r.pnl_net > 0 %}<span class="badge profit">{{ r.pnl_net }}</span>
{% elif r.pnl_net and r.pnl_net < 0 %}<span class="badge loss">{{ r.pnl_net }}</span>
{% else %}-{% endif %}
</td>
<td>{% if r.is_emotion %}<span class="badge emotion">情绪单</span>{% else %}-{% endif %}</td> <td>{% if r.is_emotion %}<span class="badge emotion">情绪单</span>{% else %}-{% endif %}</td>
<td> <td>
<button type="button" class="btn-link review-view-btn" data-review='{{ { <button type="button" class="btn-link review-view-btn" data-review='{{ {
@@ -233,6 +245,7 @@
"open_time": r.open_time, "close_time": r.close_time, "open_time": r.open_time, "close_time": r.close_time,
"holding_duration": r.holding_duration, "holding_duration": r.holding_duration,
"initial_pnl": r.initial_pnl, "actual_pnl": r.actual_pnl, "pnl": r.pnl, "initial_pnl": r.initial_pnl, "actual_pnl": r.actual_pnl, "pnl": r.pnl,
"fee": r.fee, "pnl_net": r.pnl_net,
"open_type": r.open_type, "open_type": r.open_type,
"exit_trigger": r.exit_trigger, "exit_supplement": r.exit_supplement, "exit_trigger": r.exit_trigger, "exit_supplement": r.exit_supplement,
"watch_after_breakeven": r.watch_after_breakeven, "watch_after_breakeven": r.watch_after_breakeven,
@@ -244,7 +257,7 @@
<td><a href="{{ url_for('del_review', rid=r.id) }}" class="btn-del" onclick="return confirm('删除?')"></a></td> <td><a href="{{ url_for('del_review', rid=r.id) }}" class="btn-del" onclick="return confirm('删除?')"></a></td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="7" class="text-muted">暂无复盘记录</td></tr> <tr><td colspan="9" class="text-muted">暂无复盘记录</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
+38 -5
View File
@@ -9,6 +9,13 @@
<div class="stat-item"><div class="label">胜率</div><div class="value">{{ rate }}%</div></div> <div class="stat-item"><div class="label">胜率</div><div class="value">{{ rate }}%</div></div>
</div> </div>
<div class="stat-grid">
<div class="stat-item"><div class="label">累计手续费</div><div class="value text-loss">{{ total_fee }} 元</div></div>
<div class="stat-item"><div class="label">毛盈亏合计</div><div class="value">{{ total_gross }} 元</div></div>
<div class="stat-item"><div class="label">净盈亏合计</div><div class="value {% if total_net > 0 %}text-profit{% elif total_net < 0 %}text-loss{% endif %}">{{ total_net }} 元</div></div>
<div class="stat-item"><div class="label">计费笔数</div><div class="value">{{ fee_count }}</div></div>
</div>
<div class="card"> <div class="card">
<h2>按品种统计</h2> <h2>按品种统计</h2>
<table> <table>
@@ -28,6 +35,24 @@
</table> </table>
</div> </div>
<div class="card">
<h2>手续费按品种(交易记录)</h2>
<table>
<thead><tr><th>品种</th><th>笔数</th><th>累计手续费(元)</th></tr></thead>
<tbody>
{% for f in fee_by_symbol %}
<tr>
<td>{{ f.symbol_name or f.symbol }}</td>
<td>{{ f.cnt }}</td>
<td class="text-loss">{{ round(f.total_fee, 2) }}</td>
</tr>
{% else %}
<tr><td colspan="3" class="text-muted">暂无手续费数据</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card"> <div class="card">
<h2>按类型统计</h2> <h2>按类型统计</h2>
<table> <table>
@@ -67,22 +92,30 @@
</div> </div>
<div class="card"> <div class="card">
<h2>最近 10 笔交易</h2> <h2>最近 10 笔交易记录</h2>
<table> <table>
<thead><tr><th>品种</th><th>方向</th><th>结果</th><th>时间</th></tr></thead> <thead><tr><th>品种</th><th>方向</th><th>毛盈亏</th><th>手续费</th><th>净盈亏</th><th>结果</th><th>时间</th></tr></thead>
<tbody> <tbody>
{% for r in recent %} {% for r in recent %}
<tr> <tr>
<td>{{ r.symbol_name or r.symbol }}</td> <td>{{ r.symbol_name or r.symbol }}</td>
<td><span class="badge dir">{{ '做多' if r.direction == 'long' else '做空' }}</span></td> <td><span class="badge dir">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ r.pnl if r.pnl is not none else '-' }}</td>
<td class="text-muted">{{ r.fee if r.fee is not none else '-' }}</td>
<td>
{% if r.pnl_net and r.pnl_net > 0 %}<span class="badge profit">{{ r.pnl_net }}</span>
{% elif r.pnl_net and r.pnl_net < 0 %}<span class="badge loss">{{ r.pnl_net }}</span>
{% else %}-{% endif %}
</td>
<td> <td>
{% if r.result == '止盈' %}<span class="badge profit">止盈</span> {% if r.result == '止盈' %}<span class="badge profit">止盈</span>
{% else %}<span class="badge loss">止损</span>{% endif %} {% elif r.result == '止损' %}<span class="badge loss">止损</span>
{% else %}{{ r.result or '-' }}{% endif %}
</td> </td>
<td>{{ r.created_at[:16] if r.created_at else '' }}</td> <td>{{ r.close_time[:16] if r.close_time else (r.created_at[:16] if r.created_at else '') }}</td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="4" class="text-muted">暂无数据</td></tr> <tr><td colspan="7" class="text-muted">暂无数据</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>