Initialize crypto_monitor_user (user edition) from monitor codebase.
Retarget git remote, install path, and deploy docs from crypto_monitor to crypto_monitor_user. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
"""趋势户开仓类型:反转·启动 / 顺势·大分歧 / 波段·小分歧;日内户单独 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: "波段",
|
||||
}
|
||||
|
||||
TRADE_STYLE_FALLBACK_ENTRY_REASONS: Tuple[str, ...] = ("趋势单", "波段单")
|
||||
|
||||
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_div_entry_reason_display_options() -> Tuple[str, ...]:
|
||||
return tuple(entry_model_display_label(code) for code, _, _, _, _ in _ENTRY_SPECS)
|
||||
|
||||
|
||||
def intraday_entry_reason_display_options() -> Tuple[str, ...]:
|
||||
return tuple(entry_model_display_label(code) for code, _, _, _, _ in _INTRADAY_ENTRY_SPECS)
|
||||
|
||||
|
||||
def normalize_review_entry_reason(raw: Optional[str], allowed: Sequence[str]) -> str:
|
||||
"""复盘/核对开仓类型:允许两级展示名,兼容旧短标签."""
|
||||
s = (raw or "").strip()
|
||||
if not s:
|
||||
return ""
|
||||
allowed_set = frozenset(allowed)
|
||||
if s in allowed_set:
|
||||
return s
|
||||
disp = format_entry_type_display(s)
|
||||
if disp in allowed_set:
|
||||
return disp
|
||||
code = normalize_entry_model_code(s)
|
||||
if code:
|
||||
disp2 = entry_model_display_label(code)
|
||||
if disp2 in allowed_set:
|
||||
return disp2
|
||||
return ""
|
||||
|
||||
|
||||
# 日内复盘开仓类型:手动假破/结构突破 + 自动触价(与 KEY_ENTRY_REASON_TRIGGER_OPTIONS 一致)
|
||||
_INTRADAY_JOURNAL_KEY_ENTRY_REASONS: Tuple[str, ...] = (
|
||||
"关键位回调触价开仓",
|
||||
"关键位突破触价开仓",
|
||||
)
|
||||
|
||||
|
||||
def trend_manual_entry_reason_count(policy: TradePolicy) -> int:
|
||||
if is_intraday_trading_profile(policy):
|
||||
return len(intraday_entry_reason_display_options())
|
||||
return len(trend_div_entry_reason_display_options())
|
||||
|
||||
|
||||
def build_journal_entry_reason_options() -> Tuple[str, ...]:
|
||||
"""复盘开仓类型:仅 entry model,不含 trade_style 兜底与策略下单类型."""
|
||||
return trend_div_entry_reason_display_options()
|
||||
|
||||
|
||||
def build_trend_div_entry_reason_options(
|
||||
strategy_options: Sequence[str],
|
||||
) -> Tuple[str, ...]:
|
||||
return trend_div_entry_reason_display_options() + TRADE_STYLE_FALLBACK_ENTRY_REASONS + tuple(strategy_options)
|
||||
|
||||
|
||||
def build_intraday_entry_reason_options(
|
||||
key_options: Sequence[str],
|
||||
strategy_options: Sequence[str],
|
||||
) -> Tuple[str, ...]:
|
||||
del strategy_options # 日内户无趋势回调/顺势加仓
|
||||
del key_options
|
||||
return intraday_entry_reason_display_options() + _INTRADAY_JOURNAL_KEY_ENTRY_REASONS
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# 兼容旧引用:现为两级展示文案
|
||||
TREND_DIV_ENTRY_REASON_LABELS = trend_div_entry_reason_display_options()
|
||||
Reference in New Issue
Block a user