"""趋势户开仓类型:反转·启动 / 顺势·大分歧 / 波段·小分歧;日内户单独 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_CATEGORY_REVERSAL = "reversal" ENTRY_CATEGORY_TREND = "trend" ENTRY_CATEGORY_SWING = "swing" ENTRY_MODEL_LAUNCH_A = "launch_a" ENTRY_MODEL_LAUNCH_B = "launch_b" ENTRY_MODEL_BIG_DIV_A = "big_div_a" ENTRY_MODEL_BIG_DIV_B = "big_div_b" ENTRY_MODEL_SMALL_DIV = "small_div" ENTRY_CATEGORY_INTRADAY = "intraday" ENTRY_MODEL_LIQUIDITY_FALSE_BREAK = "liquidity_false_break" ENTRY_MODEL_STRUCTURE_BREAKOUT = "structure_breakout" VALID_ENTRY_MODEL_CODES = frozenset( { ENTRY_MODEL_LAUNCH_A, ENTRY_MODEL_LAUNCH_B, ENTRY_MODEL_BIG_DIV_A, ENTRY_MODEL_BIG_DIV_B, ENTRY_MODEL_SMALL_DIV, } ) INTRADAY_ENTRY_MODEL_CODES = frozenset( { ENTRY_MODEL_LIQUIDITY_FALSE_BREAK, ENTRY_MODEL_STRUCTURE_BREAKOUT, } ) ALL_ENTRY_MODEL_CODES = VALID_ENTRY_MODEL_CODES | INTRADAY_ENTRY_MODEL_CODES ENTRY_CATEGORY_LABELS: dict[str, str] = { ENTRY_CATEGORY_REVERSAL: "反转", ENTRY_CATEGORY_TREND: "顺势", ENTRY_CATEGORY_SWING: "波段", } TREND_DIV_ENTRY_REASON_LABELS: Tuple[str, ...] = ( "启动A", "启动B", "大分歧A", "大分歧B", "小分歧", ) INTRADAY_LEGACY_TREND_ENTRY_REASONS: Tuple[str, ...] = ( "趋势多头:4h大结构突破前进场,确认条件:三次探顶,5m收敛不创新低", "趋势空头:4h大结构突破前进场,确认条件:三次探底,5m收敛不创新高", "趋势多头:小分歧低吸入场(左侧),确认条件:二次探底", "趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶", "波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20", ) # code, label, category, trade_style, help _ENTRY_SPECS: Tuple[Tuple[str, str, str, str, str], ...] = ( ( ENTRY_MODEL_LAUNCH_A, "启动A", ENTRY_CATEGORY_REVERSAL, "trend", "反转链结构内:摸参考极值前小收敛,或 B 失败后 5m N 字试仓(不单列)", ), ( ENTRY_MODEL_LAUNCH_B, "启动B", ENTRY_CATEGORY_REVERSAL, "trend", "第二次到参考极值附近、无小收敛时的实体突破", ), ( ENTRY_MODEL_BIG_DIV_A, "大分歧A", ENTRY_CATEGORY_TREND, "trend", "主升已确立:突破前收敛,不创新低企稳(空:不创新高)", ), ( ENTRY_MODEL_BIG_DIV_B, "大分歧B", ENTRY_CATEGORY_TREND, "trend", "主升已确立:结构实体突破确认后入场", ), ( ENTRY_MODEL_SMALL_DIV, "小分歧", ENTRY_CATEGORY_SWING, "swing", "主升已确立:二次探底 N 字或 5m 三均线重新多头(空:二次探顶 / 空头均线)", ), ) _INTRADAY_ENTRY_SPECS: Tuple[Tuple[str, str, str, str, str], ...] = ( ( ENTRY_MODEL_LIQUIDITY_FALSE_BREAK, "假破", ENTRY_CATEGORY_INTRADAY, "trend", "流动性扫单 → 假突破验证 → 5m N 字 → 15m 顶/底分型", ), ( ENTRY_MODEL_STRUCTURE_BREAKOUT, "结构突破", ENTRY_CATEGORY_INTRADAY, "trend", "15m 结构有效突破(收盘确认)", ), ) _CODE_TO_LABEL = {code: label for code, label, _, _, _ in _ENTRY_SPECS} _CODE_TO_LABEL.update({code: label for code, label, _, _, _ in _INTRADAY_ENTRY_SPECS}) _CODE_TO_STYLE = {code: style for code, _, _, style, _ in _ENTRY_SPECS} _CODE_TO_STYLE.update({code: style for code, _, _, style, _ in _INTRADAY_ENTRY_SPECS}) _CODE_TO_CATEGORY = {code: cat for code, _, cat, _, _ in _ENTRY_SPECS} _CODE_TO_CATEGORY.update({code: cat for code, _, cat, _, _ in _INTRADAY_ENTRY_SPECS}) _LABEL_TO_CODE = {label: code for code, label, _, _, _ in _ENTRY_SPECS} _LABEL_TO_CODE.update({label: code for code, label, _, _, _ in _INTRADAY_ENTRY_SPECS}) _CODE_TO_HELP = {code: help for code, _, _, _, help in _ENTRY_SPECS} _CODE_TO_HELP.update({code: help for code, _, _, _, help in _INTRADAY_ENTRY_SPECS}) _CATEGORY_ORDER: Tuple[str, ...] = ( ENTRY_CATEGORY_REVERSAL, ENTRY_CATEGORY_TREND, ENTRY_CATEGORY_SWING, ) _INTRADAY_WHITELIST = frozenset({"BTC", "ETH"}) @dataclass(frozen=True) class EntryModelOption: code: str label: str category: 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, category=cat, trade_style=style, help=help) for code, label, cat, style, help in _ENTRY_SPECS ) def intraday_entry_model_options() -> Tuple[EntryModelOption, ...]: return tuple( EntryModelOption(code=code, label=label, category=cat, trade_style=style, help=help) for code, label, cat, style, help in _INTRADAY_ENTRY_SPECS ) def entry_model_categories() -> list[dict[str, Any]]: """两级 UI:反转 / 顺势 / 波段 → 子选项。""" opts = entry_model_options() out: list[dict[str, Any]] = [] for cat_key in _CATEGORY_ORDER: children = [ { "code": o.code, "label": o.label, "trade_style": o.trade_style, "help": o.help, } for o in opts if o.category == cat_key ] if not children: continue out.append( { "key": cat_key, "label": ENTRY_CATEGORY_LABELS.get(cat_key, cat_key), "options": children, } ) return out def entry_model_category(code: Optional[str]) -> str: c = normalize_entry_model_code(code) return _CODE_TO_CATEGORY.get(c, "") def normalize_entry_model_code(raw: Optional[str]) -> str: v = (raw or "").strip().lower() if v in ALL_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 entry_category_display_prefix(category: str) -> str: """两级展示用的一级前缀:反转 / 顺势 / 波段单(含日内)。""" cat = (category or "").strip() if cat in (ENTRY_CATEGORY_SWING, ENTRY_CATEGORY_INTRADAY): return "波段单" return ENTRY_CATEGORY_LABELS.get(cat, "") def entry_model_display_label(code: Optional[str]) -> str: """两级展示:反转/启动A、顺势/大分歧A、波段单/小分歧、波段单/假破。""" c = normalize_entry_model_code(code) if not c: return "" label = entry_model_label(c) if not label: return "" prefix = entry_category_display_prefix(entry_model_category(c)) if prefix: return f"{prefix}/{label}" return label def format_entry_type_display( text: Optional[str] = None, *, entry_model: Optional[str] = None, trade_style: Optional[str] = None, ) -> str: """交易记录/持仓展示:已知 entry_model 或短标签 → 两级文案。""" if entry_model: disp = entry_model_display_label(entry_model) if disp: return disp raw = (text or "").strip() if not raw: ts = (trade_style or "").strip().lower() if ts in ("trend", "swing"): return trade_style_label_zh(ts) return "" if "/" in raw: return raw code = normalize_entry_model_code(raw) if code: disp = entry_model_display_label(code) if disp: return disp return raw 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): entry_model = normalize_entry_model_code(form.get("entry_model")) if entry_model in INTRADAY_ENTRY_MODEL_CODES: return "trend", entry_model, None raw_style = (form.get("trade_style") or "").strip().lower() if raw_style in ("trend", "swing"): return raw_style, None, None return "", None, "请选择开仓类型(假破 / 结构突破)" entry_model = normalize_entry_model_code(form.get("entry_model")) if not entry_model: return "", None, "请选择开仓类型(反转 / 顺势 / 波段)" 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, trade_style: 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_display_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 ts = (trade_style or "").strip().lower() if ts in ("trend", "swing"): return trade_style_label_zh(ts) return "" def resolve_effective_trade_entry_reason( *, reviewed_entry_reason: Optional[str] = None, entry_reason: Optional[str] = None, entry_model: Optional[str] = None, key_signal_type: Optional[str] = None, monitor_type: Optional[str] = None, trade_style: Optional[str] = None, entry_reason_from_key_signal=None, entry_reason_for_monitor_type=None, ) -> str: """交易记录展示/导出用:复盘优先,再回落 entry_model / 关键位 / 策略 / trade_style。""" for raw in (reviewed_entry_reason, entry_reason): er = (raw or "").strip() if er: return format_entry_type_display( er, entry_model=entry_model, trade_style=trade_style, ) return format_entry_type_display( resolve_trade_record_entry_reason( entry_model=entry_model, key_signal_type=key_signal_type, monitor_type=monitor_type, trade_style=trade_style, entry_reason_from_key_signal=entry_reason_from_key_signal, entry_reason_for_monitor_type=entry_reason_for_monitor_type, ), entry_model=entry_model, trade_style=trade_style, ) 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_display_label(code) cat = entry_model_category(code) if cat: item["entry_model_category"] = cat item["entry_model_category_label"] = ENTRY_CATEGORY_LABELS.get(cat, "") else: item.setdefault("entry_model_label", "") return item def open_position_button_label(policy: TradePolicy, sizing_mode: str) -> str: from lib.trade.position_sizing_lib import mode_label_zh mode_txt = mode_label_zh(sizing_mode) if is_intraday_trading_profile(policy): return f"开仓(日内·{mode_txt})" return f"开仓({mode_txt})" def order_entry_template_context(policy: TradePolicy) -> dict: profile = order_entry_profile(policy) opts = entry_model_options() intraday_opts = intraday_entry_model_options() return { "order_entry_profile": profile, "intraday_discipline": profile == PROFILE_INTRADAY, "intraday_entry_model_options": [ { "code": o.code, "label": o.label, "trade_style": o.trade_style, "help": o.help, } for o in intraday_opts ], "entry_model_options": [ { "code": o.code, "label": o.label, "category": o.category, "trade_style": o.trade_style, "help": o.help, } for o in opts ], "entry_model_categories": entry_model_categories(), "entry_model_trade_style_map": {o.code: o.trade_style for o in opts}, "entry_model_code_to_category": {o.code: o.category for o in opts}, } def hub_meta_entry_context(policy: TradePolicy) -> dict: """供 /api/hub/meta:中控按 profile 隐藏平仓/委托等。""" profile = order_entry_profile(policy) return { "order_entry_profile": profile, "intraday_discipline": profile == PROFILE_INTRADAY, } 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