feat: 趋势户开仓类型大分歧A/B/小分歧,自动联动趋势波段与复盘
下单监控增加 entry_model 下拉;平仓写入短标签并预填复盘。Binance/OKX 用趋势 profile,Gate 日内仍用手选 trade_style。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,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}
|
||||
|
||||
@@ -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 %}">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user