feat: 导航开关与 CTP 柜台手续费
系统设置可开关五类导航;手续费默认从 CTP 查询同步,本地/AKShare 作离线兜底;补充 FEES.md。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,17 +22,21 @@ from flask import (
|
||||
)
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from symbols import search_symbols, ths_to_codes, list_main_contracts_grouped, refresh_main_index
|
||||
from contract_specs import calc_position_metrics
|
||||
from fee_specs import (
|
||||
calc_fee_breakdown,
|
||||
calc_round_trip_fee,
|
||||
get_fee_multiplier,
|
||||
get_fee_source_mode,
|
||||
list_all_fee_rates,
|
||||
load_fee_rates_from_json,
|
||||
upsert_fee_rate,
|
||||
)
|
||||
from fee_sync import sync_fees_from_akshare
|
||||
from nav_settings import NAV_TOGGLES, get_nav_items, nav_enabled, save_nav_items
|
||||
from contract_profile import get_contract_profile
|
||||
from stats_engine import STATS_VIEWS, load_stats_cache, refresh_stats_cache
|
||||
from kline_store import ensure_kline_tables
|
||||
@@ -183,6 +187,28 @@ def set_setting(key: str, value: str):
|
||||
conn.close()
|
||||
|
||||
|
||||
def require_nav(key: str):
|
||||
"""导航项关闭时拒绝访问对应页面。"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
if not nav_enabled(get_setting, key):
|
||||
flash("该页面已在系统设置中关闭")
|
||||
return redirect(url_for("positions"))
|
||||
return f(*args, **kwargs)
|
||||
return wrapped
|
||||
return decorator
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_globals():
|
||||
return {"nav_items": get_nav_items(get_setting)}
|
||||
|
||||
|
||||
def _trading_mode() -> str:
|
||||
return (get_setting("trading_mode", "simulation") or "simulation").strip()
|
||||
|
||||
|
||||
def touch_stats_cache():
|
||||
try:
|
||||
conn = get_db()
|
||||
@@ -311,6 +337,13 @@ def init_db():
|
||||
(key TEXT PRIMARY KEY,
|
||||
data_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL)''')
|
||||
for sql in (
|
||||
"ALTER TABLE fee_rates ADD COLUMN source TEXT DEFAULT 'local'",
|
||||
):
|
||||
try:
|
||||
c.execute(sql)
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
ensure_kline_tables(conn)
|
||||
init_strategy_tables(conn)
|
||||
from risk.account_risk_lib import ensure_account_risk_schema
|
||||
@@ -339,6 +372,8 @@ def init_db():
|
||||
set_setting("position_sizing_mode", "risk")
|
||||
if not get_setting("risk_percent"):
|
||||
set_setting("risk_percent", "1")
|
||||
if not get_setting("fee_source_mode"):
|
||||
set_setting("fee_source_mode", "ctp")
|
||||
conn = get_db()
|
||||
fee_cnt = conn.execute("SELECT COUNT(*) FROM fee_rates").fetchone()[0]
|
||||
conn.close()
|
||||
@@ -768,6 +803,7 @@ def api_position_live():
|
||||
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,
|
||||
trading_mode=_trading_mode(),
|
||||
)
|
||||
est_net = None
|
||||
if metrics.get("float_pnl") is not None:
|
||||
@@ -796,11 +832,14 @@ def api_position_live():
|
||||
|
||||
@login_required
|
||||
def index():
|
||||
return redirect(url_for("plans"))
|
||||
if nav_enabled(get_setting, "plans"):
|
||||
return redirect(url_for("plans"))
|
||||
return redirect(url_for("positions"))
|
||||
|
||||
|
||||
@app.route("/plans")
|
||||
@login_required
|
||||
@require_nav("plans")
|
||||
def plans():
|
||||
today = today_str()
|
||||
start = request.args.get("start", "")
|
||||
@@ -962,7 +1001,7 @@ 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)
|
||||
fee = calc_round_trip_fee(sym, entry, close_price, lots, open_time, close_time, trading_mode=_trading_mode())
|
||||
pnl_net = round(pnl - fee, 2)
|
||||
result = classify_close_result(direction, close_price, sl, tp)
|
||||
minutes = holding_to_minutes(open_time, close_time)
|
||||
@@ -1196,6 +1235,7 @@ def add_review():
|
||||
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,
|
||||
trading_mode=_trading_mode(),
|
||||
)
|
||||
pnl_net = round((gross_pnl or 0) - fee, 2) if gross_pnl is not None else None
|
||||
|
||||
@@ -1337,6 +1377,7 @@ def api_stats_refresh():
|
||||
|
||||
@app.route("/market")
|
||||
@login_required
|
||||
@require_nav("market")
|
||||
def market_page():
|
||||
symbol = request.args.get("symbol", "").strip()
|
||||
period = request.args.get("period", "15m").strip()
|
||||
@@ -1425,6 +1466,7 @@ def api_market_quote():
|
||||
|
||||
@app.route("/contract")
|
||||
@login_required
|
||||
@require_nav("contract")
|
||||
def contract_profile_page():
|
||||
symbol = request.args.get("symbol", "").strip()
|
||||
profile = None
|
||||
@@ -1462,10 +1504,23 @@ def api_contract_profile():
|
||||
|
||||
@app.route("/fees", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@require_nav("fees")
|
||||
def fees():
|
||||
from trading_context import get_trading_mode
|
||||
from ctp_fee_sync import sync_fees_from_ctp
|
||||
from vnpy_bridge import ctp_status
|
||||
|
||||
mode = get_trading_mode(get_setting)
|
||||
if request.method == "POST":
|
||||
action = request.form.get("action")
|
||||
if action == "multiplier":
|
||||
if action == "fee_source":
|
||||
fs = request.form.get("fee_source_mode", "ctp").strip()
|
||||
set_setting("fee_source_mode", fs if fs in ("ctp", "local") else "ctp")
|
||||
flash("手续费数据源已保存")
|
||||
elif action == "sync_ctp":
|
||||
count, msg = sync_fees_from_ctp(mode)
|
||||
flash(msg)
|
||||
elif action == "multiplier":
|
||||
try:
|
||||
mult = float(request.form.get("fee_multiplier", "2"))
|
||||
if mult < 0:
|
||||
@@ -1496,13 +1551,22 @@ def fees():
|
||||
"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),
|
||||
"source": "manual",
|
||||
})
|
||||
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)
|
||||
fee_source_mode = get_fee_source_mode()
|
||||
ctp_st = ctp_status(mode)
|
||||
return render_template(
|
||||
"fees.html",
|
||||
rates=rates,
|
||||
multiplier=multiplier,
|
||||
fee_source_mode=fee_source_mode,
|
||||
ctp_connected=bool(ctp_st.get("connected")),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/settings", methods=["GET", "POST"])
|
||||
@@ -1541,6 +1605,10 @@ def settings():
|
||||
flash("风险比例无效")
|
||||
return redirect(url_for("settings"))
|
||||
flash("交易模式已保存")
|
||||
elif action == "nav":
|
||||
items = {k: request.form.get(f"nav_{k}") == "on" for k in NAV_TOGGLES}
|
||||
save_nav_items(set_setting, items)
|
||||
flash("导航显示已保存")
|
||||
elif action == "password":
|
||||
old_p = request.form.get("old_password", "")
|
||||
new_p = request.form.get("new_password", "")
|
||||
@@ -1569,12 +1637,15 @@ def settings():
|
||||
trading_mode=get_setting("trading_mode", "simulation"),
|
||||
position_sizing_mode=get_setting("position_sizing_mode", "risk"),
|
||||
risk_percent=get_setting("risk_percent", "1"),
|
||||
nav_items=get_nav_items(get_setting),
|
||||
nav_toggles=NAV_TOGGLES,
|
||||
)
|
||||
|
||||
|
||||
install_trading(
|
||||
app,
|
||||
login_required=login_required,
|
||||
require_nav=require_nav,
|
||||
get_db=get_db,
|
||||
get_setting=get_setting,
|
||||
set_setting=set_setting,
|
||||
|
||||
Reference in New Issue
Block a user