ui: 手续费/设置布局优化,行情优先 CTP
手续费数据源与本地倍率并列双列;设置页去掉参考资金、缩小改密表单;CTP 连接时订阅柜台 tick 作为行情源。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -428,7 +428,7 @@ def build_market_quote_payload(
|
||||
if codes:
|
||||
market_code = codes.get("market_code", "") or market_code
|
||||
sina_code = codes.get("sina_code", "") or sina_code
|
||||
price = market_get_price(market_code, sina_code)
|
||||
price = fetch_price(symbol, market_code, sina_code)
|
||||
name = symbol
|
||||
codes = ths_to_codes(symbol)
|
||||
if codes:
|
||||
@@ -477,7 +477,20 @@ def resolve_market_codes(ths_code: str, market_code: str = "", sina_code: str =
|
||||
|
||||
|
||||
def fetch_price(ths_code: str, market_code: str = "", sina_code: str = "") -> Optional[float]:
|
||||
mc, sc = resolve_market_codes(ths_code, market_code, sina_code)
|
||||
sym = (ths_code or "").strip()
|
||||
if sym:
|
||||
try:
|
||||
from vnpy_bridge import ctp_status, ctp_get_tick_price
|
||||
from trading_context import get_trading_mode
|
||||
|
||||
mode = get_trading_mode(get_setting)
|
||||
if ctp_status(mode).get("connected"):
|
||||
p = ctp_get_tick_price(mode, sym)
|
||||
if p and p > 0:
|
||||
return p
|
||||
except Exception:
|
||||
pass
|
||||
mc, sc = resolve_market_codes(sym, market_code, sina_code)
|
||||
if not mc and not sc:
|
||||
return None
|
||||
return market_get_price(mc, sc)
|
||||
@@ -1578,17 +1591,6 @@ def settings():
|
||||
webhook = request.form.get("wechat_webhook", "").strip()
|
||||
set_setting("wechat_webhook", webhook)
|
||||
flash("企业微信配置已保存")
|
||||
elif action == "capital":
|
||||
raw = request.form.get("live_capital", "").strip()
|
||||
try:
|
||||
val = float(raw)
|
||||
if val < 0:
|
||||
flash("实盘资金不能为负数")
|
||||
else:
|
||||
set_setting("live_capital", str(val))
|
||||
flash("参考资金已保存(CTP 已连接时以 SimNow/柜台权益为准)")
|
||||
except ValueError:
|
||||
flash("请输入有效的实盘资金金额")
|
||||
elif action == "trading":
|
||||
mode = request.form.get("trading_mode", "simulation").strip()
|
||||
if mode not in ("simulation", "live"):
|
||||
@@ -1627,13 +1629,19 @@ def settings():
|
||||
|
||||
webhook = get_setting("wechat_webhook")
|
||||
username = get_setting("admin_username")
|
||||
live_capital = get_setting("live_capital", "0")
|
||||
ctp_st = {}
|
||||
try:
|
||||
from vnpy_bridge import ctp_status
|
||||
from trading_context import get_trading_mode
|
||||
|
||||
ctp_st = ctp_status(get_trading_mode(get_setting))
|
||||
except Exception:
|
||||
pass
|
||||
return render_template(
|
||||
"settings.html",
|
||||
webhook=webhook,
|
||||
username=username,
|
||||
live_capital=live_capital,
|
||||
quote_label=get_quote_source_label(),
|
||||
quote_label=get_quote_source_label(ctp_connected=bool(ctp_st.get("connected"))),
|
||||
trading_mode=get_setting("trading_mode", "simulation"),
|
||||
position_sizing_mode=get_setting("position_sizing_mode", "risk"),
|
||||
risk_percent=get_setting("risk_percent", "1"),
|
||||
|
||||
@@ -35,16 +35,18 @@ def _has_ths_token() -> bool:
|
||||
return bool(_get_refresh_token())
|
||||
|
||||
|
||||
def get_quote_source_label() -> str:
|
||||
def get_quote_source_label(*, ctp_connected: bool = False) -> str:
|
||||
"""界面展示用行情源说明。"""
|
||||
if ctp_connected:
|
||||
return "CTP 柜台(已连接)"
|
||||
source = _quote_source()
|
||||
if source == "sina":
|
||||
return "新浪(免费)"
|
||||
return "新浪(CTP 未连接时备用)"
|
||||
if source == "ths":
|
||||
return "同花顺 iFinD" if _has_ths_token() else "同花顺(未配置 token,无法使用)"
|
||||
return "同花顺 iFinD" if _has_ths_token() else "同花顺(未配置 token)"
|
||||
if _has_ths_token():
|
||||
return "同花顺优先,失败回退新浪"
|
||||
return "新浪(免费)"
|
||||
return "新浪(CTP 未连接时备用)"
|
||||
|
||||
|
||||
def _sina_headers() -> dict:
|
||||
|
||||
+56
-44
@@ -1,54 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}手续费配置 - 国内期货监控系统{% endblock %}
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.fees-split{margin-bottom:1.25rem}
|
||||
.fees-split .card{margin-bottom:0;min-height:auto}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<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>
|
||||
<div class="split-grid fees-split">
|
||||
<div class="card">
|
||||
<h2>手续费数据源</h2>
|
||||
<div class="card-body">
|
||||
<form action="{{ url_for('fees') }}" method="post">
|
||||
<input type="hidden" name="action" value="fee_source">
|
||||
<div class="field" style="margin-bottom:.75rem">
|
||||
<label>计费依据</label>
|
||||
<select name="fee_source_mode">
|
||||
<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>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">保存</button>
|
||||
</form>
|
||||
{% if ctp_connected %}
|
||||
<span class="badge profit">CTP 已连接</span>
|
||||
{% else %}
|
||||
<span class="badge planned">CTP 未连接 — 请先连接后再同步</span>
|
||||
{% endif %}
|
||||
<p class="hint" style="margin-top:.75rem">
|
||||
默认使用 <strong>CTP 柜台</strong> 费率(连接后自动同步,与 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>
|
||||
|
||||
<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>
|
||||
<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-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:.5rem 1rem;border:1px solid var(--card-border);border-radius:8px">重载 JSON 默认表</button>
|
||||
<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>
|
||||
@@ -56,6 +66,7 @@
|
||||
<div class="card">
|
||||
<h2>品种费率表</h2>
|
||||
<div class="card-body card-scroll">
|
||||
<div class="table-responsive">
|
||||
<table class="trade-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -91,9 +102,10 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint" style="margin-top:.75rem">
|
||||
公式:单边手续费 = 固定(元/手)×手数 + 比例×价格×乘数×手数。往返 = 开仓 + 平仓(平今/平昨自动判断)。
|
||||
公式:单边 = 固定(元/手)×手数 + 比例×价格×乘数×手数;往返 = 开仓 + 平仓(平今/平昨自动判断)。
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
+32
-41
@@ -2,15 +2,19 @@
|
||||
{% block title %}系统设置 - 国内期货监控系统{% endblock %}
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.settings-page{max-width:1100px;margin:0 auto}
|
||||
.settings-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.25rem;align-items:start}
|
||||
.settings-grid .card{margin-bottom:0;height:100%}
|
||||
.settings-grid .settings-span-2{grid-column:1/-1}
|
||||
@media(max-width:900px){.settings-grid{grid-template-columns:1fr}}
|
||||
.settings-grid .card{margin-bottom:0}
|
||||
.settings-password-form{display:grid;grid-template-columns:1fr 1fr;gap:.65rem .75rem;max-width:520px}
|
||||
.settings-password-form .field-full{grid-column:1/-1}
|
||||
.settings-password-form .field label{font-size:.78rem}
|
||||
.settings-password-form input{padding:.55rem .7rem;font-size:.85rem}
|
||||
@media(max-width:900px){
|
||||
.settings-grid{grid-template-columns:1fr}
|
||||
.settings-password-form{grid-template-columns:1fr;max-width:none}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="settings-page">
|
||||
<div class="settings-grid">
|
||||
|
||||
<div class="card">
|
||||
@@ -57,31 +61,17 @@
|
||||
<button type="submit" class="btn-primary" style="margin-top:.75rem">保存交易设置</button>
|
||||
</form>
|
||||
<p class="hint" style="margin-top:.75rem">
|
||||
<strong>模拟盘</strong>连接上期 SimNow 仿真柜台(非本地假资金)。在 <code>.env</code> 配置
|
||||
<code>SIMNOW_USER</code>、<code>SIMNOW_PASSWORD</code> 等,在「持仓监控」页点击连接 CTP。<br>
|
||||
<strong>实盘</strong>后期配置 <code>CTP_LIVE_*</code> 对接你的期货公司。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>参考资金</h2>
|
||||
<form action="{{ url_for('settings') }}" method="post" class="form-row">
|
||||
<input type="hidden" name="action" value="capital">
|
||||
<input name="live_capital" type="number" step="0.01" min="0" placeholder="参考权益(元)" value="{{ live_capital }}" style="flex:1;min-width:200px;max-width:320px">
|
||||
<button type="submit" class="btn-primary">保存</button>
|
||||
</form>
|
||||
<p class="hint" style="margin-top:.75rem">
|
||||
CTP 未连接时用于<strong>品种推荐</strong>与以损定仓估算;SimNow/实盘登录成功后自动改用<strong>柜台权益</strong>。
|
||||
<strong>模拟盘</strong>在 <code>.env</code> 配置 <code>SIMNOW_USER</code> 等,于「持仓监控」连接 CTP。
|
||||
权益与行情优先来自<strong> CTP 柜台</strong>。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>行情说明</h2>
|
||||
<p class="hint" style="font-size:.9rem;line-height:1.6">
|
||||
<p class="hint" style="font-size:.88rem;line-height:1.6;margin:0">
|
||||
当前行情源:<strong class="text-accent">{{ quote_label }}</strong><br>
|
||||
合约代码按<strong>同花顺格式</strong>显示(如 ag2608、IF2606),便于与看盘软件对照;
|
||||
实际价格通过行情接口获取,普通用户无需申请 token。<br>
|
||||
<span class="text-muted" style="font-size:.85rem">同花顺 iFinD 接口面向机构用户,个人期货通用户一般无法获取 refresh_token,故系统默认不使用。</span>
|
||||
CTP 已连接时使用<strong>柜台行情</strong>(与下单、持仓一致);未连接时回退新浪免费接口。<br>
|
||||
合约代码按<strong>同花顺格式</strong>显示(如 ag2608、IF2606)。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -89,36 +79,37 @@
|
||||
<h2>企业微信推送</h2>
|
||||
<form action="{{ url_for('settings') }}" method="post" class="form-row">
|
||||
<input type="hidden" name="action" value="wechat">
|
||||
<input name="wechat_webhook" type="url" placeholder="企业微信 Webhook 地址" value="{{ webhook }}" style="flex:1;min-width:300px">
|
||||
<input name="wechat_webhook" type="url" placeholder="企业微信 Webhook 地址" value="{{ webhook }}" style="flex:1;min-width:0">
|
||||
<button type="submit" class="btn-primary">保存</button>
|
||||
</form>
|
||||
<p class="hint" style="margin-top:.75rem">在企业微信群中添加机器人后,将 Webhook 地址粘贴到上方保存即可。</p>
|
||||
</div>
|
||||
|
||||
<div class="card settings-span-2">
|
||||
<div class="card">
|
||||
<h2>修改密码</h2>
|
||||
<form action="{{ url_for('settings') }}" method="post" style="max-width:480px">
|
||||
<form action="{{ url_for('settings') }}" method="post" class="settings-password-form">
|
||||
<input type="hidden" name="action" value="password">
|
||||
<div style="margin-bottom:.75rem">
|
||||
<label class="text-label" style="font-size:.85rem;display:block;margin-bottom:.35rem">当前账号</label>
|
||||
<input type="text" value="{{ username }}" disabled style="width:100%">
|
||||
<div class="field field-full">
|
||||
<label>当前账号</label>
|
||||
<input type="text" value="{{ username }}" disabled>
|
||||
</div>
|
||||
<div style="margin-bottom:.75rem">
|
||||
<label class="text-label" style="font-size:.85rem;display:block;margin-bottom:.35rem">原密码</label>
|
||||
<input name="old_password" type="password" required style="width:100%">
|
||||
<div class="field">
|
||||
<label>原密码</label>
|
||||
<input name="old_password" type="password" required>
|
||||
</div>
|
||||
<div style="margin-bottom:.75rem">
|
||||
<label class="text-label" style="font-size:.85rem;display:block;margin-bottom:.35rem">新密码(至少 6 位)</label>
|
||||
<input name="new_password" type="password" required minlength="6" style="width:100%">
|
||||
<div class="field">
|
||||
<label>新密码(至少 6 位)</label>
|
||||
<input name="new_password" type="password" required minlength="6">
|
||||
</div>
|
||||
<div style="margin-bottom:.75rem">
|
||||
<label class="text-label" style="font-size:.85rem;display:block;margin-bottom:.35rem">确认新密码</label>
|
||||
<input name="new_password2" type="password" required minlength="6" style="width:100%">
|
||||
<div class="field field-full">
|
||||
<label>确认新密码</label>
|
||||
<input name="new_password2" type="password" required minlength="6">
|
||||
</div>
|
||||
<div class="field-full">
|
||||
<button type="submit" class="btn-primary">修改密码</button>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">修改密码</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -89,6 +89,8 @@ class CtpBridge:
|
||||
self._commission_waiters: dict[int, threading.Event] = {}
|
||||
self._commission_results: dict[int, dict] = {}
|
||||
self._commission_hooked = False
|
||||
self._subscribed: set[str] = set()
|
||||
self._tick_hooked = False
|
||||
self._init_engine()
|
||||
|
||||
def _init_engine(self) -> None:
|
||||
@@ -282,6 +284,78 @@ class CtpBridge:
|
||||
self._commission_waiters.pop(reqid, None)
|
||||
return self._commission_results.pop(reqid, {})
|
||||
|
||||
def _tick_key(self, symbol: str, ex_name: str) -> str:
|
||||
return f"{symbol.lower()}:{ex_name.upper()}"
|
||||
|
||||
def _price_from_tick(self, tick: Any) -> Optional[float]:
|
||||
for attr in ("last_price", "bid_price_1", "ask_price_1", "pre_close"):
|
||||
try:
|
||||
v = float(getattr(tick, attr, 0) or 0)
|
||||
except (TypeError, ValueError):
|
||||
v = 0.0
|
||||
if v > 0:
|
||||
return v
|
||||
return None
|
||||
|
||||
def _lookup_tick(self, symbol: str, ex_name: str) -> Optional[float]:
|
||||
if not self._engine:
|
||||
return None
|
||||
sym_l = symbol.lower()
|
||||
ex_u = ex_name.upper()
|
||||
try:
|
||||
for tick in self._engine.get_all_ticks():
|
||||
ts = (getattr(tick, "symbol", "") or "").lower()
|
||||
te = getattr(tick, "exchange", None)
|
||||
te_s = str(te.value if hasattr(te, "value") else te or "").upper()
|
||||
if ts == sym_l and te_s == ex_u:
|
||||
p = self._price_from_tick(tick)
|
||||
if p:
|
||||
return p
|
||||
except Exception as exc:
|
||||
logger.debug("lookup tick: %s", exc)
|
||||
return None
|
||||
|
||||
def _ensure_tick_handler(self) -> None:
|
||||
if self._tick_hooked or not self._ee:
|
||||
return
|
||||
self._tick_hooked = True
|
||||
|
||||
def subscribe_symbol(self, ths_code: str) -> None:
|
||||
if not self._engine or not self._connected_mode:
|
||||
return
|
||||
try:
|
||||
from vnpy.trader.object import SubscribeRequest
|
||||
|
||||
sym, ex_name = ths_to_vnpy_symbol(ths_code)
|
||||
key = self._tick_key(sym, ex_name)
|
||||
if key in self._subscribed:
|
||||
return
|
||||
exchange = to_vnpy_exchange(ex_name)
|
||||
self._ensure_tick_handler()
|
||||
req = SubscribeRequest(symbol=sym, exchange=exchange)
|
||||
self._engine.subscribe(req, GATEWAY_NAME)
|
||||
self._subscribed.add(key)
|
||||
except Exception as exc:
|
||||
logger.debug("CTP subscribe %s: %s", ths_code, exc)
|
||||
|
||||
def get_tick_price(self, ths_code: str, *, mode: str) -> Optional[float]:
|
||||
if self._connected_mode != mode or not self._engine:
|
||||
return None
|
||||
try:
|
||||
sym, ex_name = ths_to_vnpy_symbol(ths_code)
|
||||
except Exception:
|
||||
return None
|
||||
price = self._lookup_tick(sym, ex_name)
|
||||
if price:
|
||||
return price
|
||||
self.subscribe_symbol(ths_code)
|
||||
for _ in range(8):
|
||||
time.sleep(0.2)
|
||||
price = self._lookup_tick(sym, ex_name)
|
||||
if price:
|
||||
return price
|
||||
return None
|
||||
|
||||
def get_account(self) -> dict[str, Any]:
|
||||
if not self._engine:
|
||||
return {}
|
||||
@@ -478,6 +552,18 @@ def ctp_list_active_orders(mode: str) -> list[dict[str, Any]]:
|
||||
return b.list_active_orders()
|
||||
|
||||
|
||||
def ctp_get_tick_price(mode: str, ths_code: str) -> Optional[float]:
|
||||
"""CTP 柜台最新价(需已连接并订阅)。"""
|
||||
b = get_bridge()
|
||||
if b.connected_mode != mode:
|
||||
return None
|
||||
try:
|
||||
return b.get_tick_price(ths_code, mode=mode)
|
||||
except Exception as exc:
|
||||
logger.debug("ctp_get_tick_price: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def get_ctp_balance(mode: str) -> Optional[float]:
|
||||
try:
|
||||
acc = ctp_get_account(mode)
|
||||
|
||||
Reference in New Issue
Block a user