本地手续费配置(标准×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 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():
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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})"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user