Files
qihuo/templates/settings.html
T

408 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# 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 %}>SimNowvnpy 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 %}