feat: 趋势户开仓类型大分歧A/B/小分歧,自动联动趋势波段与复盘

下单监控增加 entry_model 下拉;平仓写入短标签并预填复盘。Binance/OKX 用趋势 profile,Gate 日内仍用手选 trade_style。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-06 00:18:38 +08:00
parent 21894334f8
commit ea429b2694
20 changed files with 29324 additions and 28549 deletions
+9792 -9773
View File
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -16,7 +16,7 @@
<link rel="manifest" href="/static/icons/manifest.webmanifest">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
.order-trade-style-hint{font-size:.78rem;color:#8fc8ff;margin-left:4px;white-space:nowrap}
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px 20px}
.container{width:100%;max-width:min(1440px,94vw);margin:0 auto;padding:0 clamp(8px,1.5vw,20px)}
.header{display:flex;flex-direction:column;align-items:center;gap:8px;margin-bottom:12px}
@@ -385,10 +385,8 @@
<option value="price">止盈止损:价格模式</option>
<option value="pct">止盈止损:百分比模式</option>
</select>
<select name="trade_style" required>
<option value="trend">趋势单</option>
<option value="swing">波段单</option>
</select>
{% from 'order_entry_model_fields.html' import order_entry_type_fields with context %}
{{ order_entry_type_fields() }}
{% if position_sizing_mode != 'full_margin' %}
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
{% endif %}
@@ -464,7 +462,7 @@
</div>
<div class="pos-meta">
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span>
<span class="pos-meta-item">风格: {{ o.trade_style or 'trend' }}</span>
<span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span>
<span class="pos-meta-item">风险: {% if position_sizing_mode == 'full_margin' %}{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% else %}{{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% endif %}</span>
<span class="pos-meta-item" id="order-latest-risk-wrap-{{ o.id }}" style="display:none">最新风险: —</span>
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
@@ -798,11 +796,13 @@
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/order_entry_model.js?v=1"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/symbol_live_price.js?v=2"></script>
<script src="/static/strategy_roll.js?v=6"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const ORDER_ENTRY_MODEL_TRADE_STYLE = {{ entry_model_trade_style_map | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
function syncJournalEntryReasonOtherUi(){
@@ -1354,6 +1354,8 @@ function fillJournalFromTrade(t){
setJournalField("entry_reason", "趋势回调");
} else if(mt === "顺势加仓" && JOURNAL_ENTRY_REASON_OPTIONS.includes("顺势加仓")){
setJournalField("entry_reason", "顺势加仓");
} else if(t.effective_entry_reason && JOURNAL_ENTRY_REASON_OPTIONS.includes(t.effective_entry_reason)){
setJournalField("entry_reason", t.effective_entry_reason);
} else {
const erFromKey = KEY_ENTRY_REASON_BY_SIGNAL[kst] || "";
if(erFromKey && JOURNAL_ENTRY_REASON_OPTIONS.includes(erFromKey)){
+9627 -9604
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -365,10 +365,8 @@
<option value="price">止盈止损:价格模式</option>
<option value="pct">止盈止损:百分比模式</option>
</select>
<select name="trade_style" required>
<option value="trend">趋势单</option>
<option value="swing">波段单</option>
</select>
{% from 'order_entry_model_fields.html' import order_entry_type_fields with context %}
{{ order_entry_type_fields() }}
{% if position_sizing_mode != 'full_margin' %}
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
{% endif %}
@@ -431,7 +429,7 @@
</div>
<div class="pos-meta">
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span>
<span class="pos-meta-item">风格: {{ o.trade_style or 'trend' }}</span>
<span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span>
<span class="pos-meta-item">风险: {% if position_sizing_mode == 'full_margin' %}{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% else %}{{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% endif %}</span>
<span class="pos-meta-item" id="order-latest-risk-wrap-{{ o.id }}" style="display:none">最新风险: —</span>
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
@@ -765,11 +763,13 @@
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/order_entry_model.js?v=1"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/symbol_live_price.js?v=2"></script>
<script src="/static/strategy_roll.js?v=6"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const ORDER_ENTRY_MODEL_TRADE_STYLE = {{ entry_model_trade_style_map | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
function syncJournalEntryReasonOtherUi(){
@@ -1321,6 +1321,8 @@ function fillJournalFromTrade(t){
setJournalField("entry_reason", "趋势回调");
} else if(mt === "顺势加仓" && JOURNAL_ENTRY_REASON_OPTIONS.includes("顺势加仓")){
setJournalField("entry_reason", "顺势加仓");
} else if(t.effective_entry_reason && JOURNAL_ENTRY_REASON_OPTIONS.includes(t.effective_entry_reason)){
setJournalField("entry_reason", t.effective_entry_reason);
} else {
const erFromKey = KEY_ENTRY_REASON_BY_SIGNAL[kst] || "";
if(erFromKey && JOURNAL_ENTRY_REASON_OPTIONS.includes(erFromKey)){
+9168 -9149
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -394,10 +394,8 @@
<option value="price">止盈止损:价格模式</option>
<option value="pct">止盈止损:百分比模式</option>
</select>
<select name="trade_style" required>
<option value="trend">趋势单</option>
<option value="swing">波段单</option>
</select>
{% from 'order_entry_model_fields.html' import order_entry_type_fields with context %}
{{ order_entry_type_fields() }}
{% if position_sizing_mode != 'full_margin' %}
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
{% endif %}
@@ -460,7 +458,7 @@
</div>
<div class="pos-meta">
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span>
<span class="pos-meta-item">风格: {{ o.trade_style or 'trend' }}</span>
<span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span>
<span class="pos-meta-item">风险: {% if position_sizing_mode == 'full_margin' %}{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% else %}{{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% endif %}</span>
<span class="pos-meta-item" id="order-latest-risk-wrap-{{ o.id }}" style="display:none">最新风险: —</span>
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
@@ -794,11 +792,13 @@
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/order_entry_model.js?v=1"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/symbol_live_price.js?v=2"></script>
<script src="/static/strategy_roll.js?v=6"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const ORDER_ENTRY_MODEL_TRADE_STYLE = {{ entry_model_trade_style_map | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
function syncJournalEntryReasonOtherUi(){
@@ -1350,6 +1350,8 @@ function fillJournalFromTrade(t){
setJournalField("entry_reason", "趋势回调");
} else if(mt === "顺势加仓" && JOURNAL_ENTRY_REASON_OPTIONS.includes("顺势加仓")){
setJournalField("entry_reason", "顺势加仓");
} else if(t.effective_entry_reason && JOURNAL_ENTRY_REASON_OPTIONS.includes(t.effective_entry_reason)){
setJournalField("entry_reason", t.effective_entry_reason);
} else {
const erFromKey = KEY_ENTRY_REASON_BY_SIGNAL[kst] || "";
if(erFromKey && JOURNAL_ENTRY_REASON_OPTIONS.includes(erFromKey)){
+29
View File
@@ -0,0 +1,29 @@
# 策略文档
各交易实例的人工下单策略,供 UI / 复盘对齐。
| 文档 | 实例 | 状态 |
|------|------|------|
| [binance-alt-trend-long.md](./binance-alt-trend-long.md) | 币安山寨·多头趋势 | v0.2 |
| [okx-trend-both.md](./okx-trend-both.md) | OKX·多空趋势 | v0.2 |
| gate-intraday.md | Gate·BTC/ETH 日内 | **待定** |
## 约定
- **不写盈亏比**:止盈/止损随行情人工设定,不写入策略 MD。
- **界面短标签**`大分歧A` / `大分歧B` / `小分歧`;全称在 option `title` / 策略 MD 说明。
- **多空共用三字**:方向由「做多/做空」表达,不复用为「大分歧A多」等。
- **自动联动**:大分歧 A/B → 趋势单;小分歧 → 波段单;平仓写入交易记录 `entry_reason`;复盘「填入」自动带入。
- **日内 profile 独立**env 启用 `TRADE_SYMBOL_WHITELIST=BTC,ETH` 且限制开启时,**不显示**上述三项(Gate 当前);日内开仓类型后续单独定义。
- **策略模块独立**:趋势回调、顺势加仓不走上述三项。
## 系统实现
- 库:`lib/trade/entry_model_lib.py`
- 趋势户表单:`lib/instance/templates/order_entry_model_fields.html`
- 日内判定:`is_intraday_trading_profile()`(白名单仅含 BTC/ETH
## 相关文档
- [计仓模式](../position-sizing-mode.md)
- [趋势回调策略](../trend-pullback-strategy.md)
+88
View File
@@ -0,0 +1,88 @@
# 币安山寨·多头趋势账户
> **状态**:v0.2(已实现开仓类型 UI + 复盘联动)
---
## 1. 账户定位
| 项 | 说明 |
|----|------|
| 交易所 | 币安合约 |
| 方向 | **仅做多**`TRADE_DIRECTION=long_only` |
| 计仓 | `POSITION_SIZING_MODE=risk` |
| 关键位自动单 | `KEY_AUTO_ORDER_ENABLED=false` |
| UI profile | **趋势户**(非 BTC/ETH 白名单日内) |
---
## 2. 开仓类型(下单监控)
| 界面标签 | 存储 code | 联动 | 说明(hover / 本文) |
|----------|-----------|------|----------------------|
| **大分歧A** | `big_div_a` | 趋势单 | 突破前收敛小结构,不创新低企稳 |
| **大分歧B** | `big_div_b` | 趋势单 | 结构突破确认后入场 |
| **小分歧** | `small_div` | 波段单 | 二次探底 N 字突破,或 5m 三均线重新多头 |
- 下拉 **仅三选一**,不可手输。
- 原「趋势单/波段单」手选已隐藏,由开仓类型自动设定。
- A/B 差异在 **入场方式**,不绑定盈亏比。
---
## 3. 大分歧(趋势单)
**前提**:主升浪、上方有空间、大级别结构顺势。
### 3.1 大分歧A
- 4h/日线大结构向上
- 5m/15m 收敛,**不创新低** 企稳进(不等突破)
### 3.2 大分歧B
- 同上大级别多头结构
- **突破确认** 后入场
---
## 4. 小分歧(波段单)
- **前两次**可做,**第三次不做**
- 低吸为主,不追突破
- 入场:二次探底 → N 字突破;或 5m 三均线重新多头排列
---
## 5. 纪律
1. 不做空
2. 第三次小分歧不做新单
3. 小分歧不追突破
4. 止盈/止损/是否手平:**随行情**,本文档不量化
---
## 6. 持仓与出场(定性)
- 大分歧:可长持;途中两次小分歧后远目标未到,**可手平**
- 小分歧:短拿,常手平
---
## 7. 系统字段
| 操作 | 字段 |
|------|------|
| 下单 | `order_monitors.entry_model` + 自动 `trade_style` |
| 平仓 | `trade_records.entry_reason` = 界面标签 |
| 复盘 | 下拉同三项 + 策略项 +「其他」 |
---
## 修订记录
| 版本 | 日期 | 说明 |
|------|------|------|
| v0.2 | 2026-07-06 | 定稿 UI 短标签;实现代码联动 |
| v0.1 | 2026-07-06 | 讨论稿 |
+41
View File
@@ -0,0 +1,41 @@
# OKX·多空趋势账户
> **状态**v0.2
> 与 [binance-alt-trend-long.md](./binance-alt-trend-long.md) 共用开仓类型与 UI;本节仅补 **空侧镜像**
---
## 1. 账户差异
| 项 | 币安 | OKX |
|----|------|-----|
| 方向 | 仅多 | 多 + 空 |
| 开仓类型 UI | 大分歧A / B / 小分歧 | **相同三字** |
| profile | 趋势户 | 趋势户(非 BTC/ETH 日内白名单) |
---
## 2. 空侧镜像(标签不变)
| 标签 | 做多 | 做空 |
|------|------|------|
| 大分歧A | 不创新低企稳 | **不创新高**企稳 |
| 大分歧B | 向上突破确认 | **向下突破**确认 |
| 小分歧 | 二次探底 / N 字 / 5m 多头 | 二次**探顶** / 倒 N / 5m **空头** |
方向由表单「做多/做空」决定;复盘与交易记录仍存 `大分歧A` 等短标签。
---
## 3–6. 纪律 / 出场 / 系统字段
同 Binance 文档 §3–§7(含不写盈亏比、第三次小分歧不做等)。
---
## 修订记录
| 版本 | 日期 | 说明 |
|------|------|------|
| v0.2 | 2026-07-06 | 与 Binance 同步实现 |
| v0.1 | 2026-07-06 | 讨论稿 |
+1
View File
@@ -64,6 +64,7 @@
}
if (tab === "trade") {
if (typeof global.refreshOrderDefaults === "function") global.refreshOrderDefaults();
if (typeof global.initOrderEntryModelSelect === "function") global.initOrderEntryModelSelect();
if (global.ManualOrderRrPreview && typeof global.ManualOrderRrPreview.wire === "function") {
global.ManualOrderRrPreview.wire();
}
+1 -1
View File
@@ -1,4 +1,4 @@
*{margin:0;padding:0;box-sizing:border-box}
.order-trade-style-hint{font-size:.78rem;color:#8fc8ff;margin-left:4px;white-space:nowrap}
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px 20px}
.container{width:100%;max-width:min(1440px,94vw);margin:0 auto;padding:0 clamp(8px,1.5vw,20px)}
.header{display:flex;flex-direction:column;align-items:center;gap:8px;margin-bottom:12px}
+29
View File
@@ -0,0 +1,29 @@
(function (global) {
function syncOrderEntryModelTradeStyle() {
var sel = document.getElementById("order-entry-model");
var hidden = document.getElementById("order-trade-style-hidden");
var hint = document.getElementById("order-trade-style-hint");
if (!sel || !hidden) return;
var map = global.ORDER_ENTRY_MODEL_TRADE_STYLE || {};
var labels = { trend: "趋势单", swing: "波段单" };
var ts = map[sel.value] || "trend";
hidden.value = ts;
if (hint) hint.textContent = labels[ts] || ts;
}
function initOrderEntryModelSelect() {
var sel = document.getElementById("order-entry-model");
if (!sel || sel.dataset.entryModelWired === "1") return;
sel.dataset.entryModelWired = "1";
sel.addEventListener("change", syncOrderEntryModelTradeStyle);
syncOrderEntryModelTradeStyle();
}
global.initOrderEntryModelSelect = initOrderEntryModelSelect;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initOrderEntryModelSelect);
} else {
initOrderEntryModelSelect();
}
})(typeof window !== "undefined" ? window : globalThis);
@@ -1,5 +1,6 @@
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const ORDER_ENTRY_MODEL_TRADE_STYLE = {{ entry_model_trade_style_map | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
function reloadInstancePage(){
@@ -559,6 +560,8 @@ function fillJournalFromTrade(t){
setJournalField("entry_reason", "趋势回调");
} else if(mt === "顺势加仓" && JOURNAL_ENTRY_REASON_OPTIONS.includes("顺势加仓")){
setJournalField("entry_reason", "顺势加仓");
} else if(t.effective_entry_reason && JOURNAL_ENTRY_REASON_OPTIONS.includes(t.effective_entry_reason)){
setJournalField("entry_reason", t.effective_entry_reason);
} else {
const erFromKey = KEY_ENTRY_REASON_BY_SIGNAL[kst] || "";
if(erFromKey && JOURNAL_ENTRY_REASON_OPTIONS.includes(erFromKey)){
@@ -42,10 +42,8 @@
<option value="price">止盈止损:价格模式</option>
<option value="pct">止盈止损:百分比模式</option>
</select>
<select name="trade_style" required>
<option value="trend">趋势单</option>
<option value="swing">波段单</option>
</select>
{% from 'order_entry_model_fields.html' import order_entry_type_fields with context %}
{{ order_entry_type_fields() }}
{% if position_sizing_mode != 'full_margin' %}
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
{% endif %}
@@ -108,7 +106,7 @@
</div>
<div class="pos-meta">
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span>
<span class="pos-meta-item">风格: {{ o.trade_style or 'trend' }}</span>
<span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span>
<span class="pos-meta-item">风险: {% if position_sizing_mode == 'full_margin' %}{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% else %}{{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% endif %}</span>
<span class="pos-meta-item" id="order-latest-risk-wrap-{{ o.id }}" style="display:none">最新风险: —</span>
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
+1
View File
@@ -121,6 +121,7 @@
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/order_entry_model.js?v=1"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/symbol_live_price.js?v=2"></script>
<script src="/static/strategy_roll.js?v=6"></script>
@@ -0,0 +1,18 @@
{# 趋势户:开仓类型 → 自动 trade_style;日内户:仍选手动 trend/swing #}
{% macro order_entry_type_fields() -%}
{% if order_entry_profile == 'trend_div' %}
<select name="entry_model" id="order-entry-model" required title="大分歧A/B为趋势单,小分歧为波段单">
<option value="">开仓类型</option>
{% for opt in entry_model_options %}
<option value="{{ opt.code }}" title="{{ opt.help }}">{{ opt.label }}</option>
{% endfor %}
</select>
<input type="hidden" name="trade_style" id="order-trade-style-hidden" value="trend">
<span id="order-trade-style-hint" class="order-trade-style-hint" title="由开仓类型自动设定">趋势单</span>
{% else %}
<select name="trade_style" required>
<option value="trend">趋势单</option>
<option value="swing">波段单</option>
</select>
{% endif %}
{%- endmacro %}
@@ -10,7 +10,7 @@
<input name="real_rr" placeholder="实际RR">
</div>
<div class="form-grid journal-form-row2">
<select name="entry_reason" id="journal-entry-reason" required title="固定五种或选其他手写">
<select name="entry_reason" id="journal-entry-reason" required title="大分歧A/B/小分歧或策略项;选其他手写">
<option value="">开仓类型(必选)</option>
{% for er in entry_reason_options %}
<option value="{{ er }}">{{ er }}</option>
+223
View File
@@ -0,0 +1,223 @@
"""大分歧 / 小分歧开仓类型(趋势户);日内户沿用 trade_style,单独 profile。"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping, Optional, Sequence, Tuple
from lib.trade.trade_policy_lib import TradePolicy
PROFILE_TREND_DIV = "trend_div"
PROFILE_INTRADAY = "intraday"
ENTRY_MODEL_BIG_DIV_A = "big_div_a"
ENTRY_MODEL_BIG_DIV_B = "big_div_b"
ENTRY_MODEL_SMALL_DIV = "small_div"
VALID_ENTRY_MODEL_CODES = frozenset(
{
ENTRY_MODEL_BIG_DIV_A,
ENTRY_MODEL_BIG_DIV_B,
ENTRY_MODEL_SMALL_DIV,
}
)
TREND_DIV_ENTRY_REASON_LABELS: Tuple[str, ...] = (
"大分歧A",
"大分歧B",
"小分歧",
)
INTRADAY_LEGACY_TREND_ENTRY_REASONS: Tuple[str, ...] = (
"趋势多头:4h大结构突破前进场,确认条件:三次探顶,5m收敛不创新低",
"趋势空头:4h大结构突破前进场,确认条件:三次探底,5m收敛不创新高",
"趋势多头:小分歧低吸入场(左侧),确认条件:二次探底",
"趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶",
"波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20",
)
_ENTRY_SPECS: Tuple[Tuple[str, str, str, str], ...] = (
(
ENTRY_MODEL_BIG_DIV_A,
"大分歧A",
"trend",
"突破前收敛小结构,不创新低企稳(空:不创新高)",
),
(
ENTRY_MODEL_BIG_DIV_B,
"大分歧B",
"trend",
"结构突破确认后入场",
),
(
ENTRY_MODEL_SMALL_DIV,
"小分歧",
"swing",
"二次探底 N 字突破,或 5m 三均线重新多头(空:二次探顶 / 空头均线)",
),
)
_CODE_TO_LABEL = {code: label for code, label, _, _ in _ENTRY_SPECS}
_CODE_TO_STYLE = {code: style for code, _, style, _ in _ENTRY_SPECS}
_LABEL_TO_CODE = {label: code for code, label, _, _ in _ENTRY_SPECS}
_CODE_TO_HELP = {code: help for code, _, _, help in _ENTRY_SPECS}
_INTRADAY_WHITELIST = frozenset({"BTC", "ETH"})
@dataclass(frozen=True)
class EntryModelOption:
code: str
label: str
trade_style: str
help: str
def is_intraday_trading_profile(policy: TradePolicy) -> bool:
"""日内户:启用 BTC/ETH 白名单(env 中 TRADE_SYMBOL_WHITELIST)。"""
if not policy.symbol_restrict_enabled:
return False
if not policy.symbol_whitelist:
return False
return all(s in _INTRADAY_WHITELIST for s in policy.symbol_whitelist)
def order_entry_profile(policy: TradePolicy) -> str:
return PROFILE_INTRADAY if is_intraday_trading_profile(policy) else PROFILE_TREND_DIV
def entry_model_options() -> Tuple[EntryModelOption, ...]:
return tuple(
EntryModelOption(code=code, label=label, trade_style=style, help=help)
for code, label, style, help in _ENTRY_SPECS
)
def normalize_entry_model_code(raw: Optional[str]) -> str:
v = (raw or "").strip().lower()
if v in VALID_ENTRY_MODEL_CODES:
return v
label = (raw or "").strip()
if label in _LABEL_TO_CODE:
return _LABEL_TO_CODE[label]
return ""
def entry_model_label(code: Optional[str]) -> str:
c = normalize_entry_model_code(code)
return _CODE_TO_LABEL.get(c, "")
def trade_style_for_entry_model(code: Optional[str]) -> str:
c = normalize_entry_model_code(code)
return _CODE_TO_STYLE.get(c, "trend")
def trade_style_label_zh(trade_style: str) -> str:
return "波段单" if (trade_style or "").strip().lower() == "swing" else "趋势单"
def trend_manual_entry_reason_count(policy: TradePolicy) -> int:
if is_intraday_trading_profile(policy):
return len(INTRADAY_LEGACY_TREND_ENTRY_REASONS)
return len(TREND_DIV_ENTRY_REASON_LABELS)
def build_trend_div_entry_reason_options(
strategy_options: Sequence[str],
) -> Tuple[str, ...]:
return TREND_DIV_ENTRY_REASON_LABELS + tuple(strategy_options)
def build_intraday_entry_reason_options(
key_options: Sequence[str],
strategy_options: Sequence[str],
) -> Tuple[str, ...]:
return INTRADAY_LEGACY_TREND_ENTRY_REASONS + tuple(key_options) + tuple(strategy_options)
def entry_reason_options_for_policy(
policy: TradePolicy,
key_options: Sequence[str],
strategy_options: Sequence[str],
) -> Tuple[str, ...]:
if is_intraday_trading_profile(policy):
return build_intraday_entry_reason_options(key_options, strategy_options)
return build_trend_div_entry_reason_options(strategy_options)
def parse_manual_order_style_fields(
policy: TradePolicy,
form: Mapping[str, Any],
*,
default_trade_style: str = "trend",
) -> Tuple[str, Optional[str], Optional[str]]:
"""返回 (trade_style, entry_model_code|None, error_message|None)。"""
if is_intraday_trading_profile(policy):
trade_style = (form.get("trade_style") or default_trade_style or "trend").strip().lower()
if trade_style not in ("trend", "swing"):
trade_style = "trend"
return trade_style, None, None
entry_model = normalize_entry_model_code(form.get("entry_model"))
if not entry_model:
return "", None, "请选择开仓类型(大分歧A / 大分歧B / 小分歧)"
trade_style = trade_style_for_entry_model(entry_model)
return trade_style, entry_model, None
def resolve_trade_record_entry_reason(
*,
entry_reason: Optional[str] = None,
entry_model: Optional[str] = None,
key_signal_type: Optional[str] = None,
monitor_type: Optional[str] = None,
entry_reason_from_key_signal=None,
entry_reason_for_monitor_type=None,
) -> str:
er = (entry_reason or "").strip()
if er:
return er
label = entry_model_label(entry_model)
if label:
return label
kst = (key_signal_type or "").strip()
if kst and entry_reason_from_key_signal is not None:
from_key = (entry_reason_from_key_signal(kst) or "").strip()
if from_key:
return from_key
if entry_reason_for_monitor_type is not None:
from_mt = (entry_reason_for_monitor_type(monitor_type) or "").strip()
if from_mt:
return from_mt
return ""
def enrich_entry_model_display(item: dict) -> dict:
code = normalize_entry_model_code(item.get("entry_model"))
if code:
item["entry_model"] = code
item["entry_model_label"] = entry_model_label(code)
else:
item.setdefault("entry_model_label", "")
return item
def order_entry_template_context(policy: TradePolicy) -> dict:
profile = order_entry_profile(policy)
opts = entry_model_options()
return {
"order_entry_profile": profile,
"entry_model_options": [
{"code": o.code, "label": o.label, "trade_style": o.trade_style, "help": o.help}
for o in opts
],
"entry_model_trade_style_map": {o.code: o.trade_style for o in opts},
}
def migrate_entry_model_columns(conn) -> None:
for table in ("order_monitors", "trade_records"):
try:
conn.execute(f"ALTER TABLE {table} ADD COLUMN entry_model TEXT")
except Exception:
pass
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""Patch binance/okx/gate app.py for entry_model support."""
from __future__ import annotations
import os
import re
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
IMPORT_BLOCK = """from lib.trade.entry_model_lib import (
build_intraday_entry_reason_options,
build_trend_div_entry_reason_options,
enrich_entry_model_display,
migrate_entry_model_columns,
order_entry_template_context,
parse_manual_order_style_fields,
resolve_trade_record_entry_reason,
trend_manual_entry_reason_count,
)
"""
KEY_IMPORT = "from lib.key_monitor.key_auto_order_lib import (\n check_monitor_type_add_allowed,\n effective_entry_reason_options,\n effective_stats_segment_defs,\n load_key_auto_order_enabled,"
KEY_IMPORT_WITH_KEY_OPTS = "from lib.key_monitor.key_auto_order_lib import (\n KEY_ENTRY_REASON_OPTIONS,\n check_monitor_type_add_allowed,\n effective_entry_reason_options,\n effective_stats_segment_defs,\n load_key_auto_order_enabled,"
def patch_file(path: str, exchange: str) -> bool:
with open(path, "r", encoding="utf-8") as f:
text = f.read()
orig = text
if "from lib.trade.entry_model_lib import" not in text:
text = text.replace(
"from lib.trade.trade_policy_app_lib import (",
IMPORT_BLOCK + "from lib.trade.trade_policy_app_lib import (",
1,
)
if exchange == "gate":
old_er = '''# 与用户约定的固定开仓类型
ENTRY_REASON_OPTIONS = (
"趋势多头:4h大结构突破前进场,确认条件:三次探顶,5m收敛不创新低",
"趋势空头:4h大结构突破前进场,确认条件:三次探底,5m收敛不创新高",
"趋势多头:小分歧低吸入场(左侧),确认条件:二次探底",
"趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶",
"波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20",
"关键位箱体突破",
"关键位收敛突破",
"关键位斐波0.618",
"关键位斐波0.786",
"关键位假突破",
"关键位回调触价开仓",
"关键位突破触价开仓",
) + STRATEGY_ENTRY_REASON_OPTIONS'''
new_er = """# 日内户:长句开仓类型 + 关键位 + 策略(大分歧 A/B/小分歧 仅趋势户)
ENTRY_REASON_OPTIONS = build_intraday_entry_reason_options(
KEY_ENTRY_REASON_OPTIONS,
STRATEGY_ENTRY_REASON_OPTIONS,
)"""
text = text.replace(old_er, new_er)
if "KEY_ENTRY_REASON_OPTIONS," not in text.split("load_key_auto_order_enabled")[0]:
text = text.replace(KEY_IMPORT, KEY_IMPORT_WITH_KEY_OPTS, 1)
else:
old_er = '''# 与用户约定的固定开仓类型(仅做这几类单子)
ENTRY_REASON_OPTIONS = (
"趋势多头:4h大结构突破前进场,确认条件:三次探顶,5m收敛不创新低",
"趋势空头:4h大结构突破前进场,确认条件:三次探底,5m收敛不创新高",
"趋势多头:小分歧低吸入场(左侧),确认条件:二次探底",
"趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶",
"波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20",
"关键位箱体突破",
"关键位收敛突破",
"关键位斐波0.618",
"关键位斐波0.786",
"关键位假突破",
"关键位回调触价开仓",
"关键位突破触价开仓",
) + STRATEGY_ENTRY_REASON_OPTIONS'''
new_er = """# 趋势户:大分歧A/B/小分歧 + 策略(关键位本实例关闭)
ENTRY_REASON_OPTIONS = build_trend_div_entry_reason_options(STRATEGY_ENTRY_REASON_OPTIONS)"""
text = text.replace(old_er, new_er)
if "migrate_entry_model_columns(conn)" not in text:
text = text.replace(
" conn.commit()\n conn.close()\n\n\ndef get_db",
" migrate_entry_model_columns(conn)\n conn.commit()\n conn.close()\n\n\ndef get_db",
1,
)
text = re.sub(
r" er = \(\n \(entry_reason or \"\"\)\.strip\(\)\n or entry_reason_from_key_signal\(kst\)\n or entry_reason_for_monitor_type\(monitor_type\)\n or \"\"\n \)",
""" er = resolve_trade_record_entry_reason(
entry_reason=entry_reason,
entry_model=entry_model,
key_signal_type=kst,
monitor_type=monitor_type,
entry_reason_from_key_signal=entry_reason_from_key_signal,
entry_reason_for_monitor_type=entry_reason_for_monitor_type,
)""",
text,
count=1,
)
if "entry_model=None," not in text:
text = text.replace(
" entry_reason=None,\n trend_plan_id=None,",
" entry_reason=None,\n entry_model=None,\n trend_plan_id=None,",
1,
)
if "enrich_entry_model_display(item)" not in text:
text = text.replace(
" enrich_order_display_fields(item, calc_rr_ratio)\n try:",
" enrich_order_display_fields(item, calc_rr_ratio)\n enrich_entry_model_display(item)\n try:",
1,
)
text = text.replace(
""" trade_style = (d.get("trade_style") or DEFAULT_TRADE_STYLE or "trend").strip().lower()
if trade_style not in ("trend", "swing"):
trade_style = "trend"
available_usdt = get_available_trading_usdt()""",
""" trade_style, entry_model, style_err = parse_manual_order_style_fields(
TRADE_POLICY, d, default_trade_style=DEFAULT_TRADE_STYLE or "trend"
)
if style_err:
conn.close()
flash(style_err)
return redirect("/trade")
available_usdt = get_available_trading_usdt()""",
1,
)
old_insert = (
'"INSERT INTO order_monitors (symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, margin_capital, leverage, trade_style, risk_percent, risk_amount, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, time_close_enabled, time_close_hours, time_close_at_ms) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",\n'
" (\n"
" symbol, exchange_symbol, direction, trigger_price, stop_loss, stop_loss, take_profit,\n"
" margin_capital, leverage, trade_style, risk_percent_db, risk_amount_final, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, 0, breakeven_price,\n"
" breakeven_enabled,\n"
" notional_value, position_ratio, base_amount, amount, open_order_id, opened_at_bj, opened_at_ms, trading_day,\n"
" ORDER_MONITOR_TYPE_MANUAL,\n"
" tc_en, tc_h, tc_at,\n"
" )"
)
new_insert = (
'"INSERT INTO order_monitors (symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, margin_capital, leverage, trade_style, entry_model, risk_percent, risk_amount, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, time_close_enabled, time_close_hours, time_close_at_ms) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",\n'
" (\n"
" symbol, exchange_symbol, direction, trigger_price, stop_loss, stop_loss, take_profit,\n"
" margin_capital, leverage, trade_style, entry_model, risk_percent_db, risk_amount_final, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, 0, breakeven_price,\n"
" breakeven_enabled,\n"
" notional_value, position_ratio, base_amount, amount, open_order_id, opened_at_bj, opened_at_ms, trading_day,\n"
" ORDER_MONITOR_TYPE_MANUAL,\n"
" tc_en, tc_h, tc_at,\n"
" )"
)
text = text.replace(old_insert, new_insert)
text = text.replace(
""" effective_entry_reason_options(
ENTRY_REASON_OPTIONS,
POSITION_SIZING_MODE,
KEY_AUTO_ORDER_ENABLED,
)""",
""" effective_entry_reason_options(
ENTRY_REASON_OPTIONS,
POSITION_SIZING_MODE,
KEY_AUTO_ORDER_ENABLED,
trend_manual_count=trend_manual_entry_reason_count(TRADE_POLICY),
)""",
1,
)
if "**order_entry_template_context(TRADE_POLICY)," not in text:
text = text.replace(
" trade_policy=trade_policy_template_context(TRADE_POLICY),",
" trade_policy=trade_policy_template_context(TRADE_POLICY),\n **order_entry_template_context(TRADE_POLICY),",
1,
)
# insert_trade_record from order row: add entry_model
text = re.sub(
r"(insert_trade_record\(\n\s+conn,\n(?:[^\n]+\n)+?\s+trade_style=r\[\"trade_style\"\],\n)",
r"\1 entry_model=(r[\"entry_model\"] if \"entry_model\" in r.keys() else None),\n",
text,
)
if text != orig:
with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write(text)
return True
return False
def main():
for ex in ("binance", "okx", "gate"):
path = os.path.join(REPO, f"crypto_monitor_{ex}", "app.py")
changed = patch_file(path, ex)
print(f"{ex}: {'patched' if changed else 'no change'}")
if __name__ == "__main__":
main()
+75
View File
@@ -0,0 +1,75 @@
import unittest
from lib.trade.entry_model_lib import (
ENTRY_MODEL_BIG_DIV_A,
ENTRY_MODEL_BIG_DIV_B,
ENTRY_MODEL_SMALL_DIV,
build_trend_div_entry_reason_options,
entry_model_label,
is_intraday_trading_profile,
parse_manual_order_style_fields,
resolve_trade_record_entry_reason,
trade_style_for_entry_model,
trend_manual_entry_reason_count,
)
from lib.trade.trade_policy_lib import TradePolicy, load_trade_policy
class TestEntryModelLib(unittest.TestCase):
def test_intraday_profile_btc_eth_whitelist(self):
policy = load_trade_policy(
{
"TRADE_SYMBOL_RESTRICT_ENABLED": "true",
"TRADE_SYMBOL_WHITELIST": "BTC,ETH",
}
)
self.assertTrue(is_intraday_trading_profile(policy))
self.assertEqual(trend_manual_entry_reason_count(policy), 5)
def test_trend_div_profile_alt(self):
policy = load_trade_policy(
{
"TRADE_SYMBOL_RESTRICT_ENABLED": "false",
"TRADE_SYMBOL_WHITELIST": "BTC,ETH",
}
)
self.assertFalse(is_intraday_trading_profile(policy))
self.assertEqual(trend_manual_entry_reason_count(policy), 3)
def test_entry_model_maps_trade_style(self):
self.assertEqual(trade_style_for_entry_model(ENTRY_MODEL_BIG_DIV_A), "trend")
self.assertEqual(trade_style_for_entry_model(ENTRY_MODEL_SMALL_DIV), "swing")
self.assertEqual(entry_model_label(ENTRY_MODEL_BIG_DIV_A), "大分歧A")
def test_parse_trend_div_requires_entry_model(self):
policy = TradePolicy(False, "both", False, ())
style, code, err = parse_manual_order_style_fields(policy, {})
self.assertTrue(err)
self.assertEqual(code, None)
style, code, err = parse_manual_order_style_fields(
policy, {"entry_model": ENTRY_MODEL_SMALL_DIV}
)
self.assertIsNone(err)
self.assertEqual(code, ENTRY_MODEL_SMALL_DIV)
self.assertEqual(style, "swing")
def test_parse_intraday_uses_trade_style(self):
policy = TradePolicy(False, "both", True, ("BTC", "ETH"))
style, code, err = parse_manual_order_style_fields(policy, {"trade_style": "swing"})
self.assertIsNone(err)
self.assertIsNone(code)
self.assertEqual(style, "swing")
def test_resolve_entry_reason_from_model(self):
er = resolve_trade_record_entry_reason(entry_model=ENTRY_MODEL_BIG_DIV_B)
self.assertEqual(er, "大分歧B")
def test_build_trend_div_journal_options(self):
opts = build_trend_div_entry_reason_options(("趋势回调",))
self.assertEqual(opts[:3], ("大分歧A", "大分歧B", "小分歧"))
self.assertIn("趋势回调", opts)
if __name__ == "__main__":
unittest.main()