diff --git a/app.py b/app.py index 8306a58..4f5b62c 100644 --- a/app.py +++ b/app.py @@ -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"), diff --git a/market.py b/market.py index 524112e..798b991 100644 --- a/market.py +++ b/market.py @@ -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: diff --git a/templates/fees.html b/templates/fees.html index 81008af..29d0b08 100644 --- a/templates/fees.html +++ b/templates/fees.html @@ -1,54 +1,64 @@ {% extends "base.html" %} {% block title %}手续费配置 - 国内期货监控系统{% endblock %} +{% block extra_css %} + +{% endblock %} {% block content %} -
-

手续费数据源

-
-
- - - - -
-

- 默认使用 CTP 柜台 查询到的开仓/平仓费率(连接 CTP 后自动同步,与 SimNow/期货公司一致)。 - 离线或未连接时可改用本地表估算。 -

-
-
- - +
+
+

手续费数据源

+
+ + +
+ + +
+ - {% if ctp_connected %} - CTP 已连接 - {% else %} - CTP 未连接 — 请先连接后再同步 - {% endif %} +

+ 默认使用 CTP 柜台 费率(连接后自动同步,与 SimNow/期货公司一致)。 +

+
+
+ + +
+ {% if ctp_connected %} + CTP 已连接 + {% else %} + CTP 未连接 + {% endif %} +
-
-
-

本地参考倍率(仅「本地数据源」时使用)

-
-
- - - - -
-
-
- - -
-
- - +
+

本地参考倍率

+
+

仅「本地数据源」时使用

+ + + + + +
+
+ + +
+
+ + +
+
@@ -56,6 +66,7 @@

品种费率表

+
@@ -91,9 +102,10 @@ {% endfor %}
+

- 公式:单边手续费 = 固定(元/手)×手数 + 比例×价格×乘数×手数。往返 = 开仓 + 平仓(平今/平昨自动判断)。 + 公式:单边 = 固定(元/手)×手数 + 比例×价格×乘数×手数;往返 = 开仓 + 平仓(平今/平昨自动判断)。

{% endblock %} diff --git a/templates/settings.html b/templates/settings.html index 26ba153..9a793ce 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -2,15 +2,19 @@ {% block title %}系统设置 - 国内期货监控系统{% endblock %} {% block extra_css %} {% endblock %} {% block content %} -
@@ -57,31 +61,17 @@

- 模拟盘连接上期 SimNow 仿真柜台(非本地假资金)。在 .env 配置 - SIMNOW_USERSIMNOW_PASSWORD 等,在「持仓监控」页点击连接 CTP。
- 实盘后期配置 CTP_LIVE_* 对接你的期货公司。 -

-
- -
-

参考资金

-
- - - -
-

- CTP 未连接时用于品种推荐与以损定仓估算;SimNow/实盘登录成功后自动改用柜台权益。 + 模拟盘.env 配置 SIMNOW_USER 等,于「持仓监控」连接 CTP。 + 权益与行情优先来自 CTP 柜台

行情说明

-

+

当前行情源:{{ quote_label }}
- 合约代码按同花顺格式显示(如 ag2608、IF2606),便于与看盘软件对照; - 实际价格通过行情接口获取,普通用户无需申请 token。
- 同花顺 iFinD 接口面向机构用户,个人期货通用户一般无法获取 refresh_token,故系统默认不使用。 + CTP 已连接时使用柜台行情(与下单、持仓一致);未连接时回退新浪免费接口。
+ 合约代码按同花顺格式显示(如 ag2608、IF2606)。

@@ -89,36 +79,37 @@

企业微信推送

- +

在企业微信群中添加机器人后,将 Webhook 地址粘贴到上方保存即可。

-
+

修改密码

-
+ -
- - +
+ +
-
- - +
+ +
-
- - +
+ +
-
- - +
+ + +
+
+
-
-
{% endblock %} diff --git a/vnpy_bridge.py b/vnpy_bridge.py index 9994bb4..2c7afcd 100644 --- a/vnpy_bridge.py +++ b/vnpy_bridge.py @@ -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)