feat: CTP/SimNow 配置迁入系统设置,登录失败即时报错
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@ RISK_PERCENT=1
|
||||
# CTP 断线后后台自动重连(true/false)
|
||||
CTP_AUTO_RECONNECT=true
|
||||
|
||||
# —— SimNow 模拟盘(注册见 docs/SIMNOW.md)——
|
||||
# —— SimNow 模拟盘(也可在「系统设置 → CTP 连接」配置,优先于本文件)——
|
||||
SIMNOW_USER=
|
||||
SIMNOW_PASSWORD=
|
||||
SIMNOW_BROKER_ID=9999
|
||||
|
||||
@@ -366,6 +366,9 @@ def init_db():
|
||||
if not get_setting("ths_refresh_token") and os.getenv("THS_REFRESH_TOKEN"):
|
||||
set_setting("ths_refresh_token", os.getenv("THS_REFRESH_TOKEN"))
|
||||
|
||||
from ctp_settings import seed_ctp_settings_from_env
|
||||
seed_ctp_settings_from_env(set_setting)
|
||||
|
||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||
expire_old_plans()
|
||||
|
||||
@@ -1706,6 +1709,17 @@ def settings():
|
||||
flash("移动保本缓冲无效")
|
||||
return redirect(url_for("settings"))
|
||||
flash("交易模式已保存")
|
||||
elif action == "ctp":
|
||||
from ctp_settings import save_ctp_settings_from_form
|
||||
|
||||
save_ctp_settings_from_form(request.form, set_setting)
|
||||
try:
|
||||
from vnpy_bridge import get_bridge
|
||||
|
||||
get_bridge().mark_disconnected()
|
||||
except Exception:
|
||||
pass
|
||||
flash("CTP 配置已保存,请在持仓监控页重连 CTP")
|
||||
elif action == "nav":
|
||||
items = {k: request.form.get(f"nav_{k}") == "on" for k in NAV_TOGGLES}
|
||||
save_nav_items(set_setting, items)
|
||||
@@ -1736,11 +1750,15 @@ def settings():
|
||||
ctp_st = ctp_status(get_trading_mode(get_setting))
|
||||
except Exception:
|
||||
pass
|
||||
from ctp_settings import get_ctp_settings_for_ui
|
||||
|
||||
return render_template(
|
||||
"settings.html",
|
||||
webhook=webhook,
|
||||
username=username,
|
||||
quote_label=get_quote_source_label(ctp_connected=bool(ctp_st.get("connected"))),
|
||||
ctp_status=ctp_st,
|
||||
ctp_cfg=get_ctp_settings_for_ui(),
|
||||
trading_mode=get_setting("trading_mode", "simulation"),
|
||||
position_sizing_mode=get_setting("position_sizing_mode", "fixed"),
|
||||
fixed_lots=get_setting("fixed_lots", "1"),
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
"""CTP / SimNow 配置:系统设置优先,.env 作兜底。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Callable
|
||||
|
||||
# (db_key, env_key, vnpy字段名, 默认值)
|
||||
SIMNOW_FIELDS: tuple[tuple[str, str, str, str], ...] = (
|
||||
("simnow_user", "SIMNOW_USER", "用户名", ""),
|
||||
("simnow_password", "SIMNOW_PASSWORD", "密码", ""),
|
||||
("simnow_broker_id", "SIMNOW_BROKER_ID", "经纪商代码", "9999"),
|
||||
("simnow_td_address", "SIMNOW_TD_ADDRESS", "交易服务器", "tcp://180.168.146.187:10201"),
|
||||
("simnow_md_address", "SIMNOW_MD_ADDRESS", "行情服务器", "tcp://180.168.146.187:10211"),
|
||||
("simnow_app_id", "SIMNOW_APP_ID", "产品名称", "simnow_client_test"),
|
||||
("simnow_auth_code", "SIMNOW_AUTH_CODE", "授权编码", "0000000000000000"),
|
||||
("simnow_env", "SIMNOW_ENV", "柜台环境", "实盘"),
|
||||
)
|
||||
|
||||
LIVE_FIELDS: tuple[tuple[str, str, str, str], ...] = (
|
||||
("ctp_live_user", "CTP_LIVE_USER", "用户名", ""),
|
||||
("ctp_live_password", "CTP_LIVE_PASSWORD", "密码", ""),
|
||||
("ctp_live_broker_id", "CTP_LIVE_BROKER_ID", "经纪商代码", ""),
|
||||
("ctp_live_td_address", "CTP_LIVE_TD_ADDRESS", "交易服务器", ""),
|
||||
("ctp_live_md_address", "CTP_LIVE_MD_ADDRESS", "行情服务器", ""),
|
||||
("ctp_live_app_id", "CTP_LIVE_APP_ID", "产品名称", ""),
|
||||
("ctp_live_auth_code", "CTP_LIVE_AUTH_CODE", "授权编码", ""),
|
||||
("ctp_live_env", "CTP_LIVE_ENV", "柜台环境", "实盘"),
|
||||
)
|
||||
|
||||
PASSWORD_DB_KEYS = frozenset({"simnow_password", "ctp_live_password"})
|
||||
|
||||
|
||||
def _get_db_setting(key: str, default: str = "") -> str:
|
||||
from fee_specs import get_setting
|
||||
|
||||
return (get_setting(key, default) or default).strip()
|
||||
|
||||
|
||||
def resolve_ctp_value(db_key: str, env_key: str, default: str = "") -> str:
|
||||
v = _get_db_setting(db_key, "")
|
||||
if v:
|
||||
return v
|
||||
return (os.getenv(env_key) or default).strip()
|
||||
|
||||
|
||||
def _build_setting_dict(fields: tuple[tuple[str, str, str, str], ...]) -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
for db_key, env_key, vnpy_key, default in fields:
|
||||
out[vnpy_key] = resolve_ctp_value(db_key, env_key, default)
|
||||
return out
|
||||
|
||||
|
||||
def simnow_setting_dict() -> dict[str, str]:
|
||||
return _build_setting_dict(SIMNOW_FIELDS)
|
||||
|
||||
|
||||
def live_setting_dict() -> dict[str, str]:
|
||||
return _build_setting_dict(LIVE_FIELDS)
|
||||
|
||||
|
||||
def seed_ctp_settings_from_env(set_setting: Callable[[str, str], None]) -> None:
|
||||
"""首次启动:将 .env 中已有 CTP 配置写入 settings 表。"""
|
||||
for db_key, env_key, _, _ in (*SIMNOW_FIELDS, *LIVE_FIELDS):
|
||||
if _get_db_setting(db_key, ""):
|
||||
continue
|
||||
env_val = (os.getenv(env_key) or "").strip()
|
||||
if env_val:
|
||||
set_setting(db_key, env_val)
|
||||
|
||||
|
||||
def get_ctp_settings_for_ui() -> dict[str, Any]:
|
||||
ui: dict[str, Any] = {}
|
||||
for db_key, env_key, _, default in SIMNOW_FIELDS:
|
||||
ui[db_key] = resolve_ctp_value(db_key, env_key, default)
|
||||
if db_key in PASSWORD_DB_KEYS:
|
||||
ui[f"{db_key}_set"] = bool(ui[db_key])
|
||||
ui[db_key] = ""
|
||||
for db_key, env_key, _, default in LIVE_FIELDS:
|
||||
ui[db_key] = resolve_ctp_value(db_key, env_key, default)
|
||||
if db_key in PASSWORD_DB_KEYS:
|
||||
ui[f"{db_key}_set"] = bool(ui[db_key])
|
||||
ui[db_key] = ""
|
||||
return ui
|
||||
|
||||
|
||||
def save_ctp_settings_from_form(
|
||||
form: Any,
|
||||
set_setting: Callable[[str, str], None],
|
||||
) -> None:
|
||||
"""保存 CTP 配置;密码留空表示不修改。"""
|
||||
for db_key, _, _, default in SIMNOW_FIELDS:
|
||||
if db_key in PASSWORD_DB_KEYS:
|
||||
val = (form.get(db_key) or "").strip()
|
||||
if val:
|
||||
set_setting(db_key, val)
|
||||
continue
|
||||
val = (form.get(db_key) or "").strip()
|
||||
set_setting(db_key, val or default)
|
||||
|
||||
for db_key, _, _, default in LIVE_FIELDS:
|
||||
if db_key in PASSWORD_DB_KEYS:
|
||||
val = (form.get(db_key) or "").strip()
|
||||
if val:
|
||||
set_setting(db_key, val)
|
||||
continue
|
||||
val = (form.get(db_key) or "").strip()
|
||||
if default or val:
|
||||
set_setting(db_key, val or default)
|
||||
+1
-1
@@ -133,7 +133,7 @@ SimNow 提供多种仿真环境,**IP 与端口会随官网公告调整**,部
|
||||
tcp://IP:端口
|
||||
```
|
||||
|
||||
修改 `.env` 后需重启应用:
|
||||
修改 `.env` 后需重启应用;也可在 **系统设置 → CTP 连接** 中维护(优先于 `.env`)。
|
||||
|
||||
```bash
|
||||
pm2 restart qihuo
|
||||
|
||||
+11
-8
@@ -33,18 +33,21 @@ def _probe(host_port: str) -> str:
|
||||
|
||||
|
||||
def main() -> int:
|
||||
user = os.getenv("SIMNOW_USER", "")
|
||||
td = os.getenv("SIMNOW_TD_ADDRESS", "tcp://180.168.146.187:10201")
|
||||
md = os.getenv("SIMNOW_MD_ADDRESS", "tcp://180.168.146.187:10211")
|
||||
env = os.getenv("SIMNOW_ENV", "实盘")
|
||||
from ctp_settings import resolve_ctp_value
|
||||
|
||||
print("=== SimNow 配置 ===")
|
||||
user = resolve_ctp_value("simnow_user", "SIMNOW_USER")
|
||||
pwd = resolve_ctp_value("simnow_password", "SIMNOW_PASSWORD")
|
||||
td = resolve_ctp_value("simnow_td_address", "SIMNOW_TD_ADDRESS", "tcp://180.168.146.187:10201")
|
||||
md = resolve_ctp_value("simnow_md_address", "SIMNOW_MD_ADDRESS", "tcp://180.168.146.187:10211")
|
||||
env = resolve_ctp_value("simnow_env", "SIMNOW_ENV", "实盘")
|
||||
|
||||
print("=== SimNow 配置(系统设置优先)===")
|
||||
print(f"locale = {ensure_process_locale()}")
|
||||
missing = missing_ctp_locales()
|
||||
if missing:
|
||||
print(f"警告: 缺少 CTP 所需 locale: {', '.join(missing)}")
|
||||
print(f"SIMNOW_USER = {user or '(未设置)'}")
|
||||
print(f"SIMNOW_PASSWORD = {'*' * 8 if os.getenv('SIMNOW_PASSWORD') else '(未设置)'}")
|
||||
print(f"SIMNOW_PASSWORD = {'*' * 8 if pwd else '(未设置)'}")
|
||||
print(f"SIMNOW_TD = {td}")
|
||||
print(f"SIMNOW_MD = {md}")
|
||||
print(f"SIMNOW_ENV = {env}")
|
||||
@@ -54,8 +57,8 @@ def main() -> int:
|
||||
print(f"MD {md} -> {_probe(md)}")
|
||||
print()
|
||||
|
||||
if not user or not os.getenv("SIMNOW_PASSWORD"):
|
||||
print("错误:请在 .env 填写 SIMNOW_USER / SIMNOW_PASSWORD")
|
||||
if not user or not pwd:
|
||||
print("错误:请在系统设置或 .env 填写 SimNow 投资者代码与密码")
|
||||
return 1
|
||||
|
||||
print("=== CTP 登录测试 ===")
|
||||
|
||||
+116
-2
@@ -14,8 +14,14 @@
|
||||
.settings-tips{flex:1;display:flex;flex-direction:column;justify-content:center;gap:.5rem;margin:0;padding:0;list-style:none;font-size:.85rem;color:var(--text-muted);line-height:1.55}
|
||||
.settings-tips li{padding-left:1rem;position:relative}
|
||||
.settings-tips li::before{content:"";position:absolute;left:0;top:.55em;width:5px;height:5px;border-radius:50%;background:var(--accent)}
|
||||
.settings-ctp-grid{display:grid;grid-template-columns:1fr 1fr;gap:.65rem .75rem}
|
||||
.settings-ctp-grid .field-full{grid-column:1/-1}
|
||||
.settings-ctp-section{margin-bottom:1rem;padding-bottom:1rem;border-bottom:1px solid var(--border)}
|
||||
.settings-ctp-section:last-of-type{border-bottom:none;margin-bottom:0;padding-bottom:0}
|
||||
.settings-ctp-section h3{font-size:.9rem;margin:0 0 .65rem;color:var(--text-title)}
|
||||
.settings-ctp-status{font-size:.82rem;color:var(--text-muted);margin-top:.75rem;line-height:1.5}
|
||||
@media(max-width:900px){
|
||||
.settings-password-form{grid-template-columns:1fr}
|
||||
.settings-ctp-grid{grid-template-columns:1fr}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -78,12 +84,120 @@
|
||||
</div>
|
||||
<button type="submit" class="btn-primary" style="margin-top:.75rem">保存交易设置</button>
|
||||
<p class="hint" style="margin-top:.75rem;margin-bottom:0">
|
||||
保证金上限用于开仓校验与品种最大手数估算(默认 30%)。<strong>移动保本</strong>:达 1R 后止损移至开仓价 ± N 跳(玉米 N=2 即 +2 点,棉花 N=2 即 +10 点);达 2R 移至 1R,依次类推。在 <code>.env</code> 配置 <code>SIMNOW_USER</code>,于「持仓监控」连接 CTP。
|
||||
保证金上限用于开仓校验与品种最大手数估算(默认 30%)。<strong>移动保本</strong>:达 1R 后止损移至开仓价 ± N 跳。CTP 账号与前置在下方「CTP 连接」中配置。
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>CTP 连接</h2>
|
||||
<form action="{{ url_for('settings') }}" method="post">
|
||||
<input type="hidden" name="action" value="ctp">
|
||||
<p class="hint" style="margin-bottom:.85rem">
|
||||
投资者代码、密码、前置地址在此维护(优先于 <code>.env</code>)。保存后请在持仓监控页点击「重连 CTP」。
|
||||
{% if ctp_status.connected %}
|
||||
<span class="badge profit" style="margin-left:.35rem">已连接</span>
|
||||
{% elif ctp_status.connecting %}
|
||||
<span class="badge planned" style="margin-left:.35rem">连接中</span>
|
||||
{% elif ctp_status.last_error %}
|
||||
<span class="text-loss" style="display:block;margin-top:.35rem">{{ ctp_status.last_error }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<div class="settings-ctp-section">
|
||||
<h3>SimNow 模拟盘</h3>
|
||||
<div class="settings-ctp-grid">
|
||||
<div class="field">
|
||||
<label>投资者代码</label>
|
||||
<input name="simnow_user" value="{{ ctp_cfg.simnow_user }}" placeholder="非手机号">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>交易密码</label>
|
||||
<input name="simnow_password" type="password" autocomplete="new-password"
|
||||
placeholder="{% if ctp_cfg.simnow_password_set %}已设置,留空不修改{% else %}SimNow 密码{% endif %}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>经纪商代码</label>
|
||||
<input name="simnow_broker_id" value="{{ ctp_cfg.simnow_broker_id }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>柜台环境</label>
|
||||
<select name="simnow_env">
|
||||
<option value="实盘" {% if ctp_cfg.simnow_env == '实盘' %}selected{% endif %}>实盘(看穿式,推荐)</option>
|
||||
<option value="测试" {% if ctp_cfg.simnow_env == '测试' %}selected{% endif %}>测试</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field field-full">
|
||||
<label>交易前置</label>
|
||||
<input name="simnow_td_address" value="{{ ctp_cfg.simnow_td_address }}" placeholder="tcp://180.168.146.187:10201">
|
||||
</div>
|
||||
<div class="field field-full">
|
||||
<label>行情前置</label>
|
||||
<input name="simnow_md_address" value="{{ ctp_cfg.simnow_md_address }}" placeholder="tcp://180.168.146.187:10211">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>AppID</label>
|
||||
<input name="simnow_app_id" value="{{ ctp_cfg.simnow_app_id }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>授权编码</label>
|
||||
<input name="simnow_auth_code" value="{{ ctp_cfg.simnow_auth_code }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-ctp-section">
|
||||
<h3>期货公司实盘(后期)</h3>
|
||||
<div class="settings-ctp-grid">
|
||||
<div class="field">
|
||||
<label>投资者代码</label>
|
||||
<input name="ctp_live_user" value="{{ ctp_cfg.ctp_live_user }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>交易密码</label>
|
||||
<input name="ctp_live_password" type="password" autocomplete="new-password"
|
||||
placeholder="{% if ctp_cfg.ctp_live_password_set %}已设置,留空不修改{% else %}实盘密码{% endif %}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>经纪商代码</label>
|
||||
<input name="ctp_live_broker_id" value="{{ ctp_cfg.ctp_live_broker_id }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>柜台环境</label>
|
||||
<select name="ctp_live_env">
|
||||
<option value="实盘" {% if ctp_cfg.ctp_live_env == '实盘' %}selected{% endif %}>实盘</option>
|
||||
<option value="测试" {% if ctp_cfg.ctp_live_env == '测试' %}selected{% endif %}>测试</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field field-full">
|
||||
<label>交易前置</label>
|
||||
<input name="ctp_live_td_address" value="{{ ctp_cfg.ctp_live_td_address }}" placeholder="tcp://...">
|
||||
</div>
|
||||
<div class="field field-full">
|
||||
<label>行情前置</label>
|
||||
<input name="ctp_live_md_address" value="{{ ctp_cfg.ctp_live_md_address }}" placeholder="tcp://...">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>AppID</label>
|
||||
<input name="ctp_live_app_id" value="{{ ctp_cfg.ctp_live_app_id }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>授权编码</label>
|
||||
<input name="ctp_live_auth_code" value="{{ ctp_cfg.ctp_live_auth_code }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary">保存 CTP 配置</button>
|
||||
<p class="settings-ctp-status">
|
||||
官方第一套:<code>180.168.146.187:10201/10211</code>;
|
||||
7×24:<code>182.254.243.31:40001/40011</code>(新账号可能需满 3 个交易日)。
|
||||
详见 <code>docs/SIMNOW.md</code>。
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="split-grid">
|
||||
<div class="card">
|
||||
<h2>行情说明</h2>
|
||||
|
||||
+12
-27
@@ -12,6 +12,7 @@ from locale_fix import ensure_process_locale
|
||||
|
||||
ensure_process_locale()
|
||||
|
||||
from ctp_settings import live_setting_dict, simnow_setting_dict
|
||||
from ctp_symbol import ths_to_vnpy_symbol, to_vnpy_exchange
|
||||
from contract_specs import get_contract_spec
|
||||
|
||||
@@ -43,35 +44,13 @@ _bridge: Optional["CtpBridge"] = None
|
||||
_bridge_lock = threading.Lock()
|
||||
|
||||
|
||||
def _env(key: str, default: str = "") -> str:
|
||||
return (os.getenv(key) or default).strip()
|
||||
|
||||
|
||||
def _simnow_setting() -> dict[str, str]:
|
||||
"""SimNow 仿真前置(可在 .env 覆盖)。看穿式前置需「柜台环境=实盘」。"""
|
||||
return {
|
||||
"用户名": _env("SIMNOW_USER"),
|
||||
"密码": _env("SIMNOW_PASSWORD"),
|
||||
"经纪商代码": _env("SIMNOW_BROKER_ID", "9999"),
|
||||
"交易服务器": _env("SIMNOW_TD_ADDRESS", "tcp://180.168.146.187:10201"),
|
||||
"行情服务器": _env("SIMNOW_MD_ADDRESS", "tcp://180.168.146.187:10211"),
|
||||
"产品名称": _env("SIMNOW_APP_ID", "simnow_client_test"),
|
||||
"授权编码": _env("SIMNOW_AUTH_CODE", "0000000000000000"),
|
||||
"柜台环境": _env("SIMNOW_ENV", "实盘"),
|
||||
}
|
||||
"""SimNow 仿真前置(系统设置优先,.env 兜底)。"""
|
||||
return simnow_setting_dict()
|
||||
|
||||
|
||||
def _live_setting() -> dict[str, str]:
|
||||
return {
|
||||
"用户名": _env("CTP_LIVE_USER"),
|
||||
"密码": _env("CTP_LIVE_PASSWORD"),
|
||||
"经纪商代码": _env("CTP_LIVE_BROKER_ID"),
|
||||
"交易服务器": _env("CTP_LIVE_TD_ADDRESS"),
|
||||
"行情服务器": _env("CTP_LIVE_MD_ADDRESS"),
|
||||
"产品名称": _env("CTP_LIVE_APP_ID"),
|
||||
"授权编码": _env("CTP_LIVE_AUTH_CODE"),
|
||||
"柜台环境": _env("CTP_LIVE_ENV", "实盘"),
|
||||
}
|
||||
return live_setting_dict()
|
||||
|
||||
|
||||
def _setting_for_mode(mode: str) -> dict[str, str]:
|
||||
@@ -206,12 +185,18 @@ class CtpBridge:
|
||||
self._connected_mode = None
|
||||
time.sleep(0.6)
|
||||
|
||||
def _wait_connected(self, mode: str) -> bool:
|
||||
def _login_rejected(self, ctp_logs: list[str]) -> bool:
|
||||
return any("登录失败" in m or "不合法的登录" in m for m in ctp_logs)
|
||||
|
||||
def _wait_connected(self, mode: str, ctp_logs: list[str] | None = None) -> bool:
|
||||
"""等待账户回报或交易通道登录成功。"""
|
||||
if not self._engine:
|
||||
return False
|
||||
logs = ctp_logs or []
|
||||
loops = max(1, int(CONNECT_WAIT_SEC / CONNECT_POLL_INTERVAL_SEC))
|
||||
for _ in range(loops):
|
||||
if self._login_rejected(logs):
|
||||
return False
|
||||
try:
|
||||
if self._engine.get_all_accounts():
|
||||
return True
|
||||
@@ -302,7 +287,7 @@ class CtpBridge:
|
||||
"并在服务器执行 nc -zv 验证出网。"
|
||||
)
|
||||
self._engine.connect(setting, GATEWAY_NAME)
|
||||
if self._wait_connected(mode):
|
||||
if self._wait_connected(mode, ctp_logs):
|
||||
self._connected_mode = mode
|
||||
self._last_error = ""
|
||||
logger.info("CTP 已连接 [%s] td_login=%s accounts=%s",
|
||||
|
||||
Reference in New Issue
Block a user