feat: 手续费仅CTP每日后台同步入库,前端只读展示
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -29,15 +29,10 @@ 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_source_mode,
|
|
||||||
list_all_fee_rates,
|
|
||||||
list_fee_rates_for_ui,
|
list_fee_rates_for_ui,
|
||||||
count_fee_rates_by_source,
|
count_fee_rates_by_source,
|
||||||
load_fee_rates_from_json,
|
purge_non_ctp_fee_rates,
|
||||||
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 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
|
||||||
@@ -376,11 +371,11 @@ def init_db():
|
|||||||
set_setting("risk_percent", "1")
|
set_setting("risk_percent", "1")
|
||||||
if not get_setting("fee_source_mode"):
|
if not get_setting("fee_source_mode"):
|
||||||
set_setting("fee_source_mode", "ctp")
|
set_setting("fee_source_mode", "ctp")
|
||||||
conn = get_db()
|
set_setting("fee_source_mode", "ctp")
|
||||||
fee_cnt = conn.execute("SELECT COUNT(*) FROM fee_rates").fetchone()[0]
|
try:
|
||||||
conn.close()
|
purge_non_ctp_fee_rates()
|
||||||
if fee_cnt == 0:
|
except Exception:
|
||||||
load_fee_rates_from_json()
|
pass
|
||||||
|
|
||||||
|
|
||||||
def sync_admin_from_env():
|
def sync_admin_from_env():
|
||||||
@@ -1565,66 +1560,32 @@ def api_contract_profile():
|
|||||||
@require_nav("fees")
|
@require_nav("fees")
|
||||||
def fees():
|
def fees():
|
||||||
from trading_context import get_trading_mode
|
from trading_context import get_trading_mode
|
||||||
from ctp_fee_sync import sync_fees_from_ctp
|
from ctp_fee_worker import try_daily_ctp_fee_sync, get_fee_last_sync, fees_synced_today
|
||||||
from vnpy_bridge import ctp_status
|
from vnpy_bridge import ctp_status
|
||||||
|
|
||||||
mode = get_trading_mode(get_setting)
|
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 == "fee_source":
|
if action == "sync_ctp":
|
||||||
fs = request.form.get("fee_source_mode", "ctp").strip()
|
force = request.form.get("force") == "1"
|
||||||
set_setting("fee_source_mode", fs if fs in ("ctp", "local") else "ctp")
|
count, msg = try_daily_ctp_fee_sync(
|
||||||
flash("手续费数据源已保存")
|
mode,
|
||||||
elif action == "sync_ctp":
|
get_setting=get_setting,
|
||||||
count, msg = sync_fees_from_ctp(mode)
|
set_setting=set_setting,
|
||||||
|
force=force,
|
||||||
|
)
|
||||||
flash(msg)
|
flash(msg)
|
||||||
elif 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),
|
|
||||||
"source": "manual",
|
|
||||||
})
|
|
||||||
flash(f"已保存 {product} 费率")
|
|
||||||
return redirect(url_for("fees"))
|
return redirect(url_for("fees"))
|
||||||
|
|
||||||
rates = list_fee_rates_for_ui()
|
rates = list_fee_rates_for_ui()
|
||||||
fee_counts = count_fee_rates_by_source()
|
fee_counts = count_fee_rates_by_source()
|
||||||
multiplier = get_setting("fee_multiplier", "2")
|
|
||||||
fee_source_mode = get_fee_source_mode()
|
|
||||||
ctp_st = ctp_status(mode)
|
ctp_st = ctp_status(mode)
|
||||||
return render_template(
|
return render_template(
|
||||||
"fees.html",
|
"fees.html",
|
||||||
rates=rates,
|
rates=rates,
|
||||||
fee_counts=fee_counts,
|
fee_counts=fee_counts,
|
||||||
multiplier=multiplier,
|
fee_last_sync=get_fee_last_sync(get_setting),
|
||||||
fee_source_mode=fee_source_mode,
|
fee_synced_today=fees_synced_today(get_setting),
|
||||||
ctp_connected=bool(ctp_st.get("connected")),
|
ctp_connected=bool(ctp_st.get("connected")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -58,7 +58,7 @@ def _collect_main_ths_codes() -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def sync_fees_from_ctp(mode: str, *, max_symbols: int = 80) -> tuple[int, str]:
|
def sync_fees_from_ctp(mode: str, *, max_symbols: int = 80) -> tuple[int, str]:
|
||||||
"""CTP 已连接时,按主力合约查询手续费并写入 fee_rates(source=ctp)。"""
|
"""CTP 已连接时查询手续费并写入 fee_rates(source=ctp,覆盖同品种旧数据)。"""
|
||||||
bridge = get_bridge()
|
bridge = get_bridge()
|
||||||
if not bridge.available():
|
if not bridge.available():
|
||||||
return 0, "vnpy 未安装"
|
return 0, "vnpy 未安装"
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""CTP 手续费后台同步:每日一次写入数据库,前端只读展示。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import date, datetime
|
||||||
|
from typing import Callable, Optional
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TZ = ZoneInfo("Asia/Shanghai")
|
||||||
|
FEE_SYNC_KEY = "ctp_fee_last_sync"
|
||||||
|
CHECK_INTERVAL_SEC = 3600
|
||||||
|
_sync_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _today_str() -> str:
|
||||||
|
return datetime.now(TZ).date().isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def get_fee_last_sync(get_setting: Callable[[str, str], str]) -> str:
|
||||||
|
return (get_setting(FEE_SYNC_KEY, "") or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def fees_synced_today(get_setting: Callable[[str, str], str]) -> bool:
|
||||||
|
last = get_fee_last_sync(get_setting)
|
||||||
|
return bool(last) and last[:10] == _today_str()
|
||||||
|
|
||||||
|
|
||||||
|
def mark_fees_synced(set_setting: Callable[[str, str], None]) -> None:
|
||||||
|
set_setting(FEE_SYNC_KEY, datetime.now(TZ).isoformat(timespec="seconds"))
|
||||||
|
|
||||||
|
|
||||||
|
def try_daily_ctp_fee_sync(
|
||||||
|
mode: str,
|
||||||
|
*,
|
||||||
|
get_setting: Callable[[str, str], str],
|
||||||
|
set_setting: Callable[[str, str], None],
|
||||||
|
force: bool = False,
|
||||||
|
) -> tuple[int, str]:
|
||||||
|
"""CTP 已连接且今日未同步时拉取费率入库;force=True 忽略日期限制。"""
|
||||||
|
if not force and fees_synced_today(get_setting):
|
||||||
|
return 0, "今日已从 CTP 同步过,无需重复(可点「立即同步」强制刷新)"
|
||||||
|
|
||||||
|
with _sync_lock:
|
||||||
|
if not force and fees_synced_today(get_setting):
|
||||||
|
return 0, "今日已从 CTP 同步过"
|
||||||
|
|
||||||
|
from ctp_fee_sync import sync_fees_from_ctp
|
||||||
|
|
||||||
|
count, msg = sync_fees_from_ctp(mode)
|
||||||
|
if count > 0:
|
||||||
|
mark_fees_synced(set_setting)
|
||||||
|
logger.info("CTP 手续费每日同步: %s", msg)
|
||||||
|
elif force:
|
||||||
|
logger.warning("CTP 手续费强制同步未写入: %s", msg)
|
||||||
|
return count, msg
|
||||||
|
|
||||||
|
|
||||||
|
def start_ctp_fee_worker(
|
||||||
|
*,
|
||||||
|
get_mode_fn: Callable[[], str],
|
||||||
|
get_setting_fn: Callable[[str, str], str],
|
||||||
|
set_setting_fn: Callable[[str, str], None],
|
||||||
|
interval: int = CHECK_INTERVAL_SEC,
|
||||||
|
) -> None:
|
||||||
|
"""后台线程:每小时检查,CTP 已连接且当日未同步则自动同步。"""
|
||||||
|
|
||||||
|
def _loop() -> None:
|
||||||
|
time.sleep(20)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
from vnpy_bridge import ctp_status
|
||||||
|
|
||||||
|
mode = get_mode_fn()
|
||||||
|
st = ctp_status(mode)
|
||||||
|
if st.get("connected") and not fees_synced_today(get_setting_fn):
|
||||||
|
try_daily_ctp_fee_sync(
|
||||||
|
mode,
|
||||||
|
get_setting=get_setting_fn,
|
||||||
|
set_setting=set_setting_fn,
|
||||||
|
force=False,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("CTP fee worker: %s", exc)
|
||||||
|
time.sleep(max(300, interval))
|
||||||
|
|
||||||
|
threading.Thread(target=_loop, daemon=True, name="ctp-fee-worker").start()
|
||||||
+62
-63
@@ -1,4 +1,4 @@
|
|||||||
"""期货手续费:优先 CTP 柜台费率,本地/AKShare 为离线兜底。"""
|
"""期货手续费:仅 CTP 柜台同步入库,前端只读展示。"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -37,6 +37,26 @@ def _get_db():
|
|||||||
return connect_db()
|
return connect_db()
|
||||||
|
|
||||||
|
|
||||||
|
def get_setting(key: str, default: str = "") -> str:
|
||||||
|
conn = _get_db()
|
||||||
|
row = conn.execute("SELECT value FROM settings WHERE key=?", (key,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if not row:
|
||||||
|
return default
|
||||||
|
return (row["value"] or default) if row["value"] is not None else default
|
||||||
|
|
||||||
|
|
||||||
|
def set_setting(key: str, value: str) -> None:
|
||||||
|
conn = _get_db()
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO settings (key, value) VALUES (?,?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value""",
|
||||||
|
(key, value),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def get_fee_multiplier() -> float:
|
def get_fee_multiplier() -> float:
|
||||||
conn = _get_db()
|
conn = _get_db()
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
@@ -52,14 +72,20 @@ def get_fee_multiplier() -> float:
|
|||||||
|
|
||||||
|
|
||||||
def get_fee_source_mode() -> str:
|
def get_fee_source_mode() -> str:
|
||||||
"""ctp=优先柜台同步费率;local=本地/AKShare 表。"""
|
"""固定 CTP 柜台。"""
|
||||||
|
return "ctp"
|
||||||
|
|
||||||
|
|
||||||
|
def purge_non_ctp_fee_rates() -> int:
|
||||||
|
"""删除非 CTP 来源的费率缓存。"""
|
||||||
conn = _get_db()
|
conn = _get_db()
|
||||||
row = conn.execute(
|
cur = conn.execute(
|
||||||
"SELECT value FROM settings WHERE key='fee_source_mode'"
|
"DELETE FROM fee_rates WHERE COALESCE(source, '') != 'ctp'"
|
||||||
).fetchone()
|
)
|
||||||
|
n = cur.rowcount
|
||||||
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
mode = (row["value"] if row else "ctp") or "ctp"
|
return n
|
||||||
return mode if mode in ("ctp", "local") else "ctp"
|
|
||||||
|
|
||||||
|
|
||||||
def _row_to_spec(row, mult: int) -> dict:
|
def _row_to_spec(row, mult: int) -> dict:
|
||||||
@@ -84,46 +110,21 @@ def get_fee_spec(ths_code: str, *, trading_mode: str = "simulation") -> dict:
|
|||||||
return {**DEFAULT_FEE, "mult": spec["mult"], "product": "", "exchange": "", "source": "default"}
|
return {**DEFAULT_FEE, "mult": spec["mult"], "product": "", "exchange": "", "source": "default"}
|
||||||
|
|
||||||
mult = get_contract_spec(ths_code)["mult"]
|
mult = get_contract_spec(ths_code)["mult"]
|
||||||
source_mode = get_fee_source_mode()
|
|
||||||
conn = _get_db()
|
conn = _get_db()
|
||||||
|
row = conn.execute(
|
||||||
if source_mode == "ctp":
|
"SELECT * FROM fee_rates WHERE product=? AND source='ctp'",
|
||||||
row = conn.execute(
|
(product,),
|
||||||
"SELECT * FROM fee_rates WHERE product=? AND source='ctp'",
|
).fetchone()
|
||||||
(product,),
|
conn.close()
|
||||||
).fetchone()
|
if row:
|
||||||
if not row:
|
return _row_to_spec(row, mult)
|
||||||
row = conn.execute(
|
try:
|
||||||
"SELECT * FROM fee_rates WHERE product=? ORDER BY CASE source WHEN 'ctp' THEN 0 ELSE 1 END",
|
from ctp_fee_sync import sync_fee_for_symbol
|
||||||
(product,),
|
fields = sync_fee_for_symbol(trading_mode, ths_code)
|
||||||
).fetchone()
|
if fields:
|
||||||
conn.close()
|
return {"product": product, **fields}
|
||||||
if row:
|
except Exception:
|
||||||
return _row_to_spec(row, mult)
|
pass
|
||||||
# 按需向 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 {
|
||||||
@@ -287,6 +288,16 @@ def load_fee_rates_from_json(path: Optional[str] = None) -> int:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def list_ctp_fee_rates() -> list:
|
||||||
|
"""手续费页:仅展示 CTP 同步结果。"""
|
||||||
|
conn = _get_db()
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM fee_rates WHERE source='ctp' ORDER BY product"
|
||||||
|
).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
def list_all_fee_rates() -> list:
|
def list_all_fee_rates() -> list:
|
||||||
conn = _get_db()
|
conn = _get_db()
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
@@ -297,28 +308,16 @@ def list_all_fee_rates() -> list:
|
|||||||
|
|
||||||
|
|
||||||
def list_fee_rates_for_ui() -> list:
|
def list_fee_rates_for_ui() -> list:
|
||||||
"""手续费页展示:CTP 模式下 ctp 来源优先排前。"""
|
return list_ctp_fee_rates()
|
||||||
rows = list_all_fee_rates()
|
|
||||||
if get_fee_source_mode() == "ctp":
|
|
||||||
rows.sort(
|
|
||||||
key=lambda r: (
|
|
||||||
0 if (r.get("source") or "") == "ctp" else 1,
|
|
||||||
r.get("product") or "",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return rows
|
|
||||||
|
|
||||||
|
|
||||||
def count_fee_rates_by_source() -> dict[str, int]:
|
def count_fee_rates_by_source() -> dict[str, int]:
|
||||||
conn = _get_db()
|
conn = _get_db()
|
||||||
rows = conn.execute(
|
n = conn.execute(
|
||||||
"SELECT source, COUNT(*) AS n FROM fee_rates GROUP BY source"
|
"SELECT COUNT(*) FROM fee_rates WHERE source='ctp'"
|
||||||
).fetchall()
|
).fetchone()[0]
|
||||||
conn.close()
|
conn.close()
|
||||||
out: dict[str, int] = {}
|
return {"ctp": int(n or 0)}
|
||||||
for row in rows:
|
|
||||||
out[str(row["source"] or "local")] = int(row["n"] or 0)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def upsert_fee_rate(product: str, fields: dict) -> None:
|
def upsert_fee_rate(product: str, fields: dict) -> None:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from position_sizing import (
|
|||||||
from recommend_store import load_recommend_cache, refresh_recommend_cache
|
from recommend_store import load_recommend_cache, refresh_recommend_cache
|
||||||
from recommend_stream import recommend_hub, start_recommend_worker
|
from recommend_stream import recommend_hub, start_recommend_worker
|
||||||
from ctp_reconnect import start_ctp_reconnect_worker
|
from ctp_reconnect import start_ctp_reconnect_worker
|
||||||
|
from ctp_fee_worker import start_ctp_fee_worker
|
||||||
from risk.account_risk_lib import (
|
from risk.account_risk_lib import (
|
||||||
assert_can_open,
|
assert_can_open,
|
||||||
get_risk_status,
|
get_risk_status,
|
||||||
@@ -1016,3 +1017,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
init_tables_fn=_init_tables,
|
init_tables_fn=_init_tables,
|
||||||
)
|
)
|
||||||
start_ctp_reconnect_worker(get_mode_fn=lambda: get_trading_mode(get_setting))
|
start_ctp_reconnect_worker(get_mode_fn=lambda: get_trading_mode(get_setting))
|
||||||
|
start_ctp_fee_worker(
|
||||||
|
get_mode_fn=lambda: get_trading_mode(get_setting),
|
||||||
|
get_setting_fn=get_setting,
|
||||||
|
set_setting_fn=set_setting,
|
||||||
|
)
|
||||||
|
|||||||
+43
-88
@@ -2,82 +2,46 @@
|
|||||||
{% block title %}手续费配置 - 国内期货监控系统{% endblock %}
|
{% block title %}手续费配置 - 国内期货监控系统{% endblock %}
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<style>
|
<style>
|
||||||
.fees-split{margin-bottom:1.25rem}
|
.fees-status-card .card-body{display:flex;flex-wrap:wrap;gap:.75rem 1.25rem;align-items:center}
|
||||||
.fees-split .card{margin-bottom:0;min-height:auto}
|
.fees-status-card .fees-meta{font-size:.85rem;color:var(--text-muted)}
|
||||||
.fees-table-card .trade-table-wrap{
|
.fees-table-card .trade-table-wrap{
|
||||||
max-height:min(70vh,560px);
|
max-height:min(70vh,560px);
|
||||||
width:100%;
|
width:100%;
|
||||||
border:none;
|
border:none;
|
||||||
border-radius:10px;
|
border-radius:10px;
|
||||||
}
|
}
|
||||||
.fees-table-card .trade-table{min-width:1100px}
|
.fees-table-card .trade-table{min-width:960px}
|
||||||
.fees-table-card .card-body{padding:.75rem 1rem 1rem}
|
.fees-table-card .card-body{padding:.75rem 1rem 1rem}
|
||||||
.fees-source-stats{font-size:.78rem;margin-top:.35rem}
|
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="split-grid fees-split">
|
<div class="card fees-status-card">
|
||||||
<div class="card">
|
<h2>CTP 手续费</h2>
|
||||||
<h2>手续费数据源</h2>
|
<div class="card-body">
|
||||||
<div class="card-body">
|
<p class="fees-meta" style="margin:0;flex:1;min-width:220px">
|
||||||
<form action="{{ url_for('fees') }}" method="post">
|
费率由后台从 <strong>CTP 柜台</strong> 同步写入数据库,<strong>每日自动更新一次</strong>,本页只读展示。
|
||||||
<input type="hidden" name="action" value="fee_source">
|
</p>
|
||||||
<div class="field" style="margin-bottom:.75rem">
|
{% if ctp_connected %}
|
||||||
<label>计费依据</label>
|
<span class="badge profit">CTP 已连接</span>
|
||||||
<select name="fee_source_mode">
|
{% else %}
|
||||||
<option value="ctp" {% if fee_source_mode == 'ctp' %}selected{% endif %}>CTP 柜台(SimNow/实盘,推荐)</option>
|
<span class="badge planned">CTP 未连接</span>
|
||||||
<option value="local" {% if fee_source_mode == 'local' %}selected{% endif %}>本地 / AKShare 参考表</option>
|
{% endif %}
|
||||||
</select>
|
{% if fee_synced_today %}
|
||||||
</div>
|
<span class="badge profit">今日已同步</span>
|
||||||
<button type="submit" class="btn-primary">保存</button>
|
{% else %}
|
||||||
</form>
|
<span class="badge planned">今日未同步</span>
|
||||||
<p class="hint" style="margin-top:.75rem">
|
{% endif %}
|
||||||
默认使用 <strong>CTP 柜台</strong> 费率(连接后自动同步,与 SimNow/期货公司一致)。
|
{% if fee_last_sync %}
|
||||||
</p>
|
<span class="text-muted" style="font-size:.8rem">上次:{{ fee_last_sync[:16] }}</span>
|
||||||
<div class="form-row" style="margin-top:.75rem;flex-wrap:wrap;gap:.5rem">
|
{% endif %}
|
||||||
<form action="{{ url_for('fees') }}" method="post" style="display:inline">
|
{% if fee_counts.get('ctp') %}
|
||||||
<input type="hidden" name="action" value="sync_ctp">
|
<span class="text-muted" style="font-size:.8rem">共 {{ fee_counts.ctp }} 个品种</span>
|
||||||
<button type="submit" class="btn-primary" {% if not ctp_connected %}disabled title="请先连接 CTP"{% endif %}>从 CTP 同步费率</button>
|
{% endif %}
|
||||||
</form>
|
<form action="{{ url_for('fees') }}" method="post" style="display:inline">
|
||||||
{% if ctp_connected %}
|
<input type="hidden" name="action" value="sync_ctp">
|
||||||
<span class="badge profit">CTP 已连接</span>
|
<input type="hidden" name="force" value="1">
|
||||||
{% else %}
|
<button type="submit" class="btn-primary" {% if not ctp_connected %}disabled title="请先连接 CTP"{% endif %}>立即同步</button>
|
||||||
<span class="badge planned">CTP 未连接</span>
|
</form>
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if fee_counts %}
|
|
||||||
<p class="fees-source-stats text-muted">
|
|
||||||
已缓存:
|
|
||||||
{% if fee_counts.get('ctp') %}<span class="badge profit">CTP {{ fee_counts.ctp }}</span>{% endif %}
|
|
||||||
{% if fee_counts.get('local') %}<span class="badge planned">local {{ fee_counts.local }}</span>{% endif %}
|
|
||||||
{% if fee_counts.get('json') %}<span class="badge planned">json {{ fee_counts.json }}</span>{% endif %}
|
|
||||||
{% if fee_counts.get('manual') %}<span class="badge planned">manual {{ fee_counts.manual }}</span>{% endif %}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h2>本地参考倍率</h2>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="hint" style="margin-bottom:.65rem;font-size:.78rem">仅「本地数据源」时使用</p>
|
|
||||||
<form action="{{ url_for('fees') }}" method="post" class="form-row" style="flex-wrap:wrap;gap:.5rem;align-items:center;margin-bottom:.75rem">
|
|
||||||
<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:88px">
|
|
||||||
<button type="submit" class="btn-primary">保存倍率</button>
|
|
||||||
</form>
|
|
||||||
<div class="form-row" style="flex-wrap:wrap;gap:.5rem">
|
|
||||||
<form action="{{ url_for('fees') }}" method="post" style="display:inline">
|
|
||||||
<input type="hidden" name="action" value="sync">
|
|
||||||
<button type="submit" class="btn-secondary">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:.45rem .85rem;border:1px solid var(--card-border);border-radius:8px">重载 JSON</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -88,47 +52,38 @@
|
|||||||
<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>
|
||||||
<th>更新</th><th>操作</th>
|
<th>更新</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for r in rates %}
|
{% for r in rates %}
|
||||||
{% set fid = 'fee-row-' ~ r.product %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<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 class="cell-readonly">{{ r.exchange or '—' }}</td>
|
||||||
<td><input name="exchange" form="{{ fid }}" value="{{ r.exchange or '' }}" style="width:72px;padding:.3rem"></td>
|
<td class="cell-readonly">{{ r.mult }}</td>
|
||||||
<td><input name="mult" form="{{ fid }}" type="number" value="{{ r.mult }}" style="width:56px;padding:.3rem"></td>
|
<td class="cell-readonly">{{ r.open_fixed }}</td>
|
||||||
<td><input name="open_fixed" form="{{ fid }}" type="number" step="0.0001" value="{{ r.open_fixed }}" style="width:72px;padding:.3rem"></td>
|
<td class="cell-readonly">{{ r.open_ratio }}</td>
|
||||||
<td><input name="open_ratio" form="{{ fid }}" type="number" step="0.0000001" value="{{ r.open_ratio }}" style="width:88px;padding:.3rem"></td>
|
<td class="cell-readonly">{{ r.close_yesterday_fixed }}</td>
|
||||||
<td><input name="close_yesterday_fixed" form="{{ fid }}" type="number" step="0.0001" value="{{ r.close_yesterday_fixed }}" style="width:72px;padding:.3rem"></td>
|
<td class="cell-readonly">{{ r.close_yesterday_ratio }}</td>
|
||||||
<td><input name="close_yesterday_ratio" form="{{ fid }}" type="number" step="0.0000001" value="{{ r.close_yesterday_ratio }}" style="width:88px;padding:.3rem"></td>
|
<td class="cell-readonly">{{ r.close_today_fixed }}</td>
|
||||||
<td><input name="close_today_fixed" form="{{ fid }}" type="number" step="0.0001" value="{{ r.close_today_fixed }}" style="width:72px;padding:.3rem"></td>
|
<td class="cell-readonly">{{ r.close_today_ratio }}</td>
|
||||||
<td><input name="close_today_ratio" form="{{ fid }}" 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 class="text-muted" style="font-size:.72rem">{{ (r.updated_at or '')[:16] }}</td>
|
||||||
<td><button type="submit" form="{{ fid }}" class="btn-link">保存</button></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="12" class="text-muted">暂无费率,请连接 CTP 后同步</td></tr>
|
<tr><td colspan="10" class="text-muted">暂无 CTP 费率,请连接 CTP 后等待自动同步或点击「立即同步」</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% for r in rates %}
|
|
||||||
<form id="fee-row-{{ r.product }}" action="{{ url_for('fees') }}" method="post" hidden>
|
|
||||||
<input type="hidden" name="action" value="save_row">
|
|
||||||
<input type="hidden" name="product" value="{{ r.product }}">
|
|
||||||
</form>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<p class="hint" style="margin-top:.75rem;padding:0 1rem 1rem">
|
<p class="hint" style="margin-top:.75rem;padding:0 1rem 1rem">
|
||||||
公式:单边 = 固定(元/手)×手数 + 比例×价格×乘数×手数;往返 = 开仓 + 平仓(平今/平昨自动判断)。
|
公式:单边 = 固定(元/手)×手数 + 比例×价格×乘数×手数;往返 = 开仓 + 平仓(平今/平昨自动判断)。
|
||||||
{% if fee_source_mode == 'ctp' and ctp_connected and not fee_counts.get('ctp') %}
|
{% if ctp_connected and not fee_counts.get('ctp') %}
|
||||||
<br><strong class="text-loss">当前无 CTP 费率缓存,请点击「从 CTP 同步费率」。</strong>
|
<br><strong class="text-loss">数据库尚无 CTP 费率,请点击「立即同步」或等待后台每日任务。</strong>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+21
-6
@@ -223,15 +223,30 @@ class CtpBridge:
|
|||||||
self._connected_mode = None
|
self._connected_mode = None
|
||||||
|
|
||||||
def _schedule_fee_sync(self, mode: str) -> None:
|
def _schedule_fee_sync(self, mode: str) -> None:
|
||||||
|
"""连接成功后触发每日同步检查(非每次全量)。"""
|
||||||
|
|
||||||
def _run() -> None:
|
def _run() -> None:
|
||||||
try:
|
try:
|
||||||
from ctp_fee_sync import sync_fees_from_ctp
|
from ctp_fee_worker import try_daily_ctp_fee_sync
|
||||||
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 _gs(key: str, default: str = "") -> str:
|
||||||
|
from fee_specs import get_setting
|
||||||
|
return get_setting(key, default)
|
||||||
|
|
||||||
|
def _ss(key: str, val: str) -> None:
|
||||||
|
from fee_specs import set_setting
|
||||||
|
set_setting(key, val)
|
||||||
|
|
||||||
|
try_daily_ctp_fee_sync(
|
||||||
|
mode,
|
||||||
|
get_setting=_gs,
|
||||||
|
set_setting=_ss,
|
||||||
|
force=False,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("CTP 手续费连接后检查: %s", exc)
|
||||||
|
|
||||||
|
threading.Thread(target=_run, daemon=True, name="ctp-fee-sync-check").start()
|
||||||
|
|
||||||
def _ensure_commission_callback(self) -> None:
|
def _ensure_commission_callback(self) -> None:
|
||||||
if self._commission_hooked or not self._engine:
|
if self._commission_hooked or not self._engine:
|
||||||
|
|||||||
Reference in New Issue
Block a user