94c566fbe5
Co-authored-by: Cursor <cursoragent@cursor.com>
617 lines
35 KiB
HTML
617 lines
35 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;align-items:start}
|
||
.settings-page .split-grid .card:not(.settings-fold){margin-bottom:0;min-height:100%;height:100%;display:flex;flex-direction:column}
|
||
.settings-page .split-grid .settings-fold.card,
|
||
.settings-page .split-grid .settings-ctp-fold.card{
|
||
margin-bottom:0;min-height:auto !important;height:auto !important;align-self:start;
|
||
display:flex;flex-direction:column;
|
||
}
|
||
.settings-page .split-grid .card > form,
|
||
.settings-page .split-grid .card > .card-inner,
|
||
.settings-page .split-grid .settings-fold-body > form,
|
||
.settings-page .split-grid .settings-fold-body > .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}
|
||
.settings-backup-table{width:100%;border-collapse:collapse;font-size:.82rem;margin-top:.65rem}
|
||
.settings-backup-table th,.settings-backup-table td{padding:.45rem .5rem;border-bottom:1px solid var(--border);text-align:left}
|
||
.settings-backup-table th{color:var(--text-muted);font-weight:600}
|
||
.settings-backup-restore{
|
||
margin-top:.85rem;padding:.75rem .85rem;border-radius:8px;
|
||
border:1px solid var(--border);background:var(--card-inner);
|
||
font-size:.82rem;color:var(--text-muted);line-height:1.6;
|
||
}
|
||
.settings-backup-restore summary{cursor:pointer;color:var(--text-title);font-weight:600}
|
||
.settings-backup-meta{font-size:.82rem;color:var(--text-muted);line-height:1.55;margin:.35rem 0 .65rem}
|
||
.settings-backup-actions{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem .65rem}
|
||
.settings-backup-download{color:var(--accent);text-decoration:none;font-weight:600}
|
||
.settings-backup-download:hover{text-decoration:underline}
|
||
.settings-admin-row .settings-compact-card{font-size:.78rem}
|
||
.settings-admin-row .settings-compact-card .hint,
|
||
.settings-admin-row .settings-backup-meta,
|
||
.settings-admin-row .settings-backup-restore{font-size:.72rem;line-height:1.5}
|
||
.settings-admin-row .settings-backup-table{font-size:.7rem}
|
||
.settings-admin-row .settings-backup-table th,
|
||
.settings-admin-row .settings-backup-table td{padding:.35rem .4rem}
|
||
.settings-admin-row .settings-backup-table td:first-child code{word-break:break-all;font-size:.68rem}
|
||
.settings-admin-row .field label{font-size:.72rem}
|
||
.settings-admin-row .field input{padding:.4rem .55rem;font-size:.78rem}
|
||
.settings-admin-row .settings-backup-config{display:grid;grid-template-columns:1fr;gap:.45rem;margin-bottom:.55rem}
|
||
.settings-admin-row .settings-backup-actions{margin-top:.35rem}
|
||
.settings-admin-row .settings-backup-actions .btn-primary,
|
||
.settings-admin-row .settings-compact-card > form .btn-primary{padding:.42rem .7rem;font-size:.78rem}
|
||
.settings-admin-row .settings-password-form{grid-template-columns:1fr;gap:.45rem .55rem}
|
||
.settings-admin-row .settings-password-form input{padding:.4rem .55rem;font-size:.78rem}
|
||
.settings-ai-full{margin-bottom:1.25rem}
|
||
.settings-ai-full .settings-fold.card{min-height:auto;height:auto}
|
||
.settings-ai-usage{margin-bottom:1rem;font-size:.84rem;color:var(--text-muted)}
|
||
.settings-ai-usage summary{cursor:pointer;color:var(--accent);font-weight:600;margin-bottom:.4rem}
|
||
.settings-ai-usage-body ul{margin:.25rem 0 0 1.1rem;padding:0;line-height:1.55}
|
||
.settings-ai-usage-body li{margin:.2rem 0}
|
||
.settings-ai-form{max-width:none}
|
||
.settings-ai-form input[type="checkbox"]{width:auto;flex-shrink:0;margin:0}
|
||
.settings-ai-form label.check-inline{
|
||
display:inline-flex;align-items:center;gap:.45rem;width:auto;
|
||
cursor:pointer;margin-bottom:0;font-size:.85rem;color:var(--text-muted)
|
||
}
|
||
.settings-ai-cards-row{
|
||
display:grid;grid-template-columns:1fr 1fr;gap:.85rem;margin-bottom:.85rem
|
||
}
|
||
.settings-ai-card{
|
||
border:1px solid var(--border);border-radius:10px;
|
||
padding:.85rem 1rem;background:var(--card-inner)
|
||
}
|
||
.settings-ai-card.is-active{border-color:var(--accent);box-shadow:0 0 0 1px rgba(56,189,248,.25)}
|
||
.settings-ai-card-head{
|
||
display:flex;align-items:center;justify-content:space-between;gap:.5rem;
|
||
margin-bottom:.65rem;font-size:.92rem;font-weight:600;color:var(--text-title)
|
||
}
|
||
.settings-ai-card .field{margin-bottom:.55rem}
|
||
.settings-ai-card .field:last-child{margin-bottom:0}
|
||
.settings-ai-daily{
|
||
border:1px solid var(--border);border-radius:10px;
|
||
padding:.85rem 1rem;background:var(--card-inner);margin-bottom:.85rem
|
||
}
|
||
.settings-ai-daily-grid{display:grid;grid-template-columns:auto 1fr 1fr;gap:.65rem .75rem;align-items:end}
|
||
.settings-ai-daily-grid .check-inline{align-self:center}
|
||
@media (max-width:768px){
|
||
.settings-ai-cards-row{grid-template-columns:1fr}
|
||
.settings-ai-daily-grid{grid-template-columns:1fr}
|
||
}
|
||
.settings-page .settings-fold.card{padding:0;overflow:hidden}
|
||
.settings-page .split-grid .settings-fold.card{min-height:auto;height:auto;align-self:start}
|
||
.settings-fold-head{
|
||
width:100%;display:flex;align-items:center;justify-content:space-between;gap:.75rem;
|
||
padding:1rem 1rem .85rem;margin:0;border:none;background:transparent;cursor:pointer;
|
||
text-align:left;
|
||
}
|
||
.settings-fold-head:hover .settings-fold-title{color:var(--accent)}
|
||
.settings-fold-title{
|
||
display:flex;align-items:center;gap:.5rem;font-size:1.15rem;font-weight:600;
|
||
color:var(--text-label);letter-spacing:.03em;
|
||
}
|
||
.settings-fold-title:before{
|
||
content:"";width:4px;height:16px;flex-shrink:0;
|
||
background:linear-gradient(180deg,var(--accent),var(--accent-2));
|
||
border-radius:2px;box-shadow:0 0 8px var(--card-glow);
|
||
}
|
||
.settings-fold-chevron{flex-shrink:0;font-size:.72rem;color:var(--text-muted);transition:transform .2s ease}
|
||
.settings-fold.is-collapsed .settings-fold-chevron{transform:rotate(-90deg)}
|
||
.settings-fold-body{padding:0 1rem 1rem;display:flex;flex-direction:column}
|
||
.settings-fold.is-collapsed .settings-fold-body{display:none}
|
||
.settings-admin-row .settings-fold-head{padding:.75rem .85rem .6rem}
|
||
.settings-admin-row .settings-fold-title{font-size:.95rem}
|
||
.settings-admin-row .settings-fold-body{padding:0 .85rem .85rem}
|
||
@media(max-width:900px){
|
||
.settings-admin-row{grid-template-columns:1fr}
|
||
.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 %}
|
||
{% macro settings_card(key, title, extra_class='') %}
|
||
<div class="card settings-fold is-collapsed {{ extra_class }}" data-settings-fold="{{ key }}">
|
||
<button type="button" class="settings-fold-head" aria-expanded="false">
|
||
<span class="settings-fold-title">{{ title }}</span>
|
||
<span class="settings-fold-chevron" aria-hidden="true">▼</span>
|
||
</button>
|
||
<div class="settings-fold-body">
|
||
{{ caller() }}
|
||
</div>
|
||
</div>
|
||
{% endmacro %}
|
||
<div class="settings-page">
|
||
|
||
<div class="split-grid">
|
||
{% call settings_card('nav', '导航显示') %}
|
||
<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>
|
||
{% endcall %}
|
||
|
||
{% call settings_card('trading', '交易模式') %}
|
||
<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="roll_max_margin_pct" type="number" step="1" min="1" max="100" value="{{ roll_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>。
|
||
滚仓保证金上限为滚仓后<strong>总持仓</strong>占用上限(默认 50%,可在下方修改)。
|
||
<strong>移动保本</strong>:达 1R 后止损移至开仓价 ± N 跳。
|
||
<strong>挂单超时</strong>:限价开仓未成交时,超过设定分钟数自动向柜台撤单(1~60 分钟)。
|
||
<span class="text-muted">{{ small_account_margin_rec.label }}。</span>
|
||
CTP 账号与前置在下方「CTP 连接」中配置。
|
||
</p>
|
||
</form>
|
||
{% endcall %}
|
||
</div>
|
||
|
||
{% call settings_card('ctp', 'CTP 连接', 'settings-ctp-wrap') %}
|
||
<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.disabled_hint %}
|
||
<span class="text-muted" style="display:block;margin-top:.35rem">{{ ctp_status.disabled_hint }}</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" data-simnow-pwd-set="{{ '1' if ctp_cfg.simnow_password_set else '0' }}">
|
||
<input type="hidden" name="action" value="ctp">
|
||
|
||
<div class="settings-ctp-auto card" style="margin-bottom:.85rem;padding:.75rem 1rem">
|
||
<label class="settings-ctp-auto-label" style="display:flex;align-items:flex-start;gap:.65rem;cursor:pointer;margin:0">
|
||
<input type="checkbox" name="ctp_auto_connect" value="1" {% if ctp_auto_connect %}checked{% endif %}
|
||
style="margin-top:.2rem;width:auto">
|
||
<span>
|
||
<strong>CTP 自动连接</strong>
|
||
<span class="hint" style="display:block;margin:.25rem 0 0;font-size:.78rem;line-height:1.55">
|
||
开启:盘前自动连接、断线重连、持仓页可连 CTP。关闭:立即断开,非交易时段不再重连;开盘前 30 分钟及交易时段仍会自动连接。
|
||
SimNow 非交易时段前置常不可用(与快期相同),建议收盘后关闭。
|
||
</span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="settings-ctp-cards-row">
|
||
<div class="settings-ctp-fold card is-collapsed" data-ctp-fold="simnow">
|
||
<button type="button" class="settings-ctp-fold-head" aria-expanded="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 is-collapsed" data-ctp-fold="live">
|
||
<button type="button" class="settings-ctp-fold-head" aria-expanded="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>
|
||
{% endcall %}
|
||
|
||
<div class="split-grid">
|
||
{% call settings_card('quote', '行情说明') %}
|
||
<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>
|
||
{% endcall %}
|
||
|
||
{% call settings_card('wechat', '企业微信推送') %}
|
||
<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>
|
||
{% endcall %}
|
||
</div>
|
||
|
||
<div class="settings-ai-full">
|
||
{% call settings_card('ai', 'AI 分析 · 使用说明') %}
|
||
<details class="settings-ai-usage" open>
|
||
<summary>使用说明</summary>
|
||
<div class="settings-ai-usage-body">
|
||
<ul>
|
||
<li><strong>触发时机</strong>:开仓成交、平仓入账、日终报告(默认日盘 15:05,可在下方修改)</li>
|
||
<li><strong>Ollama</strong>:服务器需能访问填写的地址(如本机 <code>127.0.0.1:11434</code>)</li>
|
||
<li><strong>OpenAI 兼容</strong>:支持 DeepSeek、硅基流动等 OpenAI 格式 API</li>
|
||
<li><strong>输出位置</strong>:分析写入导航「AI 消息」;若已配置企业微信,日终报告会同步推送摘要</li>
|
||
<li><strong>不替代交易</strong>:AI 仅作复盘与风险提示,下单仍以系统规则与 CTP 为准</li>
|
||
</ul>
|
||
</div>
|
||
</details>
|
||
<form action="{{ url_for('settings') }}" method="post" class="settings-ai-form">
|
||
<input type="hidden" name="action" value="ai">
|
||
<div class="field" style="margin-bottom:.75rem">
|
||
<label class="check-inline">
|
||
<input type="checkbox" name="ai_enabled" value="1" {% if ai_enabled %}checked{% endif %}>
|
||
<span>启用 AI 分析</span>
|
||
</label>
|
||
</div>
|
||
<div class="field" style="margin-bottom:.85rem;max-width:280px">
|
||
<label>当前使用的提供商</label>
|
||
<select name="ai_provider" id="ai-provider-select">
|
||
<option value="ollama" {% if ai_provider == 'ollama' %}selected{% endif %}>本地 Ollama</option>
|
||
<option value="openai" {% if ai_provider == 'openai' %}selected{% endif %}>OpenAI 兼容 API</option>
|
||
</select>
|
||
</div>
|
||
<div class="settings-ai-cards-row">
|
||
<div class="settings-ai-card{% if ai_provider == 'ollama' %} is-active{% endif %}" data-ai-provider="ollama">
|
||
<div class="settings-ai-card-head">
|
||
<span>本地 Ollama</span>
|
||
{% if ai_provider == 'ollama' %}<span class="badge profit">当前</span>{% endif %}
|
||
</div>
|
||
<div class="field">
|
||
<label>接口地址</label>
|
||
<input name="ai_ollama_base_url" type="url" placeholder="http://127.0.0.1:11434" value="{{ ai_ollama_base_url }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>模型名</label>
|
||
<input name="ai_ollama_model" type="text" placeholder="qwen2.5:7b" value="{{ ai_ollama_model }}">
|
||
</div>
|
||
</div>
|
||
<div class="settings-ai-card{% if ai_provider == 'openai' %} is-active{% endif %}" data-ai-provider="openai">
|
||
<div class="settings-ai-card-head">
|
||
<span>OpenAI 兼容</span>
|
||
{% if ai_provider == 'openai' %}<span class="badge profit">当前</span>{% endif %}
|
||
</div>
|
||
<div class="field">
|
||
<label>API Base URL</label>
|
||
<input name="ai_openai_base_url" type="url" placeholder="https://api.openai.com/v1" value="{{ ai_openai_base_url }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>API Key</label>
|
||
<input name="ai_openai_api_key" type="password" placeholder="sk-..." value="{{ ai_openai_api_key }}" autocomplete="off">
|
||
</div>
|
||
<div class="field">
|
||
<label>模型名</label>
|
||
<input name="ai_openai_model" type="text" placeholder="gpt-4o-mini" value="{{ ai_openai_model }}">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="settings-ai-daily">
|
||
<p class="hint" style="margin:0 0 .65rem">日终报告(国内期货日盘收盘后推送一次)</p>
|
||
<div class="settings-ai-daily-grid">
|
||
<label class="check-inline">
|
||
<input type="checkbox" name="ai_daily_report_enabled" value="1" {% if ai_daily_report_enabled %}checked{% endif %}>
|
||
<span>启用</span>
|
||
</label>
|
||
<div class="field" style="margin:0">
|
||
<label>报告时刻(时)</label>
|
||
<input name="ai_daily_report_hour" type="number" min="0" max="23" step="1" value="{{ ai_daily_report_hour }}">
|
||
</div>
|
||
<div class="field" style="margin:0">
|
||
<label>报告时刻(分)</label>
|
||
<input name="ai_daily_report_minute" type="number" min="0" max="59" step="1" value="{{ ai_daily_report_minute }}">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn-primary">保存 AI 配置</button>
|
||
</form>
|
||
{% endcall %}
|
||
</div>
|
||
|
||
<div class="split-grid settings-admin-row">
|
||
{% call settings_card('backup', '数据备份与恢复', 'settings-compact-card') %}
|
||
<p class="settings-backup-meta">
|
||
自动备份目录:<code>{{ backup_dir }}</code>
|
||
{% if backup_last_at %} · 上次备份 {{ backup_last_at.replace('T', ' ') }}{% else %} · 尚未备份{% endif %}
|
||
{% if backup_running %} · <span style="color:var(--accent)">备份进行中…</span>{% endif %}
|
||
</p>
|
||
<form action="{{ url_for('settings') }}" method="post" style="margin-bottom:.55rem">
|
||
<input type="hidden" name="action" value="backup_config">
|
||
<div class="settings-backup-config">
|
||
<div class="field">
|
||
<label style="display:flex;align-items:center;gap:.45rem;cursor:pointer">
|
||
<input type="checkbox" name="backup_auto_enabled" value="1" {% if backup_auto_enabled %}checked{% endif %}>
|
||
<span>启用每日自动备份</span>
|
||
</label>
|
||
</div>
|
||
<div class="field">
|
||
<label>自动备份时刻(0–23 点)</label>
|
||
<input name="backup_auto_hour" type="number" min="0" max="23" step="1" value="{{ backup_auto_hour }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>保留最近份数</label>
|
||
<input name="backup_keep_count" type="number" min="5" max="200" step="1" value="{{ backup_keep_count }}">
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn-primary">保存备份策略</button>
|
||
</form>
|
||
<div class="settings-backup-actions">
|
||
<form action="{{ url_for('settings') }}" method="post">
|
||
<input type="hidden" name="action" value="backup_now">
|
||
<button type="submit" class="btn-primary" {% if backup_running %}disabled{% endif %}>立即备份</button>
|
||
</form>
|
||
</div>
|
||
<p class="hint" style="margin:.5rem 0 0">备份含 <code>futures.db</code>、<code>uploads/</code>,默认恢复至 <code>{{ backup_restore_dir }}</code>。</p>
|
||
|
||
{% if backup_items %}
|
||
<table class="settings-backup-table">
|
||
<thead>
|
||
<tr><th>文件名</th><th>大小</th><th>时间</th><th></th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for item in backup_items %}
|
||
<tr>
|
||
<td><code>{{ item.name }}</code></td>
|
||
<td>{{ item.size_mb }} MB</td>
|
||
<td>{{ item.mtime.replace('T', ' ')[:16] }}</td>
|
||
<td><a href="{{ url_for('api_backup_download', filename=item.name) }}" class="settings-backup-download">下载</a></td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<p class="hint" style="margin-top:.5rem;margin-bottom:0">暂无备份,可点「立即备份」。</p>
|
||
{% endif %}
|
||
|
||
<details class="settings-backup-restore">
|
||
<summary>备份恢复说明</summary>
|
||
<ol style="margin:.5rem 0 0 1rem;padding:0">
|
||
<li>下载 <code>.tar.gz</code> 到目标服务器(如 <code>/root/</code>)。</li>
|
||
<li>解压:<code>tar -xzf qihuo_backup_*.tar.gz</code></li>
|
||
<li>执行:<code>chmod +x restore.sh && ./restore.sh</code></li>
|
||
<li>指定目录:<code>RESTORE_DIR=/opt/qihuo ./restore.sh</code></li>
|
||
<li>部署代码、配置 <code>.env</code> 后重启服务。</li>
|
||
</ol>
|
||
</details>
|
||
{% endcall %}
|
||
|
||
{% call settings_card('password', '登录账号', 'settings-compact-card') %}
|
||
<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 name="admin_username" type="text" value="{{ username }}" required maxlength="64"
|
||
pattern="[A-Za-z0-9_.@-]+" autocomplete="username">
|
||
</div>
|
||
<div class="field field-full">
|
||
<label>原密码</label>
|
||
<input name="old_password" type="password" required autocomplete="current-password">
|
||
</div>
|
||
<div class="field">
|
||
<label>新密码</label>
|
||
<input name="new_password" type="password" minlength="6" placeholder="留空则不修改" autocomplete="new-password">
|
||
</div>
|
||
<div class="field">
|
||
<label>确认新密码</label>
|
||
<input name="new_password2" type="password" minlength="6" placeholder="修改密码时填写" autocomplete="new-password">
|
||
</div>
|
||
<div class="field-full">
|
||
<button type="submit" class="btn-primary">保存账号</button>
|
||
</div>
|
||
<p class="hint" style="margin:.45rem 0 0;font-size:.72rem">保存后写入数据库,并同步至 <code>.env</code> 的 <code>ADMIN_USERNAME</code> / <code>ADMIN_PASSWORD</code>。</p>
|
||
</form>
|
||
{% endcall %}
|
||
</div>
|
||
|
||
<div class="split-grid">
|
||
{% call settings_card('tips', '使用提示') %}
|
||
<ul class="settings-tips">
|
||
<li>下单监控:连接 CTP 后下单、看持仓与可开仓品种</li>
|
||
<li>策略交易:趋势回调自动补仓;顺势加仓需先开仓</li>
|
||
<li>手续费:默认 CTP 柜台费率,连接后点同步</li>
|
||
<li>手机端:浏览器菜单可「添加到主屏幕」安装 App</li>
|
||
</ul>
|
||
{% endcall %}
|
||
</div>
|
||
|
||
</div>
|
||
{% endblock %}
|
||
{% block extra_js %}
|
||
<script src="{{ url_for('static', filename='js/settings.js') }}?v={{ asset_v }}"></script>
|
||
{% endblock %}
|