Add key-level auto trade, AI analysis, and trading UX improvements.

Key monitors use 5m close triggers with WeChat alerts and box/convergence auto orders; add pending-order worker, structured WeChat notify, AI settings/messages, session clock, CTP margin sizing, and dual-layer position limits.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-28 10:36:56 +08:00
parent 0109b59f27
commit 840e88daad
33 changed files with 2514 additions and 143 deletions
+39
View File
@@ -0,0 +1,39 @@
{# Copyright (c) 2025-2026 马建军. All rights reserved. 专有软件,详见 LICENSE.zh-CN.txt #}
{% extends "base.html" %}
{% block title %}AI 消息 - 国内期货 · 交易复盘系统{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/ai_messages.css') }}">
{% endblock %}
{% block content %}
<div class="card ai-page">
<h2>AI 分析 · 使用说明</h2>
<div class="card-body">
<details class="ai-usage" open>
<summary>使用说明</summary>
<div class="ai-usage-body">
<ul>
<li><a href="{{ url_for('settings') }}">系统设置</a> →「AI 分析 · 使用说明」中配置 Ollama 或 OpenAI 并启用</li>
<li><strong>开仓 / 平仓</strong>:成交后自动生成简要复盘(本页下方列表)</li>
<li><strong>日终报告</strong>:每个交易日按设定时刻汇总当日盈亏与持仓</li>
<li>配置企业微信后,日终报告摘要会同步推送到群</li>
</ul>
</div>
</details>
<h3 class="ai-section-label">分析消息</h3>
<div class="ai-msg-list card-scroll">
{% for m in messages %}
<article class="ai-msg" data-kind="{{ m.kind }}">
<header class="ai-msg-head">
<span class="ai-msg-kind">{{ m.kind }}</span>
<time>{{ m.created_at }}</time>
</header>
{% if m.title %}<h3 class="ai-msg-title">{{ m.title }}</h3>{% endif %}
<pre class="ai-msg-body">{{ m.content }}</pre>
</article>
{% else %}
<p class="text-muted empty-hint">暂无 AI 消息。开启 AI 并完成一笔交易,或等待日终报告后此处会显示分析。</p>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
+1
View File
@@ -60,6 +60,7 @@
{% if nav_items.strategy %}<a href="{{ url_for('strategy_page') }}" class="{% if request.endpoint in ('strategy_page', 'strategy_records_page') %}active{% endif %}">策略交易</a>{% endif %}
{% if nav_items.plans %}<a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a>{% endif %}
<a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a>
{% if nav_items.ai %}<a href="{{ url_for('ai_messages_page') }}" class="{% if request.endpoint == 'ai_messages_page' %}active{% endif %}">AI 分析</a>{% endif %}
{% if nav_items.market %}<a href="{{ url_for('market_page') }}" class="{% if request.endpoint == 'market_page' %}active{% endif %}">行情K线</a>{% endif %}
<a href="{{ url_for('records') }}" class="{% if request.endpoint in ('records', 'trades') %}active{% endif %}">交易记录与复盘</a>
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
+65 -17
View File
@@ -1,12 +1,34 @@
{# Copyright (c) 2025-2026 马建军. All rights reserved. 专有软件,详见 LICENSE.zh-CN.txt #}
{% extends "base.html" %}
{% block title %}关键位监控 - 国内期货 · 交易复盘系统{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/keys.css') }}">
{% endblock %}
{% block content %}
<div class="split-grid">
<div class="card">
<h2>新增监控</h2>
<div class="card-body">
<form action="{{ url_for('add_key') }}" method="post" class="form-compact">
<details class="key-rules" open>
<summary>规则说明</summary>
<div class="key-rules-body">
<p><strong>箱体突破 / 收敛突破(自动单)</strong></p>
<ul>
<li>触发:<strong>5 分钟 K 线收盘</strong>收在上沿或下沿之外</li>
<li>顺势:上破做多、下破做空;反转:上破做空、下破做多</li>
<li>自动<strong>市价开仓</strong>;止损 = 突破 K 线极值 ± 2 跳</li>
<li>盈亏比默认 2(可改);开启移动保本时默认 3,达目标价自动止盈</li>
<li>成交后进入「下单监控」持仓,来源备注为监控类型</li>
</ul>
<p><strong>关键支阻区(仅提醒)</strong></p>
<ul>
<li>上沿 = 阻力,下沿 = 支撑,合并为一个区间</li>
<li>5m 收盘突破上沿或跌破下沿 → 微信推送(最多 3 次,间隔约 5 分钟)</li>
<li>推送完毕后自动结案,<strong>不参与自动开仓</strong></li>
</ul>
</div>
</details>
<form action="{{ url_for('add_key') }}" method="post" class="form-compact" id="key-add-form">
<div class="form-line line-3">
<div class="symbol-wrap symbol-mains">
<input type="text" class="symbol-input" placeholder="主力合约" autocomplete="off" required>
@@ -17,22 +39,31 @@
<div class="symbol-dropdown"></div>
<div class="symbol-selected"></div>
</div>
<select name="type" required>
<select name="type" id="key-type" required>
<option value="箱体突破">箱体突破</option>
<option value="收敛突破">收敛突破</option>
<option value="关键阻力位">关键阻力位</option>
<option value="关键支撑位">关键支撑位</option>
</select>
<select name="direction" required>
<option value="">方向</option>
<option value="long">做多</option>
<option value="short">做空</option>
<option value="关键支阻区">关键支阻区</option>
</select>
<div id="key-trade-mode-wrap">
<select name="trade_mode" id="key-trade-mode">
<option value="顺势">顺势</option>
<option value="反转">反转</option>
</select>
</div>
</div>
<div class="form-line line-3">
<input name="upper" type="number" step="0.0001" placeholder="上沿/阻力" required>
<input name="lower" type="number" step="0.0001" placeholder="下沿/支撑" required>
<button type="submit" class="btn-primary">添加</button>
<div class="form-line line-3" id="key-row-prices">
<input name="upper" type="number" step="0.0001" placeholder="上沿阻力" required>
<input name="lower" type="number" step="0.0001" placeholder="下沿支撑" required>
<div id="key-rr-wrap">
<input name="risk_reward" id="key-rr" type="number" step="0.1" min="0.5" max="10" value="2" placeholder="盈亏比">
</div>
</div>
<div class="form-line line-key-actions" id="key-row-actions">
<label class="key-check" id="key-trailing-wrap">
<input type="checkbox" name="trailing_be" id="key-trailing" value="1">
<span class="key-check-text">移动保本(默认盈亏比 3,达 3R 止盈)</span>
</label>
<button type="submit" class="btn-primary key-submit-btn">添加</button>
</div>
</form>
<h3 class="section-label">监控列表</h3>
@@ -41,13 +72,22 @@
<div class="list-item key-item" data-key-id="{{ k.id }}" style="padding:.75rem;font-size:.85rem">
<div>
<strong>{{ k.symbol_name or k.symbol }}</strong> {{ k.monitor_type }}
<span class="badge dir">{{ '多' if k.direction == 'long' else '空' }}</span>
{% if k.monitor_type in ('箱体突破', '收敛突破') %}
<span class="badge planned">{{ k.trade_mode or '顺势' }}</span>
{% endif %}
{% if k.trailing_be %}
<span class="badge profit">移动保本</span>
{% endif %}
</div>
<div class="key-live">
<span class="live-price-line">现价:<span class="live-price">--</span></span>
<span class="live-dist">距上<span class="dist-up">--</span> 距下<span class="dist-down">--</span></span>
</div>
<div>上{{ k.upper }} {{ k.lower }}</div>
<div>沿 {{ k.upper }} · 下沿 {{ k.lower }}
{% if k.monitor_type in ('箱体突破', '收敛突破') %}
· 盈亏比 {{ k.risk_reward or 2 }}
{% endif %}
</div>
<a href="{{ url_for('del_key', pid=k.id) }}" class="btn-del" onclick="return confirm('移入历史?')"></a>
</div>
{% else %}
@@ -61,13 +101,21 @@
<h2>监控历史</h2>
<div class="card-body card-scroll">
<table>
<thead><tr><th>品种</th><th>类型</th><th>方向</th><th>上沿</th><th>下沿</th><th>归档</th></tr></thead>
<thead><tr><th>品种</th><th>类型</th><th>模式</th><th>上沿</th><th>下沿</th><th>归档</th></tr></thead>
<tbody>
{% for k in history %}
<tr>
<td>{{ k.symbol_name or k.symbol }}</td>
<td>{{ k.monitor_type }}</td>
<td><span class="badge dir">{{ '多' if k.direction == 'long' else '空' }}</span></td>
<td>
{% if k.monitor_type in ('箱体突破', '收敛突破') %}
{{ k.trade_mode or '顺势' }}{% if k.trailing_be %} · 移动保本{% endif %}
{% elif k.monitor_type in ('关键阻力位', '关键支撑位') %}
支阻区
{% else %}
提醒
{% endif %}
</td>
<td>{{ k.upper }}</td>
<td>{{ k.lower }}</td>
<td>{{ k.archived_at[:16] if k.archived_at else '' }}</td>
+131 -2
View File
@@ -85,6 +85,42 @@
.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}
.settings-fold-head{
@@ -180,6 +216,10 @@
<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 }}">
@@ -191,8 +231,12 @@
</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 连接」中配置
开仓保证金上限用于开仓校验与品种最大手数估算(默认 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 %}
@@ -369,6 +413,91 @@
{% 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">
+20 -1
View File
@@ -17,6 +17,23 @@
{% if ctp_account.available is defined and ctp_status.connected %}
<span class="text-muted">可用 <strong id="avail-display">{{ '%.2f'|format(ctp_account.available) }}</strong></span>
{% endif %}
<span class="text-muted trade-session-clock" id="session-clock-wrap">
· <span id="clock-now">{{ session_clock.now_time }}</span>
· <span id="clock-status" class="{% if session_clock.in_session %}text-profit{% else %}text-muted{% endif %}">{{ session_clock.status_label }}</span>
<span id="clock-detail" class="session-clock-detail">
{% if not session_clock.in_session and session_clock.next_open_at %}
· 下次{{ session_clock.next_open_label }} {{ session_clock.next_open_at }}
· 距开盘 <strong id="clock-countdown-open">{{ session_clock.countdown_open }}</strong>
{% elif session_clock.in_session %}
{% if session_clock.countdown_break %}
· 距{{ session_clock.break_label }} <strong id="clock-countdown-break">{{ session_clock.countdown_break }}</strong>
{% endif %}
{% if session_clock.countdown_close %}
· 距{{ session_clock.close_label }} <strong id="clock-countdown-close">{{ session_clock.countdown_close }}</strong>
{% endif %}
{% endif %}
</span>
</span>
</div>
<div class="trade-top-bar-actions">
<button type="button" class="btn-primary btn-ctp-sm" id="btn-ctp-connect"
@@ -135,6 +152,7 @@
<p class="hint">最大手数 = floor(权益 × 保证金上限 <strong>{{ max_margin_pct }}%</strong> ÷ 1手保证金);当前权益 <strong class="text-accent" id="rec-capital">{{ '%.2f'|format(recommend_capital) }}</strong> 元。
{% if sizing_mode == 'fixed' %}仅显示最大手数 ≥ <strong>{{ fixed_lots }}</strong> 手的品种。{% endif %}
{% if small_account_scope %}<span class="text-muted">{{ small_account_scope_hint }}。</span>{% endif %}
{% if small_account_margin_rec %}<span class="text-muted">{{ small_account_margin_rec.label }}。</span>{% endif %}
{% if night_session %}<span class="text-muted">当前为夜盘时段,品种下拉与下表仅显示有夜盘品种;带「夜盘」标记。</span>{% elif not small_account_scope %}<span class="text-muted">有夜盘交易的品种带「夜盘」标记。</span>{% endif %}
保证金优先读取 CTP 柜台合约信息。
{% if recommend_updated_at %}<span class="text-muted">每日后台更新 · 最近 {{ recommend_updated_at }}</span>{% else %}<span class="text-muted" id="rec-updated">等待今日后台刷新…</span>{% endif %}
@@ -237,7 +255,8 @@
'fixed_amount': fixed_amount,
'product_categories': product_categories | default([]),
'recommend_rows': recommend_rows | default([]),
'ctp_auto_connect': ctp_auto_connect
'ctp_auto_connect': ctp_auto_connect,
'session_clock': session_clock
} | tojson }}</script>
<script src="{{ url_for('static', filename='js/trade.js') }}?v={{ asset_v }}"></script>
{% endblock %}