ab9987e4c7
Co-authored-by: Cursor <cursoragent@cursor.com>
408 lines
21 KiB
HTML
408 lines
21 KiB
HTML
{# Copyright (c) 2025-2026 马建军. All rights reserved. 专有软件,详见 LICENSE.zh-CN.txt #}
|
||
{% extends "base.html" %}
|
||
{% block title %}系统设置 - 国内期货监控系统{% endblock %}
|
||
{% block extra_css %}
|
||
<style>
|
||
.settings-page{display:flex;flex-direction:column;gap:1.25rem}
|
||
.settings-page .split-grid{margin-bottom:0}
|
||
.settings-page .split-grid .card{margin-bottom:0;min-height:100%;height:100%;display:flex;flex-direction:column}
|
||
.settings-page .split-grid .card > form,
|
||
.settings-page .split-grid .card > .card-inner{flex:1;display:flex;flex-direction:column}
|
||
.settings-password-form{display:grid;grid-template-columns:1fr 1fr;gap:.65rem .75rem}
|
||
.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}
|
||
.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-wrap .card-body{padding-top:0}
|
||
.settings-ctp-cards-row{
|
||
display:grid;grid-template-columns:1fr 1fr;gap:.75rem;
|
||
align-items:start;margin-bottom:.75rem;
|
||
}
|
||
.settings-ctp-cards-row .settings-ctp-fold.card{margin-bottom:0;height:100%}
|
||
.settings-ctp-cards-row .settings-ctp-grid{
|
||
grid-template-columns:repeat(6,minmax(0,1fr));
|
||
gap:.5rem .6rem;
|
||
}
|
||
.settings-ctp-cards-row .settings-ctp-grid .field{grid-column:span 2}
|
||
.settings-ctp-cards-row .settings-ctp-grid .field-ctp-front-span{grid-column:span 3}
|
||
.settings-ctp-cards-row .settings-ctp-grid .field label{font-size:.75rem}
|
||
.settings-ctp-cards-row .settings-ctp-grid input,
|
||
.settings-ctp-cards-row .settings-ctp-grid select{
|
||
padding:.45rem .55rem;font-size:.8rem;
|
||
}
|
||
.settings-ctp-fold.card{
|
||
margin-bottom:.75rem;padding:0;overflow:hidden;
|
||
border:1px solid var(--border);border-radius:8px;background:var(--card-inner);
|
||
}
|
||
.settings-ctp-fold.card:last-of-type{margin-bottom:0}
|
||
.settings-ctp-fold-head{
|
||
width:100%;display:flex;align-items:center;justify-content:space-between;gap:.75rem;
|
||
padding:.7rem 1rem;margin:0;border:none;background:transparent;cursor:pointer;
|
||
font-size:.92rem;font-weight:600;color:var(--text-title);text-align:left;
|
||
}
|
||
.settings-ctp-fold-head:hover{color:var(--accent)}
|
||
.settings-ctp-fold-title{display:flex;align-items:center;gap:.5rem}
|
||
.settings-ctp-fold-chevron{
|
||
flex-shrink:0;font-size:.72rem;color:var(--text-muted);
|
||
transition:transform .2s ease;
|
||
}
|
||
.settings-ctp-fold.is-collapsed .settings-ctp-fold-chevron{transform:rotate(-90deg)}
|
||
.settings-ctp-fold-body{padding:0 1rem .85rem}
|
||
.settings-ctp-fold.is-collapsed .settings-ctp-fold-body{display:none}
|
||
.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-cards-row{grid-template-columns:1fr}
|
||
.settings-ctp-grid{grid-template-columns:1fr}
|
||
.settings-ctp-cards-row .settings-ctp-grid .field,
|
||
.settings-ctp-cards-row .settings-ctp-grid .field-ctp-front-span{grid-column:span 1}
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
{% block content %}
|
||
<div class="settings-page">
|
||
|
||
<div class="split-grid">
|
||
<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="check-row">
|
||
{% for key, label in nav_toggles.items() %}
|
||
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;white-space:nowrap">
|
||
<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">
|
||
<h2>交易模式</h2>
|
||
<form action="{{ url_for('settings') }}" method="post">
|
||
<input type="hidden" name="action" value="trading">
|
||
<div class="form-grid">
|
||
<div class="field">
|
||
<label>交易通道</label>
|
||
<select name="trading_mode">
|
||
<option value="simulation" {% if trading_mode == 'simulation' %}selected{% endif %}>SimNow(vnpy CTP)</option>
|
||
<option value="live" {% if trading_mode == 'live' %}selected{% endif %}>期货公司 CTP(后期接入)</option>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label>计仓模式</label>
|
||
<select name="position_sizing_mode" id="position-sizing-mode">
|
||
<option value="fixed" {% if position_sizing_mode == 'fixed' %}selected{% endif %}>固定手数</option>
|
||
<option value="amount" {% if position_sizing_mode in ('amount', 'risk') %}selected{% endif %}>固定金额</option>
|
||
</select>
|
||
</div>
|
||
<div class="field" id="field-fixed-lots" {% if position_sizing_mode in ('amount', 'risk') %}hidden{% endif %}>
|
||
<label>固定手数(手)</label>
|
||
<input name="fixed_lots" type="number" step="1" min="1" value="{{ fixed_lots }}">
|
||
</div>
|
||
<div class="field" id="field-fixed-amount" {% if position_sizing_mode not in ('amount', 'risk') %}hidden{% endif %}>
|
||
<label>固定金额(元)</label>
|
||
<input name="fixed_amount" type="number" step="1" min="1" value="{{ fixed_amount }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>保证金占用上限(%)</label>
|
||
<input name="max_margin_pct" type="number" step="1" min="1" max="100" value="{{ max_margin_pct }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>移动保本缓冲(最小变动价位倍数)</label>
|
||
<input name="trailing_be_tick_buffer" type="number" step="1" min="1" max="20" value="{{ trailing_be_tick_buffer }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>开仓挂单超时(分钟)</label>
|
||
<input name="pending_order_timeout_min" type="number" step="1" min="1" max="60" value="{{ pending_order_timeout_min }}">
|
||
</div>
|
||
</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 跳。
|
||
<strong>挂单超时</strong>:限价开仓未成交时,超过设定分钟数自动向柜台撤单(1~60 分钟)。CTP 账号与前置在下方「CTP 连接」中配置。
|
||
</p>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card settings-ctp-wrap">
|
||
<h2>CTP 连接</h2>
|
||
<div class="card-body">
|
||
<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>
|
||
|
||
<form action="{{ url_for('settings') }}" method="post" id="ctp-settings-form">
|
||
<input type="hidden" name="action" value="ctp">
|
||
|
||
<div class="settings-ctp-cards-row">
|
||
<div class="settings-ctp-fold card{% if trading_mode != 'simulation' %} is-collapsed{% endif %}" data-ctp-fold="simnow">
|
||
<button type="button" class="settings-ctp-fold-head" aria-expanded="{{ 'true' if trading_mode == 'simulation' else 'false' }}">
|
||
<span class="settings-ctp-fold-title">
|
||
SimNow 模拟盘
|
||
{% if trading_mode == 'simulation' %}<span class="badge planned" style="font-size:.7rem">当前通道</span>{% endif %}
|
||
</span>
|
||
<span class="settings-ctp-fold-chevron" aria-hidden="true">▼</span>
|
||
</button>
|
||
<div class="settings-ctp-fold-body">
|
||
<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 id="simnow_password" name="simnow_password" type="password"
|
||
autocomplete="off" spellcheck="false"
|
||
placeholder="{% if ctp_cfg.simnow_password_set %}已设置:须重新输入才会更新{% else %}SimNow 交易密码(必填){% endif %}">
|
||
<p class="hint" style="margin:.25rem 0 0;font-size:.75rem">
|
||
与快期相同密码,保存前须在此<strong>手打</strong>;留空则不改。下方「修改密码」是网页登录密码,不是 SimNow。
|
||
</p>
|
||
</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">
|
||
<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 class="field field-ctp-front-span">
|
||
<label>行情前置</label>
|
||
<input name="simnow_md_address" value="{{ ctp_cfg.simnow_md_address }}" placeholder="tcp://180.168.146.187:10211">
|
||
</div>
|
||
<div class="field field-ctp-front-span">
|
||
<label>交易前置</label>
|
||
<input name="simnow_td_address" value="{{ ctp_cfg.simnow_td_address }}" placeholder="tcp://180.168.146.187:10201">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-ctp-fold card{% if trading_mode != 'live' %} is-collapsed{% endif %}" data-ctp-fold="live">
|
||
<button type="button" class="settings-ctp-fold-head" aria-expanded="{{ 'true' if trading_mode == 'live' else 'false' }}">
|
||
<span class="settings-ctp-fold-title">
|
||
期货公司实盘
|
||
{% if trading_mode == 'live' %}<span class="badge planned" style="font-size:.7rem">当前通道</span>{% endif %}
|
||
</span>
|
||
<span class="settings-ctp-fold-chevron" aria-hidden="true">▼</span>
|
||
</button>
|
||
<div class="settings-ctp-fold-body">
|
||
<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">
|
||
<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 class="field field-ctp-front-span">
|
||
<label>行情前置</label>
|
||
<input name="ctp_live_md_address" value="{{ ctp_cfg.ctp_live_md_address }}" placeholder="tcp://...">
|
||
</div>
|
||
<div class="field field-ctp-front-span">
|
||
<label>交易前置</label>
|
||
<input name="ctp_live_td_address" value="{{ ctp_cfg.ctp_live_td_address }}" placeholder="tcp://...">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn-primary">保存 CTP 配置</button>
|
||
<p class="settings-ctp-status">
|
||
官方第一套:<code>180.168.146.187:10201/10211</code>;
|
||
第二套(云服务器常用):<code>182.254.243.31:30001/30011</code>;
|
||
7×24:<code>182.254.243.31:40001/40011</code>(部分账号在 40001 会报「不合法登录」,与快期前置保持一致)。
|
||
详见 <code>docs/SIMNOW.md</code>。
|
||
</p>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="split-grid">
|
||
<div class="card">
|
||
<h2>行情说明</h2>
|
||
<div class="card-inner">
|
||
<p class="hint" style="font-size:.88rem;line-height:1.6;margin:0">
|
||
当前行情源:<strong class="text-accent">{{ quote_label }}</strong><br>
|
||
CTP 已连接时使用<strong>柜台行情</strong>;未连接时回退新浪接口。<br>
|
||
合约代码按同花顺格式(如 ag2608、IF2606)。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>企业微信推送</h2>
|
||
<form action="{{ url_for('settings') }}" method="post">
|
||
<input type="hidden" name="action" value="wechat">
|
||
<div class="field" style="margin-bottom:.75rem">
|
||
<label>Webhook 地址</label>
|
||
<input name="wechat_webhook" type="url" placeholder="https://qyapi.weixin.qq.com/..." value="{{ webhook }}">
|
||
</div>
|
||
<button type="submit" class="btn-primary">保存</button>
|
||
<p class="hint" style="margin-top:.75rem;margin-bottom:0">在企业微信群添加机器人后,粘贴 Webhook 地址保存。</p>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="split-grid">
|
||
<div class="card">
|
||
<h2>修改密码</h2>
|
||
<form action="{{ url_for('settings') }}" method="post" class="settings-password-form">
|
||
<input type="hidden" name="action" value="password">
|
||
<div class="field field-full">
|
||
<label>当前账号</label>
|
||
<input type="text" value="{{ username }}" disabled>
|
||
</div>
|
||
<div class="field">
|
||
<label>原密码</label>
|
||
<input name="old_password" type="password" required>
|
||
</div>
|
||
<div class="field">
|
||
<label>新密码</label>
|
||
<input name="new_password" type="password" required minlength="6" placeholder="至少 6 位">
|
||
</div>
|
||
<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>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>使用提示</h2>
|
||
<ul class="settings-tips">
|
||
<li>下单监控:连接 CTP 后下单、看持仓与可开仓品种</li>
|
||
<li>策略交易:趋势回调自动补仓;顺势加仓需先开仓</li>
|
||
<li>手续费:默认 CTP 柜台费率,连接后点同步</li>
|
||
<li>手机端:浏览器菜单可「添加到主屏幕」安装 App</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
{% endblock %}
|
||
{% block extra_js %}
|
||
<script>
|
||
(function () {
|
||
var sel = document.getElementById('position-sizing-mode');
|
||
var lotsField = document.getElementById('field-fixed-lots');
|
||
var amountField = document.getElementById('field-fixed-amount');
|
||
function syncSizingFields() {
|
||
if (!sel) return;
|
||
var isAmount = sel.value === 'amount';
|
||
if (lotsField) lotsField.hidden = isAmount;
|
||
if (amountField) amountField.hidden = !isAmount;
|
||
}
|
||
if (sel) sel.addEventListener('change', syncSizingFields);
|
||
syncSizingFields();
|
||
|
||
var CTP_FOLD_KEY = 'qihuo_ctp_fold';
|
||
function setCtpFold(el, collapsed) {
|
||
if (!el) return;
|
||
el.classList.toggle('is-collapsed', collapsed);
|
||
var head = el.querySelector('.settings-ctp-fold-head');
|
||
if (head) head.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
||
}
|
||
function saveCtpFoldState() {
|
||
var state = {};
|
||
document.querySelectorAll('[data-ctp-fold]').forEach(function (el) {
|
||
state[el.getAttribute('data-ctp-fold')] = el.classList.contains('is-collapsed');
|
||
});
|
||
try { localStorage.setItem(CTP_FOLD_KEY, JSON.stringify(state)); } catch (e) { /* ignore */ }
|
||
}
|
||
function loadCtpFoldState() {
|
||
try {
|
||
var raw = localStorage.getItem(CTP_FOLD_KEY);
|
||
if (!raw) return;
|
||
var state = JSON.parse(raw);
|
||
document.querySelectorAll('[data-ctp-fold]').forEach(function (el) {
|
||
var key = el.getAttribute('data-ctp-fold');
|
||
if (Object.prototype.hasOwnProperty.call(state, key)) {
|
||
setCtpFold(el, !!state[key]);
|
||
}
|
||
});
|
||
} catch (e) { /* ignore */ }
|
||
}
|
||
document.querySelectorAll('.settings-ctp-fold-head').forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
var panel = btn.closest('[data-ctp-fold]');
|
||
if (!panel) return;
|
||
setCtpFold(panel, !panel.classList.contains('is-collapsed'));
|
||
saveCtpFoldState();
|
||
});
|
||
});
|
||
loadCtpFoldState();
|
||
|
||
var ctpForm = document.getElementById('ctp-settings-form');
|
||
if (ctpForm) {
|
||
ctpForm.addEventListener('submit', function (ev) {
|
||
var simnowFold = document.querySelector('[data-ctp-fold="simnow"]');
|
||
if (simnowFold) setCtpFold(simnowFold, false);
|
||
var pwd = document.getElementById('simnow_password');
|
||
var pwdVal = pwd && pwd.value ? pwd.value.trim() : '';
|
||
var pwdWasSet = {{ 'true' if ctp_cfg.simnow_password_set else 'false' }};
|
||
if (pwdWasSet && !pwdVal) {
|
||
var ok = window.confirm(
|
||
'SimNow 交易密码为空,保存后不会更新密码(仍用旧密码)。\n\n'
|
||
+ '若快期已改密,请取消后在「交易密码」框手打新密码再保存。\n\n仍要保存其他项?'
|
||
);
|
||
if (!ok) ev.preventDefault();
|
||
}
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
{% endblock %}
|