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 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 symbols import search_symbols, ths_to_codes, list_main_contracts_grouped, refresh_main_index
|
||||||
from contract_specs import calc_position_metrics
|
from contract_specs import calc_position_metrics
|
||||||
from fee_specs import (
|
from fee_specs import (
|
||||||
calc_fee_breakdown,
|
calc_fee_breakdown,
|
||||||
calc_round_trip_fee,
|
calc_round_trip_fee,
|
||||||
get_fee_multiplier,
|
get_fee_multiplier,
|
||||||
|
get_fee_source_mode,
|
||||||
list_all_fee_rates,
|
list_all_fee_rates,
|
||||||
load_fee_rates_from_json,
|
load_fee_rates_from_json,
|
||||||
upsert_fee_rate,
|
upsert_fee_rate,
|
||||||
)
|
)
|
||||||
from fee_sync import sync_fees_from_akshare
|
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 contract_profile import get_contract_profile
|
||||||
from stats_engine import STATS_VIEWS, load_stats_cache, refresh_stats_cache
|
from stats_engine import STATS_VIEWS, load_stats_cache, refresh_stats_cache
|
||||||
from kline_store import ensure_kline_tables
|
from kline_store import ensure_kline_tables
|
||||||
@@ -183,6 +187,28 @@ def set_setting(key: str, value: str):
|
|||||||
conn.close()
|
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():
|
def touch_stats_cache():
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
@@ -311,6 +337,13 @@ def init_db():
|
|||||||
(key TEXT PRIMARY KEY,
|
(key TEXT PRIMARY KEY,
|
||||||
data_json TEXT NOT NULL,
|
data_json TEXT NOT NULL,
|
||||||
updated_at 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)
|
ensure_kline_tables(conn)
|
||||||
init_strategy_tables(conn)
|
init_strategy_tables(conn)
|
||||||
from risk.account_risk_lib import ensure_account_risk_schema
|
from risk.account_risk_lib import ensure_account_risk_schema
|
||||||
@@ -339,6 +372,8 @@ def init_db():
|
|||||||
set_setting("position_sizing_mode", "risk")
|
set_setting("position_sizing_mode", "risk")
|
||||||
if not get_setting("risk_percent"):
|
if not get_setting("risk_percent"):
|
||||||
set_setting("risk_percent", "1")
|
set_setting("risk_percent", "1")
|
||||||
|
if not get_setting("fee_source_mode"):
|
||||||
|
set_setting("fee_source_mode", "ctp")
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
fee_cnt = conn.execute("SELECT COUNT(*) FROM fee_rates").fetchone()[0]
|
fee_cnt = conn.execute("SELECT COUNT(*) FROM fee_rates").fetchone()[0]
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -768,6 +803,7 @@ def api_position_live():
|
|||||||
close_est = mark if mark is not None else entry
|
close_est = mark if mark is not None else entry
|
||||||
fee_info = calc_fee_breakdown(
|
fee_info = calc_fee_breakdown(
|
||||||
sym, entry, close_est, lots, r["open_time"] or "", now_iso,
|
sym, entry, close_est, lots, r["open_time"] or "", now_iso,
|
||||||
|
trading_mode=_trading_mode(),
|
||||||
)
|
)
|
||||||
est_net = None
|
est_net = None
|
||||||
if metrics.get("float_pnl") is not None:
|
if metrics.get("float_pnl") is not None:
|
||||||
@@ -796,11 +832,14 @@ def api_position_live():
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
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")
|
@app.route("/plans")
|
||||||
@login_required
|
@login_required
|
||||||
|
@require_nav("plans")
|
||||||
def plans():
|
def plans():
|
||||||
today = today_str()
|
today = today_str()
|
||||||
start = request.args.get("start", "")
|
start = request.args.get("start", "")
|
||||||
@@ -962,7 +1001,7 @@ 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)
|
fee = calc_round_trip_fee(sym, entry, close_price, lots, open_time, close_time, trading_mode=_trading_mode())
|
||||||
pnl_net = round(pnl - fee, 2)
|
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)
|
||||||
@@ -1196,6 +1235,7 @@ def add_review():
|
|||||||
gross_pnl = spec_mult.get("float_pnl")
|
gross_pnl = spec_mult.get("float_pnl")
|
||||||
fee = calc_round_trip_fee(
|
fee = calc_round_trip_fee(
|
||||||
symbol, entry_price or 0, close_price or 0, lots, open_time, close_time,
|
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
|
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")
|
@app.route("/market")
|
||||||
@login_required
|
@login_required
|
||||||
|
@require_nav("market")
|
||||||
def market_page():
|
def market_page():
|
||||||
symbol = request.args.get("symbol", "").strip()
|
symbol = request.args.get("symbol", "").strip()
|
||||||
period = request.args.get("period", "15m").strip()
|
period = request.args.get("period", "15m").strip()
|
||||||
@@ -1425,6 +1466,7 @@ def api_market_quote():
|
|||||||
|
|
||||||
@app.route("/contract")
|
@app.route("/contract")
|
||||||
@login_required
|
@login_required
|
||||||
|
@require_nav("contract")
|
||||||
def contract_profile_page():
|
def contract_profile_page():
|
||||||
symbol = request.args.get("symbol", "").strip()
|
symbol = request.args.get("symbol", "").strip()
|
||||||
profile = None
|
profile = None
|
||||||
@@ -1462,10 +1504,23 @@ def api_contract_profile():
|
|||||||
|
|
||||||
@app.route("/fees", methods=["GET", "POST"])
|
@app.route("/fees", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
|
@require_nav("fees")
|
||||||
def 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":
|
if request.method == "POST":
|
||||||
action = request.form.get("action")
|
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:
|
try:
|
||||||
mult = float(request.form.get("fee_multiplier", "2"))
|
mult = float(request.form.get("fee_multiplier", "2"))
|
||||||
if mult < 0:
|
if mult < 0:
|
||||||
@@ -1496,13 +1551,22 @@ def fees():
|
|||||||
"close_yesterday_ratio": float(request.form.get("close_yesterday_ratio") 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_fixed": float(request.form.get("close_today_fixed") or 0),
|
||||||
"close_today_ratio": float(request.form.get("close_today_ratio") or 0),
|
"close_today_ratio": float(request.form.get("close_today_ratio") or 0),
|
||||||
|
"source": "manual",
|
||||||
})
|
})
|
||||||
flash(f"已保存 {product} 费率")
|
flash(f"已保存 {product} 费率")
|
||||||
return redirect(url_for("fees"))
|
return redirect(url_for("fees"))
|
||||||
|
|
||||||
rates = list_all_fee_rates()
|
rates = list_all_fee_rates()
|
||||||
multiplier = get_setting("fee_multiplier", "2")
|
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"])
|
@app.route("/settings", methods=["GET", "POST"])
|
||||||
@@ -1541,6 +1605,10 @@ def settings():
|
|||||||
flash("风险比例无效")
|
flash("风险比例无效")
|
||||||
return redirect(url_for("settings"))
|
return redirect(url_for("settings"))
|
||||||
flash("交易模式已保存")
|
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":
|
elif action == "password":
|
||||||
old_p = request.form.get("old_password", "")
|
old_p = request.form.get("old_password", "")
|
||||||
new_p = request.form.get("new_password", "")
|
new_p = request.form.get("new_password", "")
|
||||||
@@ -1569,12 +1637,15 @@ def settings():
|
|||||||
trading_mode=get_setting("trading_mode", "simulation"),
|
trading_mode=get_setting("trading_mode", "simulation"),
|
||||||
position_sizing_mode=get_setting("position_sizing_mode", "risk"),
|
position_sizing_mode=get_setting("position_sizing_mode", "risk"),
|
||||||
risk_percent=get_setting("risk_percent", "1"),
|
risk_percent=get_setting("risk_percent", "1"),
|
||||||
|
nav_items=get_nav_items(get_setting),
|
||||||
|
nav_toggles=NAV_TOGGLES,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
install_trading(
|
install_trading(
|
||||||
app,
|
app,
|
||||||
login_required=login_required,
|
login_required=login_required,
|
||||||
|
require_nav=require_nav,
|
||||||
get_db=get_db,
|
get_db=get_db,
|
||||||
get_setting=get_setting,
|
get_setting=get_setting,
|
||||||
set_setting=set_setting,
|
set_setting=set_setting,
|
||||||
|
|||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
"""从 CTP 柜台同步手续费率(SimNow / 期货公司)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from contract_specs import get_contract_spec
|
||||||
|
from fee_specs import upsert_fee_rate
|
||||||
|
from vnpy_bridge import get_bridge
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _product_from_instrument(instrument_id: str) -> str:
|
||||||
|
m = re.match(r"^([A-Za-z]+)", instrument_id or "")
|
||||||
|
return m.group(1).lower() if m else ""
|
||||||
|
|
||||||
|
|
||||||
|
def ctp_commission_to_fee_fields(data: dict, ths_code: str) -> dict:
|
||||||
|
"""CTP OnRspQryInstrumentCommissionRate → fee_rates 字段。"""
|
||||||
|
mult = int(get_contract_spec(ths_code)["mult"])
|
||||||
|
exchange = str(data.get("ExchangeID") or "").strip()
|
||||||
|
return {
|
||||||
|
"exchange": exchange,
|
||||||
|
"mult": mult,
|
||||||
|
"open_fixed": float(data.get("OpenRatioByVolume") or 0),
|
||||||
|
"open_ratio": float(data.get("OpenRatioByMoney") or 0),
|
||||||
|
"close_yesterday_fixed": float(data.get("CloseRatioByVolume") or 0),
|
||||||
|
"close_yesterday_ratio": float(data.get("CloseRatioByMoney") or 0),
|
||||||
|
"close_today_fixed": float(data.get("CloseTodayRatioByVolume") or 0),
|
||||||
|
"close_today_ratio": float(data.get("CloseTodayRatioByMoney") or 0),
|
||||||
|
"source": "ctp",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sync_fees_from_ctp(mode: str, *, max_symbols: int = 80) -> tuple[int, str]:
|
||||||
|
"""CTP 已连接时,按主力合约查询手续费并写入 fee_rates(source=ctp)。"""
|
||||||
|
bridge = get_bridge()
|
||||||
|
if not bridge.available():
|
||||||
|
return 0, "vnpy 未安装"
|
||||||
|
if bridge.connected_mode != mode:
|
||||||
|
return 0, "请先连接 CTP"
|
||||||
|
if not bridge.ping():
|
||||||
|
return 0, "CTP 连接无效,请重连"
|
||||||
|
|
||||||
|
from symbols import list_main_contracts_grouped
|
||||||
|
|
||||||
|
mains = list_main_contracts_grouped()
|
||||||
|
symbols: list[str] = []
|
||||||
|
for g in mains:
|
||||||
|
ths = (g.get("ths") or g.get("code") or "").strip()
|
||||||
|
if ths:
|
||||||
|
symbols.append(ths)
|
||||||
|
symbols = symbols[:max_symbols]
|
||||||
|
|
||||||
|
if not symbols:
|
||||||
|
return 0, "无主力合约列表"
|
||||||
|
|
||||||
|
seen: set[str] = set()
|
||||||
|
ok = 0
|
||||||
|
errors = 0
|
||||||
|
for ths in symbols:
|
||||||
|
product = _product_from_instrument(ths)
|
||||||
|
if not product or product in seen:
|
||||||
|
continue
|
||||||
|
seen.add(product)
|
||||||
|
try:
|
||||||
|
raw = bridge.query_instrument_commission(ths, mode=mode)
|
||||||
|
if not raw:
|
||||||
|
errors += 1
|
||||||
|
continue
|
||||||
|
fields = ctp_commission_to_fee_fields(raw, ths)
|
||||||
|
upsert_fee_rate(product, fields)
|
||||||
|
ok += 1
|
||||||
|
time.sleep(0.35)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("CTP fee sync %s: %s", ths, exc)
|
||||||
|
errors += 1
|
||||||
|
|
||||||
|
if ok == 0:
|
||||||
|
return 0, f"CTP 未返回手续费率(失败 {errors} 次),请确认柜台支持查询"
|
||||||
|
msg = f"已从 CTP 同步 {ok} 个品种手续费"
|
||||||
|
if errors:
|
||||||
|
msg += f"({errors} 个跳过)"
|
||||||
|
return ok, msg
|
||||||
|
|
||||||
|
|
||||||
|
def sync_fee_for_symbol(mode: str, ths_code: str) -> Optional[dict]:
|
||||||
|
"""单品种按需从 CTP 拉取并缓存。"""
|
||||||
|
bridge = get_bridge()
|
||||||
|
if bridge.connected_mode != mode or not bridge.ping():
|
||||||
|
return None
|
||||||
|
raw = bridge.query_instrument_commission(ths_code, mode=mode)
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
product = _product_from_instrument(ths_code)
|
||||||
|
if not product:
|
||||||
|
return None
|
||||||
|
fields = ctp_commission_to_fee_fields(raw, ths_code)
|
||||||
|
upsert_fee_rate(product, fields)
|
||||||
|
return fields
|
||||||
@@ -403,5 +403,6 @@ pm2 restart qihuo
|
|||||||
|
|
||||||
- [功能说明文档](./FEATURES.md)
|
- [功能说明文档](./FEATURES.md)
|
||||||
- [SimNow 注册与接入说明](./SIMNOW.md)
|
- [SimNow 注册与接入说明](./SIMNOW.md)
|
||||||
|
- [手续费与导航设置](./FEES.md)
|
||||||
- [交易与 SimNow 配置](./TRADING.md)
|
- [交易与 SimNow 配置](./TRADING.md)
|
||||||
- [README](../README.md)
|
- [README](../README.md)
|
||||||
|
|||||||
+3
-1
@@ -304,7 +304,9 @@ API:`GET /api/contract_profile?symbol=rb2510` 返回 JSON。
|
|||||||
| `trade_logs` | 平仓交易记录(含 fee、pnl_net) |
|
| `trade_logs` | 平仓交易记录(含 fee、pnl_net) |
|
||||||
| `trade_records` | 计划/关键位自动止盈止损记录 |
|
| `trade_records` | 计划/关键位自动止盈止损记录 |
|
||||||
| `review_records` | 复盘记录(含 fee、pnl_net) |
|
| `review_records` | 复盘记录(含 fee、pnl_net) |
|
||||||
| `fee_rates` | 品种手续费本地配置 |
|
| `fee_rates` | 品种手续费(`source`:ctp / akshare / json / manual) |
|
||||||
|
|
||||||
|
手续费默认 **CTP 柜台** 费率,见 [FEES.md](./FEES.md)。
|
||||||
|
|
||||||
数据库文件:`futures.db`(项目根目录,运行后生成)。
|
数据库文件:`futures.db`(项目根目录,运行后生成)。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 手续费与导航设置
|
||||||
|
|
||||||
|
## 手续费数据源
|
||||||
|
|
||||||
|
| 模式 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **CTP 柜台**(默认) | 连接 SimNow/实盘 CTP 后,通过 `ReqQryInstrumentCommissionRate` 查询柜台费率并缓存到 `fee_rates`(`source=ctp`) |
|
||||||
|
| **本地 / AKShare** | 使用 `data/fee_rates.json` 或 AKShare 交易所参考表 × 倍率,仅作离线估算 |
|
||||||
|
|
||||||
|
### 计算公式
|
||||||
|
|
||||||
|
```
|
||||||
|
单边手续费 = 固定(元/手) × 手数 + 比例 × 成交价 × 合约乘数 × 手数
|
||||||
|
往返手续费 = 开仓费 + 平仓费(同日持仓用平今,否则平昨)
|
||||||
|
```
|
||||||
|
|
||||||
|
CTP 返回字段映射:
|
||||||
|
|
||||||
|
| CTP 字段 | 本地字段 |
|
||||||
|
|----------|----------|
|
||||||
|
| OpenRatioByVolume | open_fixed |
|
||||||
|
| OpenRatioByMoney | open_ratio |
|
||||||
|
| CloseRatioByVolume | close_yesterday_fixed |
|
||||||
|
| CloseRatioByMoney | close_yesterday_ratio |
|
||||||
|
| CloseTodayRatioByVolume | close_today_fixed |
|
||||||
|
| CloseTodayRatioByMoney | close_today_ratio |
|
||||||
|
|
||||||
|
### 同步时机
|
||||||
|
|
||||||
|
1. **连接 CTP 成功后** — 后台自动同步主力合约费率(约 60 个品种)
|
||||||
|
2. **手续费配置页** — 点击「从 CTP 同步费率」
|
||||||
|
3. **按需查询** — 计算某品种手续费时,若缓存无 CTP 费率则单品种查询
|
||||||
|
|
||||||
|
### 配置路径
|
||||||
|
|
||||||
|
- 系统设置 → **手续费配置**(可在导航中开关显示)
|
||||||
|
- 选择「计费依据」→ 保存
|
||||||
|
- CTP 已连接时点击「从 CTP 同步费率」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 导航显示开关
|
||||||
|
|
||||||
|
**系统设置 → 导航显示** 可单独开关以下顶栏入口:
|
||||||
|
|
||||||
|
| 开关 key | 菜单名 |
|
||||||
|
|----------|--------|
|
||||||
|
| `fees` | 手续费配置 |
|
||||||
|
| `contract` | 品种简介 |
|
||||||
|
| `plans` | 开单计划 |
|
||||||
|
| `market` | 行情K线 |
|
||||||
|
| `strategy` | 策略交易 |
|
||||||
|
|
||||||
|
关闭后:
|
||||||
|
|
||||||
|
- 顶栏不显示该链接
|
||||||
|
- 直接访问对应 URL 会提示并跳转到 **持仓监控**
|
||||||
|
|
||||||
|
始终显示的入口:持仓监控、关键位监控、交易记录与复盘、统计分析、系统设置。
|
||||||
|
|
||||||
|
设置保存在 SQLite `settings.nav_items`(JSON)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `fee_specs.py` | 费率查询与计算 |
|
||||||
|
| `ctp_fee_sync.py` | CTP 费率同步 |
|
||||||
|
| `nav_settings.py` | 导航开关 |
|
||||||
|
| `vnpy_bridge.py` | CTP 连接与费率查询 |
|
||||||
|
|
||||||
|
详见 [DEPLOY.md](./DEPLOY.md)、[TRADING.md](./TRADING.md)。
|
||||||
+86
-29
@@ -1,4 +1,4 @@
|
|||||||
"""期货手续费:本地费率表 + 开平合计估算(模拟盘参考)。"""
|
"""期货手续费:优先 CTP 柜台费率,本地/AKShare 为离线兜底。"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -51,31 +51,79 @@ def get_fee_multiplier() -> float:
|
|||||||
return 2.0
|
return 2.0
|
||||||
|
|
||||||
|
|
||||||
def get_fee_spec(ths_code: str) -> dict:
|
def get_fee_source_mode() -> str:
|
||||||
|
"""ctp=优先柜台同步费率;local=本地/AKShare 表。"""
|
||||||
|
conn = _get_db()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT value FROM settings WHERE key='fee_source_mode'"
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
mode = (row["value"] if row else "ctp") or "ctp"
|
||||||
|
return mode if mode in ("ctp", "local") else "ctp"
|
||||||
|
|
||||||
|
|
||||||
|
def _row_to_spec(row, mult: int) -> dict:
|
||||||
|
return {
|
||||||
|
"product": row["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),
|
||||||
|
"source": row["source"] if "source" in row.keys() else "local",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_fee_spec(ths_code: str, *, trading_mode: str = "simulation") -> dict:
|
||||||
product = product_from_code(ths_code)
|
product = product_from_code(ths_code)
|
||||||
if not product:
|
if not product:
|
||||||
spec = get_contract_spec(ths_code)
|
spec = get_contract_spec(ths_code)
|
||||||
return {**DEFAULT_FEE, "mult": spec["mult"], "product": "", "exchange": ""}
|
return {**DEFAULT_FEE, "mult": spec["mult"], "product": "", "exchange": "", "source": "default"}
|
||||||
|
|
||||||
conn = _get_db()
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT * FROM fee_rates WHERE product=?", (product,)
|
|
||||||
).fetchone()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
mult = get_contract_spec(ths_code)["mult"]
|
mult = get_contract_spec(ths_code)["mult"]
|
||||||
if row:
|
source_mode = get_fee_source_mode()
|
||||||
return {
|
conn = _get_db()
|
||||||
"product": product,
|
|
||||||
"exchange": row["exchange"] or "",
|
if source_mode == "ctp":
|
||||||
"mult": int(row["mult"] or mult),
|
row = conn.execute(
|
||||||
"open_fixed": float(row["open_fixed"] or 0),
|
"SELECT * FROM fee_rates WHERE product=? AND source='ctp'",
|
||||||
"open_ratio": float(row["open_ratio"] or 0),
|
(product,),
|
||||||
"close_yesterday_fixed": float(row["close_yesterday_fixed"] or 0),
|
).fetchone()
|
||||||
"close_yesterday_ratio": float(row["close_yesterday_ratio"] or 0),
|
if not row:
|
||||||
"close_today_fixed": float(row["close_today_fixed"] or 0),
|
row = conn.execute(
|
||||||
"close_today_ratio": float(row["close_today_ratio"] or 0),
|
"SELECT * FROM fee_rates WHERE product=? ORDER BY CASE source WHEN 'ctp' THEN 0 ELSE 1 END",
|
||||||
}
|
(product,),
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if row:
|
||||||
|
return _row_to_spec(row, mult)
|
||||||
|
# 按需向 CTP 查询
|
||||||
|
try:
|
||||||
|
from ctp_fee_sync import sync_fee_for_symbol
|
||||||
|
fields = sync_fee_for_symbol(trading_mode, ths_code)
|
||||||
|
if fields:
|
||||||
|
return {"product": product, **fields}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
conn = _get_db()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM fee_rates WHERE product=?", (product,)
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if row:
|
||||||
|
spec = _row_to_spec(row, mult)
|
||||||
|
spec["source"] = spec.get("source") or "local_fallback"
|
||||||
|
return spec
|
||||||
|
else:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM fee_rates WHERE product=?", (product,)
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if row:
|
||||||
|
return _row_to_spec(row, mult)
|
||||||
|
|
||||||
if product in _INDEX_PRODUCTS:
|
if product in _INDEX_PRODUCTS:
|
||||||
return {
|
return {
|
||||||
@@ -95,6 +143,7 @@ def get_fee_spec(ths_code: str) -> dict:
|
|||||||
"exchange": "",
|
"exchange": "",
|
||||||
"mult": mult,
|
"mult": mult,
|
||||||
**DEFAULT_FEE,
|
**DEFAULT_FEE,
|
||||||
|
"source": "default",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -126,10 +175,11 @@ def calc_round_trip_fee(
|
|||||||
lots: float,
|
lots: float,
|
||||||
open_time: str = "",
|
open_time: str = "",
|
||||||
close_time: str = "",
|
close_time: str = "",
|
||||||
|
trading_mode: str = "simulation",
|
||||||
) -> float:
|
) -> float:
|
||||||
if not entry_price or not close_price:
|
if not entry_price or not close_price:
|
||||||
return 0.0
|
return 0.0
|
||||||
spec = get_fee_spec(ths_code)
|
spec = get_fee_spec(ths_code, trading_mode=trading_mode)
|
||||||
mult = spec["mult"]
|
mult = spec["mult"]
|
||||||
lots = lots or 1.0
|
lots = lots or 1.0
|
||||||
|
|
||||||
@@ -157,8 +207,9 @@ def calc_fee_breakdown(
|
|||||||
lots: float,
|
lots: float,
|
||||||
open_time: str = "",
|
open_time: str = "",
|
||||||
close_time: str = "",
|
close_time: str = "",
|
||||||
|
trading_mode: str = "simulation",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
spec = get_fee_spec(ths_code)
|
spec = get_fee_spec(ths_code, trading_mode=trading_mode)
|
||||||
mult = spec["mult"]
|
mult = spec["mult"]
|
||||||
lots = lots or 1.0
|
lots = lots or 1.0
|
||||||
open_fee = calc_side_fee(
|
open_fee = calc_side_fee(
|
||||||
@@ -184,6 +235,7 @@ def calc_fee_breakdown(
|
|||||||
"close_type": close_type,
|
"close_type": close_type,
|
||||||
"total_fee": total,
|
"total_fee": total,
|
||||||
"same_day": same_day,
|
"same_day": same_day,
|
||||||
|
"fee_source": spec.get("source", "local"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -204,8 +256,8 @@ def load_fee_rates_from_json(path: Optional[str] = None) -> int:
|
|||||||
(product, exchange, mult,
|
(product, exchange, mult,
|
||||||
open_fixed, open_ratio,
|
open_fixed, open_ratio,
|
||||||
close_yesterday_fixed, close_yesterday_ratio,
|
close_yesterday_fixed, close_yesterday_ratio,
|
||||||
close_today_fixed, close_today_ratio, updated_at)
|
close_today_fixed, close_today_ratio, updated_at, source)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||||
ON CONFLICT(product) DO UPDATE SET
|
ON CONFLICT(product) DO UPDATE SET
|
||||||
exchange=excluded.exchange, mult=excluded.mult,
|
exchange=excluded.exchange, mult=excluded.mult,
|
||||||
open_fixed=excluded.open_fixed, open_ratio=excluded.open_ratio,
|
open_fixed=excluded.open_fixed, open_ratio=excluded.open_ratio,
|
||||||
@@ -213,7 +265,8 @@ def load_fee_rates_from_json(path: Optional[str] = None) -> int:
|
|||||||
close_yesterday_ratio=excluded.close_yesterday_ratio,
|
close_yesterday_ratio=excluded.close_yesterday_ratio,
|
||||||
close_today_fixed=excluded.close_today_fixed,
|
close_today_fixed=excluded.close_today_fixed,
|
||||||
close_today_ratio=excluded.close_today_ratio,
|
close_today_ratio=excluded.close_today_ratio,
|
||||||
updated_at=excluded.updated_at""",
|
updated_at=excluded.updated_at,
|
||||||
|
source=excluded.source""",
|
||||||
(
|
(
|
||||||
product.lower(),
|
product.lower(),
|
||||||
item.get("exchange", ""),
|
item.get("exchange", ""),
|
||||||
@@ -225,6 +278,7 @@ def load_fee_rates_from_json(path: Optional[str] = None) -> int:
|
|||||||
float(item.get("close_today_fixed") or 0),
|
float(item.get("close_today_fixed") or 0),
|
||||||
float(item.get("close_today_ratio") or 0),
|
float(item.get("close_today_ratio") or 0),
|
||||||
now,
|
now,
|
||||||
|
item.get("source", "json"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
count += 1
|
count += 1
|
||||||
@@ -246,13 +300,14 @@ def upsert_fee_rate(product: str, fields: dict) -> None:
|
|||||||
product = product.lower().strip()
|
product = product.lower().strip()
|
||||||
conn = _get_db()
|
conn = _get_db()
|
||||||
now = datetime.now().isoformat(timespec="seconds")
|
now = datetime.now().isoformat(timespec="seconds")
|
||||||
|
source = fields.get("source", "manual")
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO fee_rates
|
"""INSERT INTO fee_rates
|
||||||
(product, exchange, mult,
|
(product, exchange, mult,
|
||||||
open_fixed, open_ratio,
|
open_fixed, open_ratio,
|
||||||
close_yesterday_fixed, close_yesterday_ratio,
|
close_yesterday_fixed, close_yesterday_ratio,
|
||||||
close_today_fixed, close_today_ratio, updated_at)
|
close_today_fixed, close_today_ratio, updated_at, source)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||||
ON CONFLICT(product) DO UPDATE SET
|
ON CONFLICT(product) DO UPDATE SET
|
||||||
exchange=excluded.exchange, mult=excluded.mult,
|
exchange=excluded.exchange, mult=excluded.mult,
|
||||||
open_fixed=excluded.open_fixed, open_ratio=excluded.open_ratio,
|
open_fixed=excluded.open_fixed, open_ratio=excluded.open_ratio,
|
||||||
@@ -260,7 +315,8 @@ def upsert_fee_rate(product: str, fields: dict) -> None:
|
|||||||
close_yesterday_ratio=excluded.close_yesterday_ratio,
|
close_yesterday_ratio=excluded.close_yesterday_ratio,
|
||||||
close_today_fixed=excluded.close_today_fixed,
|
close_today_fixed=excluded.close_today_fixed,
|
||||||
close_today_ratio=excluded.close_today_ratio,
|
close_today_ratio=excluded.close_today_ratio,
|
||||||
updated_at=excluded.updated_at""",
|
updated_at=excluded.updated_at,
|
||||||
|
source=excluded.source""",
|
||||||
(
|
(
|
||||||
product,
|
product,
|
||||||
fields.get("exchange", ""),
|
fields.get("exchange", ""),
|
||||||
@@ -272,6 +328,7 @@ def upsert_fee_rate(product: str, fields: dict) -> None:
|
|||||||
float(fields.get("close_today_fixed") or 0),
|
float(fields.get("close_today_fixed") or 0),
|
||||||
float(fields.get("close_today_ratio") or 0),
|
float(fields.get("close_today_ratio") or 0),
|
||||||
now,
|
now,
|
||||||
|
source,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ def _parse_akshare_row(row: dict, multiplier: float) -> Optional[dict]:
|
|||||||
"close_yesterday_ratio": round(close_y_ratio * multiplier, 8),
|
"close_yesterday_ratio": round(close_y_ratio * multiplier, 8),
|
||||||
"close_today_fixed": round(close_t_fixed * multiplier, 6),
|
"close_today_fixed": round(close_t_fixed * multiplier, 6),
|
||||||
"close_today_ratio": round(close_t_ratio * multiplier, 8),
|
"close_today_ratio": round(close_t_ratio * multiplier, 8),
|
||||||
|
"source": "akshare",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -54,8 +54,9 @@ from vnpy_bridge import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def install_trading(app, *, login_required, get_db, get_setting, set_setting, fetch_price, send_wechat_msg):
|
def install_trading(app, *, login_required, require_nav, get_db, get_setting, set_setting, fetch_price, send_wechat_msg):
|
||||||
"""注册交易相关路由。"""
|
"""注册交易相关路由。"""
|
||||||
|
_nav = require_nav
|
||||||
|
|
||||||
def _settings_dict() -> dict:
|
def _settings_dict() -> dict:
|
||||||
return {
|
return {
|
||||||
@@ -291,6 +292,7 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
|
|||||||
|
|
||||||
@app.route("/strategy")
|
@app.route("/strategy")
|
||||||
@login_required
|
@login_required
|
||||||
|
@_nav("strategy")
|
||||||
def strategy_page():
|
def strategy_page():
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
init_strategy_tables(conn)
|
init_strategy_tables(conn)
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""顶栏导航项显示开关(系统设置)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
# 可在系统设置中开关的导航项
|
||||||
|
NAV_TOGGLES: dict[str, str] = {
|
||||||
|
"fees": "手续费配置",
|
||||||
|
"contract": "品种简介",
|
||||||
|
"plans": "开单计划",
|
||||||
|
"market": "行情K线",
|
||||||
|
"strategy": "策略交易",
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_NAV: dict[str, bool] = {k: True for k in NAV_TOGGLES}
|
||||||
|
|
||||||
|
|
||||||
|
def get_nav_items(get_setting: Callable[[str, str], str]) -> dict[str, bool]:
|
||||||
|
raw = (get_setting("nav_items", "") or "").strip()
|
||||||
|
out = dict(DEFAULT_NAV)
|
||||||
|
if not raw:
|
||||||
|
return out
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
for k in NAV_TOGGLES:
|
||||||
|
if k in data:
|
||||||
|
out[k] = bool(data[k])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def save_nav_items(set_setting: Callable[[str, str], None], items: dict[str, bool]) -> None:
|
||||||
|
merged = dict(DEFAULT_NAV)
|
||||||
|
for k in NAV_TOGGLES:
|
||||||
|
if k in items:
|
||||||
|
merged[k] = bool(items[k])
|
||||||
|
set_setting("nav_items", json.dumps(merged, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
def nav_enabled(get_setting: Callable[[str, str], str], key: str) -> bool:
|
||||||
|
if key not in NAV_TOGGLES:
|
||||||
|
return True
|
||||||
|
return get_nav_items(get_setting).get(key, True)
|
||||||
+5
-5
@@ -484,14 +484,14 @@
|
|||||||
<button type="button" class="nav-backdrop" id="nav-backdrop" aria-label="关闭菜单" hidden></button>
|
<button type="button" class="nav-backdrop" id="nav-backdrop" aria-label="关闭菜单" hidden></button>
|
||||||
<nav class="site-nav" id="site-nav">
|
<nav class="site-nav" id="site-nav">
|
||||||
<a href="{{ url_for('positions') }}" class="{% if request.endpoint in ('positions', 'trade_page', 'recommend_page') %}active{% endif %}">持仓监控</a>
|
<a href="{{ url_for('positions') }}" class="{% if request.endpoint in ('positions', 'trade_page', 'recommend_page') %}active{% endif %}">持仓监控</a>
|
||||||
<a href="{{ url_for('strategy_page') }}" class="{% if request.endpoint in ('strategy_page', 'strategy_records_page') %}active{% endif %}">策略交易</a>
|
{% if nav_items.strategy %}<a href="{{ url_for('strategy_page') }}" class="{% if request.endpoint in ('strategy_page', 'strategy_records_page') %}active{% endif %}">策略交易</a>{% endif %}
|
||||||
<a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a>
|
{% if nav_items.plans %}<a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a>{% endif %}
|
||||||
<a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a>
|
<a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a>
|
||||||
<a href="{{ url_for('market_page') }}" class="{% if request.endpoint == 'market_page' %}active{% endif %}">行情K线</a>
|
{% if nav_items.market %}<a href="{{ url_for('market_page') }}" class="{% if request.endpoint == 'market_page' %}active{% endif %}">行情K线</a>{% endif %}
|
||||||
<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>
|
{% if nav_items.fees %}<a href="{{ url_for('fees') }}" class="{% if request.endpoint == 'fees' %}active{% endif %}">手续费配置</a>{% endif %}
|
||||||
<a href="{{ url_for('contract_profile_page') }}" class="{% if request.endpoint == 'contract_profile_page' %}active{% endif %}">品种简介</a>
|
{% if nav_items.contract %}<a href="{{ url_for('contract_profile_page') }}" class="{% if request.endpoint == 'contract_profile_page' %}active{% endif %}">品种简介</a>{% endif %}
|
||||||
<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>
|
||||||
|
|||||||
+40
-8
@@ -2,7 +2,37 @@
|
|||||||
{% block title %}手续费配置 - 国内期货监控系统{% endblock %}
|
{% block title %}手续费配置 - 国内期货监控系统{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>手续费倍率</h2>
|
<h2>手续费数据源</h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ url_for('fees') }}" method="post" class="form-row" style="flex-wrap:wrap;gap:.75rem;align-items:center">
|
||||||
|
<input type="hidden" name="action" value="fee_source">
|
||||||
|
<label class="text-muted" style="font-size:.85rem">计费依据</label>
|
||||||
|
<select name="fee_source_mode" style="min-width:220px">
|
||||||
|
<option value="ctp" {% if fee_source_mode == 'ctp' %}selected{% endif %}>CTP 柜台(SimNow/实盘,推荐)</option>
|
||||||
|
<option value="local" {% if fee_source_mode == 'local' %}selected{% endif %}>本地 / AKShare 参考表</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="btn-primary">保存</button>
|
||||||
|
</form>
|
||||||
|
<p class="hint" style="margin-top:.75rem">
|
||||||
|
默认使用 <strong>CTP 柜台</strong> 查询到的开仓/平仓费率(连接 CTP 后自动同步,与 SimNow/期货公司一致)。
|
||||||
|
离线或未连接时可改用本地表估算。
|
||||||
|
</p>
|
||||||
|
<div class="form-row" style="margin-top:.75rem;flex-wrap:wrap;gap:.5rem">
|
||||||
|
<form action="{{ url_for('fees') }}" method="post" style="display:inline">
|
||||||
|
<input type="hidden" name="action" value="sync_ctp">
|
||||||
|
<button type="submit" class="btn-primary" {% if not ctp_connected %}disabled title="请先连接 CTP"{% endif %}>从 CTP 同步费率</button>
|
||||||
|
</form>
|
||||||
|
{% if ctp_connected %}
|
||||||
|
<span class="badge profit">CTP 已连接</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge planned">CTP 未连接 — 请先连接后再同步</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>本地参考倍率(仅「本地数据源」时使用)</h2>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form action="{{ url_for('fees') }}" method="post" class="form-row">
|
<form action="{{ url_for('fees') }}" method="post" class="form-row">
|
||||||
<input type="hidden" name="action" value="multiplier">
|
<input type="hidden" name="action" value="multiplier">
|
||||||
@@ -10,27 +40,26 @@
|
|||||||
<input name="fee_multiplier" type="number" step="0.1" min="0" value="{{ multiplier }}" style="width:100px">
|
<input name="fee_multiplier" type="number" step="0.1" min="0" value="{{ multiplier }}" style="width:100px">
|
||||||
<button type="submit" class="btn-primary">保存倍率</button>
|
<button type="submit" class="btn-primary">保存倍率</button>
|
||||||
</form>
|
</form>
|
||||||
<p class="hint">默认 2 倍:从第三方拉取交易所参考标准后自动乘以该倍率写入本地表。模拟盘估算用,非实盘账单。</p>
|
|
||||||
<div class="form-row" style="margin-top:.75rem">
|
<div class="form-row" style="margin-top:.75rem">
|
||||||
<form action="{{ url_for('fees') }}" method="post" style="display:inline">
|
<form action="{{ url_for('fees') }}" method="post" style="display:inline">
|
||||||
<input type="hidden" name="action" value="sync">
|
<input type="hidden" name="action" value="sync">
|
||||||
<button type="submit" class="btn-primary">从第三方同步(AKShare)</button>
|
<button type="submit" class="btn-secondary">从 AKShare 同步(本地)</button>
|
||||||
</form>
|
</form>
|
||||||
<form action="{{ url_for('fees') }}" method="post" style="display:inline">
|
<form action="{{ url_for('fees') }}" method="post" style="display:inline">
|
||||||
<input type="hidden" name="action" value="reload_json">
|
<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>
|
<button type="submit" class="btn-link" style="padding:.5rem 1rem;border:1px solid var(--card-border);border-radius:8px">重载 JSON 默认表</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>品种费率表(已含倍率)</h2>
|
<h2>品种费率表</h2>
|
||||||
<div class="card-body card-scroll">
|
<div class="card-body card-scroll">
|
||||||
<table class="trade-table">
|
<table class="trade-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><th>平今(比例)</th>
|
<th>平今(元/手)</th><th>平今(比例)</th>
|
||||||
@@ -44,6 +73,7 @@
|
|||||||
<input type="hidden" name="action" value="save_row">
|
<input type="hidden" name="action" value="save_row">
|
||||||
<input type="hidden" name="product" value="{{ r.product }}">
|
<input type="hidden" name="product" value="{{ r.product }}">
|
||||||
<td><strong>{{ r.product }}</strong></td>
|
<td><strong>{{ r.product }}</strong></td>
|
||||||
|
<td><span class="badge {% if r.source == 'ctp' %}profit{% else %}planned{% endif %}">{{ r.source or 'local' }}</span></td>
|
||||||
<td><input name="exchange" value="{{ r.exchange or '' }}" style="width:72px;padding:.3rem"></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="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_fixed" type="number" step="0.0001" value="{{ r.open_fixed }}" style="width:72px;padding:.3rem"></td>
|
||||||
@@ -57,11 +87,13 @@
|
|||||||
</form>
|
</form>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="11" class="text-muted">暂无费率,请同步或重载 JSON</td></tr>
|
<tr><td colspan="12" class="text-muted">暂无费率,请连接 CTP 后同步</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint" style="margin-top:.75rem">比例按「成交价×乘数×手数×比例」计费;元/手为固定每手。开+平合计为一笔往返手续费。</p>
|
<p class="hint" style="margin-top:.75rem">
|
||||||
|
公式:单边手续费 = 固定(元/手)×手数 + 比例×价格×乘数×手数。往返 = 开仓 + 平仓(平今/平昨自动判断)。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,6 +2,23 @@
|
|||||||
{% block title %}系统设置 - 国内期货监控系统{% endblock %}
|
{% block title %}系统设置 - 国内期货监控系统{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>导航显示</h2>
|
||||||
|
<form action="{{ url_for('settings') }}" method="post">
|
||||||
|
<input type="hidden" name="action" value="nav">
|
||||||
|
<p class="hint" style="margin-bottom:.75rem">关闭后顶栏隐藏对应入口,直接访问 URL 也会跳转回持仓监控。</p>
|
||||||
|
<div class="form-grid" style="max-width:640px;grid-template-columns:1fr 1fr">
|
||||||
|
{% for key, label in nav_toggles.items() %}
|
||||||
|
<label class="field" style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
|
||||||
|
<input type="checkbox" name="nav_{{ key }}" {% if nav_items[key] %}checked{% endif %}>
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary" style="margin-top:.75rem">保存导航</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>交易模式</h2>
|
<h2>交易模式</h2>
|
||||||
<form action="{{ url_for('settings') }}" method="post">
|
<form action="{{ url_for('settings') }}" method="post">
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ class CtpBridge:
|
|||||||
self._connected_mode: Optional[str] = None
|
self._connected_mode: Optional[str] = None
|
||||||
self._last_error: str = ""
|
self._last_error: str = ""
|
||||||
self._connect_lock = threading.Lock()
|
self._connect_lock = threading.Lock()
|
||||||
|
self._commission_waiters: dict[int, threading.Event] = {}
|
||||||
|
self._commission_results: dict[int, dict] = {}
|
||||||
|
self._commission_hooked = False
|
||||||
self._init_engine()
|
self._init_engine()
|
||||||
|
|
||||||
def _init_engine(self) -> None:
|
def _init_engine(self) -> None:
|
||||||
@@ -184,6 +187,7 @@ class CtpBridge:
|
|||||||
self._connected_mode = mode
|
self._connected_mode = mode
|
||||||
self._last_error = ""
|
self._last_error = ""
|
||||||
logger.info("CTP 已连接 [%s] account=%s", mode, len(accounts))
|
logger.info("CTP 已连接 [%s] account=%s", mode, len(accounts))
|
||||||
|
self._schedule_fee_sync(mode)
|
||||||
return
|
return
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
finally:
|
finally:
|
||||||
@@ -213,6 +217,71 @@ class CtpBridge:
|
|||||||
def mark_disconnected(self) -> None:
|
def mark_disconnected(self) -> None:
|
||||||
self._connected_mode = None
|
self._connected_mode = None
|
||||||
|
|
||||||
|
def _schedule_fee_sync(self, mode: str) -> None:
|
||||||
|
def _run() -> None:
|
||||||
|
try:
|
||||||
|
from ctp_fee_sync import sync_fees_from_ctp
|
||||||
|
n, msg = sync_fees_from_ctp(mode, max_symbols=60)
|
||||||
|
logger.info("CTP 手续费同步: %s", msg if n else msg)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("CTP 手续费后台同步: %s", exc)
|
||||||
|
|
||||||
|
threading.Thread(target=_run, daemon=True, name="ctp-fee-sync").start()
|
||||||
|
|
||||||
|
def _ensure_commission_callback(self) -> None:
|
||||||
|
if self._commission_hooked or not self._engine:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
gw = self._engine.get_gateway(GATEWAY_NAME)
|
||||||
|
td = gw.td_api
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
bridge = self
|
||||||
|
|
||||||
|
def on_rsp(data: dict, error: dict, reqid: int, last: bool) -> None:
|
||||||
|
if data and data.get("InstrumentID"):
|
||||||
|
bridge._commission_results[reqid] = dict(data)
|
||||||
|
ev = bridge._commission_waiters.get(reqid)
|
||||||
|
if last and ev:
|
||||||
|
ev.set()
|
||||||
|
|
||||||
|
td.onRspQryInstrumentCommissionRate = on_rsp # type: ignore[method-assign]
|
||||||
|
self._commission_hooked = True
|
||||||
|
|
||||||
|
def query_instrument_commission(self, ths_code: str, *, mode: str) -> dict:
|
||||||
|
"""查询单合约 CTP 手续费率(需已连接)。"""
|
||||||
|
if self._connected_mode != mode or not self._engine:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
from ctp_symbol import ths_to_vnpy_symbol
|
||||||
|
sym, _ = ths_to_vnpy_symbol(ths_code)
|
||||||
|
gw = self._engine.get_gateway(GATEWAY_NAME)
|
||||||
|
td = gw.td_api
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("commission query init: %s", exc)
|
||||||
|
return {}
|
||||||
|
if not getattr(td, "login_status", False):
|
||||||
|
return {}
|
||||||
|
if not hasattr(td, "reqQryInstrumentCommissionRate"):
|
||||||
|
return {}
|
||||||
|
self._ensure_commission_callback()
|
||||||
|
reqid = int(getattr(td, "reqid", 0)) + 1
|
||||||
|
td.reqid = reqid
|
||||||
|
ev = threading.Event()
|
||||||
|
self._commission_waiters[reqid] = ev
|
||||||
|
req = {
|
||||||
|
"BrokerID": td.brokerid,
|
||||||
|
"InvestorID": td.userid,
|
||||||
|
"InstrumentID": sym,
|
||||||
|
}
|
||||||
|
ret = td.reqQryInstrumentCommissionRate(req, reqid)
|
||||||
|
if ret != 0:
|
||||||
|
self._commission_waiters.pop(reqid, None)
|
||||||
|
return {}
|
||||||
|
ev.wait(timeout=8)
|
||||||
|
self._commission_waiters.pop(reqid, None)
|
||||||
|
return self._commission_results.pop(reqid, {})
|
||||||
|
|
||||||
def get_account(self) -> dict[str, Any]:
|
def get_account(self) -> dict[str, Any]:
|
||||||
if not self._engine:
|
if not self._engine:
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
Reference in New Issue
Block a user