本地手续费配置(标准×2),持仓/交易记录/复盘/统计展示扣费后盈亏
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user