本地手续费配置(标准×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 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 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 market_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:
try:
@@ -253,6 +266,17 @@ def init_db():
pnl REAL, result TEXT,
verified INTEGER DEFAULT 0,
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.close()
@@ -267,6 +291,14 @@ def init_db():
os.makedirs(UPLOAD_DIR, exist_ok=True)
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():
"""
@@ -616,6 +648,13 @@ def api_position_live():
direction, entry, sl, tp, lots, mark, capital, sym,
)
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({
"id": r["id"],
"symbol": r["symbol_name"] or sym,
@@ -628,6 +667,11 @@ def api_position_live():
"open_time": r["open_time"],
"mark_price": mark,
"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,
})
return jsonify(out)
@@ -840,24 +884,26 @@ def close_position(pid):
capital = float(get_setting("live_capital", "0") or 0)
metrics = calc_position_metrics(direction, entry, sl, tp, lots, close_price, capital, sym)
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)
minutes = holding_to_minutes(open_time, close_time)
conn.execute(
"""INSERT INTO trade_logs
(symbol, symbol_name, market_code, sina_code, monitor_type, direction,
entry_price, stop_loss, take_profit, close_price, lots, margin,
holding_minutes, open_time, close_time, pnl, result)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
holding_minutes, open_time, close_time, pnl, fee, pnl_net, result)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
sym, row["symbol_name"], market, sina, "持仓监控", direction,
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.commit()
conn.close()
flash(f"已平仓,盈亏 {pnl:.2f} 元,已记入交易记录")
flash(f"已平仓,盈亏 {pnl:.2f}(扣费后 {pnl_net:.2f} 元),已记入交易记录")
return redirect(url_for("positions"))
@@ -1060,6 +1106,18 @@ def add_review():
initial_pnl = calc_rr_ratio(direction, entry_price, stop_loss, take_profit)
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"))
if auto_kline and not screenshot:
try:
@@ -1087,19 +1145,19 @@ def add_review():
(open_time, close_time, symbol, symbol_name, market_code, sina_code,
timeframe, direction,
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,
watch_after_breakeven, new_position_while_occupied, screenshot,
auto_kline, kline_period1, kline_period2, kline_count, kline_cutoff,
behavior_tags, is_emotion, notes)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
open_time, close_time,
symbol, symbol_name, market_code, sina_code,
d.get("timeframe", "").strip(),
direction,
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,
None,
None,
@@ -1195,7 +1253,28 @@ def stats():
).fetchall()
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()
conn.close()
@@ -1204,9 +1283,59 @@ def stats():
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,
)
@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"])
@login_required
def settings():