1
This commit is contained in:
parent
b65fc75893
commit
1e94714234
25
AGENTS.md
25
AGENTS.md
@ -75,7 +75,7 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组
|
|||||||
- `app/cli.py`
|
- `app/cli.py`
|
||||||
- 统一命令入口:`screener`, `confirm`, `tracker`, `paper-trader`, `price-streamer`, `market`, `review`, `event`, `sentiment`, `onchain`, `llm-insights`。
|
- 统一命令入口:`screener`, `confirm`, `tracker`, `paper-trader`, `price-streamer`, `market`, `review`, `event`, `sentiment`, `onchain`, `llm-insights`。
|
||||||
|
|
||||||
## 4. 代码主线
|
## 4. 代码结构
|
||||||
|
|
||||||
### 4.1 推荐系统业务闭环
|
### 4.1 推荐系统业务闭环
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组
|
|||||||
3. `app/services/altcoin_confirm.py`
|
3. `app/services/altcoin_confirm.py`
|
||||||
负责确认,判断候选是否形成更可执行的机会,并生成入场计划、上下文和推送候选。
|
负责确认,判断候选是否形成更可执行的机会,并生成入场计划、上下文和推送候选。
|
||||||
4. `app/services/event_driven_screener.py`
|
4. `app/services/event_driven_screener.py`
|
||||||
负责事件/舆情驱动的快速触发检查,是技术筛选主链路的补充入口。
|
负责事件/舆情驱动的快速触发检查,是多策略发现层的补充入口。
|
||||||
5. `app/services/price_streamer.py`
|
5. `app/services/price_streamer.py`
|
||||||
负责实时价格缓存,不等同于完整推荐状态跟踪。
|
负责实时价格缓存,不等同于完整推荐状态跟踪。
|
||||||
6. `app/services/price_tracker.py`
|
6. `app/services/price_tracker.py`
|
||||||
@ -104,7 +104,7 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组
|
|||||||
|
|
||||||
- 核心认知:因子不等于策略。一个因子可以是先决条件、触发、确认、入场、风控或归因,但不能因为单个因子表现好就直接升级成完整策略。
|
- 核心认知:因子不等于策略。一个因子可以是先决条件、触发、确认、入场、风控或归因,但不能因为单个因子表现好就直接升级成完整策略。
|
||||||
- 完整策略必须至少包含:适用市场环境、交易宇宙、先决条件、核心触发、辅助确认、入场规则、止盈止损、失效条件、仓位/杠杆约束和独立复盘口径。
|
- 完整策略必须至少包含:适用市场环境、交易宇宙、先决条件、核心触发、辅助确认、入场规则、止盈止损、失效条件、仓位/杠杆约束和独立复盘口径。
|
||||||
- 多策略链路必须保持独立:`main_composite_v1`、`box_retest_1h_v1`、`box_retest_4h_v1` 等策略可以共享行情、账户级风控和执行框架,但不能共享同一套入场门槛、RR、挂单距离和 paper trading 执行门禁。
|
- 多策略链路必须保持独立:`main_composite_v1`、`box_retest_1h_v1`、`box_retest_4h_v1`、`volume_ignition_1h_v1`、`compression_breakout_4h_v1`、`intraday_momentum_15m_v1` 等策略可以共享行情、账户级风控和执行框架,但不能共享同一套入场门槛、RR、挂单距离和 paper trading 执行门禁。
|
||||||
- 策略级配置入口在 `app/core/strategy_registry.py`,`StrategyDefinition.entry_gate_config` 控制确认/跟踪/展示派生的买点质量闸门,`StrategyDefinition.paper_config` 控制该策略进入 paper trading 的入场、挂单和动态杠杆门槛。
|
- 策略级配置入口在 `app/core/strategy_registry.py`,`StrategyDefinition.entry_gate_config` 控制确认/跟踪/展示派生的买点质量闸门,`StrategyDefinition.paper_config` 控制该策略进入 paper trading 的入场、挂单和动态杠杆门槛。
|
||||||
- 新增策略时必须注册稳定 `strategy_code`,并明确自己的 `entry_gate_config` / `paper_config`。不要把新策略的特殊门槛写进全局 `paper_trading_config()` 或 `DEFAULT_ENTRY_GATE`,否则会影响其他策略的信号生成和成交样本。
|
- 新增策略时必须注册稳定 `strategy_code`,并明确自己的 `entry_gate_config` / `paper_config`。不要把新策略的特殊门槛写进全局 `paper_trading_config()` 或 `DEFAULT_ENTRY_GATE`,否则会影响其他策略的信号生成和成交样本。
|
||||||
- `apply_entry_quality_gate()` 必须传入或从 `entry_plan.strategy_code` 派生策略身份;`paper_trader.py` 中开仓、挂单、挂单成交和挂单维护应通过策略级配置合并后的参数执行。
|
- `apply_entry_quality_gate()` 必须传入或从 `entry_plan.strategy_code` 派生策略身份;`paper_trader.py` 中开仓、挂单、挂单成交和挂单维护应通过策略级配置合并后的参数执行。
|
||||||
@ -126,8 +126,8 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组
|
|||||||
- 多策略改造计划记录在 `docs/MULTI_STRATEGY_ARCHITECTURE.md`。后续做策略级改造前必须先阅读并更新该文档。
|
- 多策略改造计划记录在 `docs/MULTI_STRATEGY_ARCHITECTURE.md`。后续做策略级改造前必须先阅读并更新该文档。
|
||||||
- 目标架构是:统一交易宇宙 -> 多个独立策略并行扫描 -> 标准策略信号 -> 冲突/重复仲裁 -> 推荐/观察/挂单 -> paper trading 保留策略血缘 -> 按策略独立复盘。
|
- 目标架构是:统一交易宇宙 -> 多个独立策略并行扫描 -> 标准策略信号 -> 冲突/重复仲裁 -> 推荐/观察/挂单 -> paper trading 保留策略血缘 -> 按策略独立复盘。
|
||||||
- `strategy_version` 只表示版本,不应替代策略身份;后续推荐、挂单和交易账本都应补充 `strategy_code`、`strategy_signal_id`、`strategy_snapshot_json` 和 `factor_roles_json`。
|
- `strategy_version` 只表示版本,不应替代策略身份;后续推荐、挂单和交易账本都应补充 `strategy_code`、`strategy_signal_id`、`strategy_snapshot_json` 和 `factor_roles_json`。
|
||||||
- 现有综合确认链路在迁移期应标记为 `main_composite_v1`,避免无策略来源的推荐继续进入 paper trading。
|
- 现有综合确认策略在迁移期标记为 `main_composite_v1`,它只是平等策略之一,用于避免无策略来源的推荐继续进入 paper trading。
|
||||||
- 第一个建议拆出的独立策略是 `box_retest_4h_v1`:核心触发来自 `box_breakout_pullback_4h`,但策略成立还需要市场环境、交易量、回踩距离、15m/1H 承接、盈亏比、失效条件和账户风控。
|
- 当前已拆出的独立策略包括:`box_retest_1h_v1` / `box_retest_4h_v1` 箱体突破回踩、`volume_ignition_1h_v1` 1H放量突破启动、`compression_breakout_4h_v1` 4H压缩蓄力突破、`intraday_momentum_15m_v1` 15m日内动量延续。它们可以共享交易宇宙和行情数据,但必须保留各自的触发、入场、失效和 paper trading 门禁。
|
||||||
- 新增策略必须先 observe-only 或 paper-only 积累样本,再进入灰度/发布;不能因为某个因子短期表现好就直接同步真实交易。
|
- 新增策略必须先 observe-only 或 paper-only 积累样本,再进入灰度/发布;不能因为某个因子短期表现好就直接同步真实交易。
|
||||||
|
|
||||||
### 4.1.3 链上数据源
|
### 4.1.3 链上数据源
|
||||||
@ -180,7 +180,7 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组
|
|||||||
- 市场环境识别中心,第一版基于市场快照、BTC/ETH 涨跌、山寨涨跌广度、强势/大跌数量和 funding 热度识别 `risk_off`、`btc_main_uptrend`、`altcoin_rotation`、`sideways_chop`、`meme_frenzy`、`unknown`。
|
- 市场环境识别中心,第一版基于市场快照、BTC/ETH 涨跌、山寨涨跌广度、强势/大跌数量和 funding 热度识别 `risk_off`、`btc_main_uptrend`、`altcoin_rotation`、`sideways_chop`、`meme_frenzy`、`unknown`。
|
||||||
- `app/core/global_risk.py`
|
- `app/core/global_risk.py`
|
||||||
- paper trading 全局风控门禁。单币机会进入开仓或挂单成交前,需要先检查市场环境和账户风险;critical 禁止新开仓,high 只允许高质量机会。
|
- paper trading 全局风控门禁。单币机会进入开仓或挂单成交前,需要先检查市场环境和账户风险;critical 禁止新开仓,high 只允许高质量机会。
|
||||||
- 多策略基础设施当前内置 `main_composite_v1`、`box_retest_4h_v1`、`box_retest_1h_v1`。`box_breakout_pullback_4h` / `box_breakout_pullback_1h` 只是触发因子,只有和入场确认、风控、失效条件组成完整剧本后,才作为对应策略信号写入 `strategy_signals`。
|
- 多策略基础设施当前内置 `main_composite_v1`、`box_retest_4h_v1`、`box_retest_1h_v1`、`volume_ignition_1h_v1`、`compression_breakout_4h_v1`、`intraday_momentum_15m_v1`。`box_breakout_pullback_4h` / `box_breakout_pullback_1h`、`vp_fly_1h_current`、`short_tf_15m_ignition` 等只是因子,只有和入场确认、风控、失效条件组成完整剧本后,才作为对应策略信号写入 `strategy_signals`。
|
||||||
- 确认层也会应用同一市场风控语义:`risk_level=critical` 且 `position_multiplier=0` 时,强势发现仍可记录为观察,但不能输出 `buy_now` 或新挂单动作;已有活跃可交易推荐会被降级为观察并写入 `market_risk_gate`。
|
- 确认层也会应用同一市场风控语义:`risk_level=critical` 且 `position_multiplier=0` 时,强势发现仍可记录为观察,但不能输出 `buy_now` 或新挂单动作;已有活跃可交易推荐会被降级为观察并写入 `market_risk_gate`。
|
||||||
|
|
||||||
## 5. 数据与状态中心
|
## 5. 数据与状态中心
|
||||||
@ -205,7 +205,7 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组
|
|||||||
- `app/db/recommendation_queries.py`
|
- `app/db/recommendation_queries.py`
|
||||||
- 推荐热路径查询、active/deduped 查询;不应反向依赖 `altcoin_db.py`。
|
- 推荐热路径查询、active/deduped 查询;不应反向依赖 `altcoin_db.py`。
|
||||||
- `app/db/push_queries.py`
|
- `app/db/push_queries.py`
|
||||||
- 推送冷却去重、推送日志、推送前单条推荐读取;推送层只能消费这里派生后的主链路口径。
|
- 推送冷却去重、推送日志、推送前单条推荐读取;推送层只能消费这里派生后的统一状态口径。
|
||||||
- `app/db/tracking_queries.py`
|
- `app/db/tracking_queries.py`
|
||||||
- 最新价格缓存、推荐跟踪价格/PnL 写入、入场时点更新。
|
- 最新价格缓存、推荐跟踪价格/PnL 写入、入场时点更新。
|
||||||
- `app/db/cron_queries.py`
|
- `app/db/cron_queries.py`
|
||||||
@ -296,7 +296,7 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组
|
|||||||
- `/docker`
|
- `/docker`
|
||||||
- 容器入口与调度器
|
- 容器入口与调度器
|
||||||
- `/tools`
|
- `/tools`
|
||||||
- 非主链路工具脚本,如回测和输出摘要脚本
|
- 非运行服务工具脚本,如回测和输出摘要脚本
|
||||||
- `/templates`
|
- `/templates`
|
||||||
- 后端读取的 HTML 模板资源
|
- 后端读取的 HTML 模板资源
|
||||||
- `/docs`
|
- `/docs`
|
||||||
@ -423,7 +423,7 @@ docker compose run --rm alphax-web python scripts/postgres/validate_import.py --
|
|||||||
- schema 变化必须通过 `app/db/migrations/*.sql`。
|
- schema 变化必须通过 `app/db/migrations/*.sql`。
|
||||||
- 查询最新运行状态优先看 PostgreSQL 表,而不是历史文件。
|
- 查询最新运行状态优先看 PostgreSQL 表,而不是历史文件。
|
||||||
- Docker 容器内运行和宿主机运行可能使用不同连接地址,排查时先确认 `DATABASE_URL`。
|
- Docker 容器内运行和宿主机运行可能使用不同连接地址,排查时先确认 `DATABASE_URL`。
|
||||||
- 调度器并发运行时要检查 lock group,避免多个任务同时写推荐主链路。
|
- 调度器并发运行时要检查 lock group,避免多个任务同时写推荐状态机和策略交易账本。
|
||||||
|
|
||||||
### 9.3 状态机不要各写各的
|
### 9.3 状态机不要各写各的
|
||||||
|
|
||||||
@ -448,7 +448,7 @@ docker compose run --rm alphax-web python scripts/postgres/validate_import.py --
|
|||||||
|
|
||||||
### 9.4 推荐链路当前特别注意点
|
### 9.4 推荐链路当前特别注意点
|
||||||
|
|
||||||
当前主链路已经能持续产生筛选和确认样本,但后半段仍需要重点盯住:
|
当前多策略发现与确认链路已经能持续产生筛选和确认样本,但后半段仍需要重点盯住:
|
||||||
|
|
||||||
- `latest_price_cache` 可能是实时的,但不代表 `recommendation.pnl_pct` 已更新。
|
- `latest_price_cache` 可能是实时的,但不代表 `recommendation.pnl_pct` 已更新。
|
||||||
- `price_tracking` 是跟踪流水,不应和 `latest_price_cache` 混为一谈。
|
- `price_tracking` 是跟踪流水,不应和 `latest_price_cache` 混为一谈。
|
||||||
@ -507,6 +507,11 @@ docker compose run --rm alphax-web python scripts/postgres/validate_import.py --
|
|||||||
4. 修改后至少补 1 个相关测试,最好补到最接近业务口径的那层。
|
4. 修改后至少补 1 个相关测试,最好补到最接近业务口径的那层。
|
||||||
5. 如果变更影响推荐状态或展示桶,务必同时检查 API、前端、推送、paper trading、历史统计五个面。
|
5. 如果变更影响推荐状态或展示桶,务必同时检查 API、前端、推送、paper trading、历史统计五个面。
|
||||||
6. 如果变更影响调度任务,务必检查 `scheduler_job_config`、`scheduler_runtime_status` 和最近 `cron_run_log`。
|
6. 如果变更影响调度任务,务必检查 `scheduler_job_config`、`scheduler_runtime_status` 和最近 `cron_run_log`。
|
||||||
|
7. 不要把“底座已建好”当成“功能已完成”。涉及策略、复盘、交易、UI 的任务必须形成端到端闭环:数据写入、状态流转、API、页面展示、操作入口、测试和文档至少各检查一次。
|
||||||
|
8. 少问用户开放问题。除非存在资金安全、真实下单、删除数据、不可逆迁移等高风险选择,否则应基于当前代码和产品目标直接推进,并在结果里说明假设和取舍。
|
||||||
|
9. 如果发现自己只完成了半成品,不能用“后续可以”收尾;应继续把缺口补完,或明确标记为阻塞并说明为什么当前环境无法完成。
|
||||||
|
10. 多策略相关改动必须同时回答三个问题:每个策略是否独立产生信号、是否能独立进入/退出策略交易、是否能独立复盘评价并给出保留/灰度/暂停建议。
|
||||||
|
11. UI 层必须与后端能力对等。后端支持多策略筛选、评价或状态时,页面也要提供可理解的筛选、展示和操作入口,不能只返回隐藏字段。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -425,7 +425,7 @@ def discover_new_rules(pattern_summary, all_features, sector_alignments, signifi
|
|||||||
if not rule:
|
if not rule:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 新体系:逆向分析只生成候选规则,不直接写 learned_rules,避免涨幅榜小样本污染主策略。
|
# 新体系:逆向分析只生成候选规则,不直接写 learned_rules,避免涨幅榜小样本污染已发布策略。
|
||||||
rule["candidate_id"] = upsert_strategy_rule_candidate(
|
rule["candidate_id"] = upsert_strategy_rule_candidate(
|
||||||
source="reverse_analysis",
|
source="reverse_analysis",
|
||||||
rule_type=rule.get("type", "bonus"),
|
rule_type=rule.get("type", "bonus"),
|
||||||
|
|||||||
@ -425,7 +425,7 @@ def promote_candidate_rule_to_learned_rule(candidate, release_version=""):
|
|||||||
"""把通过发布门槛的候选规则正式写入 learned_rules。
|
"""把通过发布门槛的候选规则正式写入 learned_rules。
|
||||||
|
|
||||||
候选规则来自 DB strategy_rule_candidate;只有发布闸门通过时才调用,
|
候选规则来自 DB strategy_rule_candidate;只有发布闸门通过时才调用,
|
||||||
避免日常研究直接污染主策略。
|
避免日常研究直接污染已发布策略。
|
||||||
"""
|
"""
|
||||||
desc = (candidate.get("rule_description") or "").strip()
|
desc = (candidate.get("rule_description") or "").strip()
|
||||||
if not desc:
|
if not desc:
|
||||||
|
|||||||
@ -8,6 +8,9 @@ from dataclasses import dataclass, field
|
|||||||
MAIN_COMPOSITE_STRATEGY = "main_composite_v1"
|
MAIN_COMPOSITE_STRATEGY = "main_composite_v1"
|
||||||
BOX_RETEST_1H_STRATEGY = "box_retest_1h_v1"
|
BOX_RETEST_1H_STRATEGY = "box_retest_1h_v1"
|
||||||
BOX_RETEST_4H_STRATEGY = "box_retest_4h_v1"
|
BOX_RETEST_4H_STRATEGY = "box_retest_4h_v1"
|
||||||
|
VOLUME_IGNITION_1H_STRATEGY = "volume_ignition_1h_v1"
|
||||||
|
COMPRESSION_BREAKOUT_4H_STRATEGY = "compression_breakout_4h_v1"
|
||||||
|
INTRADAY_MOMENTUM_15M_STRATEGY = "intraday_momentum_15m_v1"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -24,8 +27,8 @@ class StrategyDefinition:
|
|||||||
STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
|
STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
|
||||||
MAIN_COMPOSITE_STRATEGY: StrategyDefinition(
|
MAIN_COMPOSITE_STRATEGY: StrategyDefinition(
|
||||||
strategy_code=MAIN_COMPOSITE_STRATEGY,
|
strategy_code=MAIN_COMPOSITE_STRATEGY,
|
||||||
strategy_name="综合确认主链路",
|
strategy_name="综合确认策略",
|
||||||
description="迁移期兼容主链路,承载现有综合筛选与确认逻辑。",
|
description="迁移期兼容综合策略,承载现有综合筛选与确认逻辑;它与其他策略平等运行。",
|
||||||
mode="paper_enabled",
|
mode="paper_enabled",
|
||||||
),
|
),
|
||||||
BOX_RETEST_1H_STRATEGY: StrategyDefinition(
|
BOX_RETEST_1H_STRATEGY: StrategyDefinition(
|
||||||
@ -72,6 +75,70 @@ STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
|
|||||||
"dynamic_leverage_min": 3,
|
"dynamic_leverage_min": 3,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
VOLUME_IGNITION_1H_STRATEGY: StrategyDefinition(
|
||||||
|
strategy_code=VOLUME_IGNITION_1H_STRATEGY,
|
||||||
|
strategy_name="1H放量突破启动",
|
||||||
|
description="1H量价齐飞或连续放量后的启动策略,适合捕捉日内到3天的第一段加速。",
|
||||||
|
mode="paper_only",
|
||||||
|
entry_gate_config={
|
||||||
|
"min_entry_score_buy_now": 2,
|
||||||
|
"min_entry_score_wait_pullback": 1,
|
||||||
|
"min_rr_buy_now": 1.25,
|
||||||
|
"breakout_distance_wait_pct": 12,
|
||||||
|
"gain_24h_wait_pct": 10,
|
||||||
|
},
|
||||||
|
paper_config={
|
||||||
|
"entry_min_rr": 1.5,
|
||||||
|
"order_min_rr": 1.5,
|
||||||
|
"order_min_distance_to_entry_pct": 0,
|
||||||
|
"order_require_current_trigger": True,
|
||||||
|
"dynamic_leverage_enabled": True,
|
||||||
|
"dynamic_leverage_min": 3,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
COMPRESSION_BREAKOUT_4H_STRATEGY: StrategyDefinition(
|
||||||
|
strategy_code=COMPRESSION_BREAKOUT_4H_STRATEGY,
|
||||||
|
strategy_name="4H压缩蓄力突破",
|
||||||
|
description="4H静K蓄力、底部抬高或压缩放量后的突破策略,偏向捕捉1周以内主升前段。",
|
||||||
|
mode="paper_only",
|
||||||
|
entry_gate_config={
|
||||||
|
"min_entry_score_buy_now": 2,
|
||||||
|
"min_entry_score_wait_pullback": 0,
|
||||||
|
"min_rr_buy_now": 1.3,
|
||||||
|
"max_wait_pullback_deviation_pct": 18,
|
||||||
|
"breakout_distance_wait_pct": 18,
|
||||||
|
"gain_24h_wait_pct": 12,
|
||||||
|
},
|
||||||
|
paper_config={
|
||||||
|
"entry_min_rr": 1.6,
|
||||||
|
"order_min_rr": 1.6,
|
||||||
|
"order_min_distance_to_entry_pct": 0,
|
||||||
|
"order_require_current_trigger": False,
|
||||||
|
"dynamic_leverage_enabled": True,
|
||||||
|
"dynamic_leverage_min": 3,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
INTRADAY_MOMENTUM_15M_STRATEGY: StrategyDefinition(
|
||||||
|
strategy_code=INTRADAY_MOMENTUM_15M_STRATEGY,
|
||||||
|
strategy_name="15m日内动量延续",
|
||||||
|
description="短周期放量突破与1H背景共振的日内动量策略,只做当前触发,不做纯观察追高。",
|
||||||
|
mode="paper_only",
|
||||||
|
entry_gate_config={
|
||||||
|
"min_entry_score_buy_now": 3,
|
||||||
|
"min_entry_score_wait_pullback": 2,
|
||||||
|
"min_rr_buy_now": 1.4,
|
||||||
|
"breakout_distance_wait_pct": 8,
|
||||||
|
"gain_24h_wait_pct": 8,
|
||||||
|
},
|
||||||
|
paper_config={
|
||||||
|
"entry_min_rr": 1.6,
|
||||||
|
"order_min_rr": 1.6,
|
||||||
|
"order_min_distance_to_entry_pct": 0,
|
||||||
|
"order_require_current_trigger": True,
|
||||||
|
"dynamic_leverage_enabled": True,
|
||||||
|
"dynamic_leverage_min": 3,
|
||||||
|
},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -65,7 +65,7 @@ CREATE INDEX IF NOT EXISTS idx_paper_orders_strategy_code
|
|||||||
INSERT INTO strategy_catalog (
|
INSERT INTO strategy_catalog (
|
||||||
strategy_code, strategy_name, strategy_version, status, mode, description, config_json, created_at, updated_at
|
strategy_code, strategy_name, strategy_version, status, mode, description, config_json, created_at, updated_at
|
||||||
) VALUES
|
) VALUES
|
||||||
('main_composite_v1', '综合确认主链路', '', 'active', 'paper_enabled', '迁移期兼容主链路:统一承载旧的综合筛选与确认逻辑。', '{}', NOW()::TEXT, NOW()::TEXT),
|
('main_composite_v1', '综合确认策略', '', 'active', 'paper_enabled', '迁移期兼容综合策略:统一承载旧的综合筛选与确认逻辑,与其他策略平等运行。', '{}', NOW()::TEXT, NOW()::TEXT),
|
||||||
('box_retest_4h_v1', '4H箱体突破回踩', '', 'active', 'paper_only', '底部箱体突破后回踩箱体上沿或EMA承接的结构策略雏形。', '{}', NOW()::TEXT, NOW()::TEXT)
|
('box_retest_4h_v1', '4H箱体突破回踩', '', 'active', 'paper_only', '底部箱体突破后回踩箱体上沿或EMA承接的结构策略雏形。', '{}', NOW()::TEXT, NOW()::TEXT)
|
||||||
ON CONFLICT(strategy_code) DO UPDATE SET
|
ON CONFLICT(strategy_code) DO UPDATE SET
|
||||||
strategy_name=EXCLUDED.strategy_name,
|
strategy_name=EXCLUDED.strategy_name,
|
||||||
|
|||||||
12
app/db/migrations/0017_altcoin_parallel_strategies.sql
Normal file
12
app/db/migrations/0017_altcoin_parallel_strategies.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
INSERT INTO strategy_catalog (
|
||||||
|
strategy_code, strategy_name, strategy_version, status, mode, description, config_json, created_at, updated_at
|
||||||
|
) VALUES
|
||||||
|
('volume_ignition_1h_v1', '1H放量突破启动', '', 'active', 'paper_only', '1H量价齐飞或连续放量后的启动策略,捕捉日内到3天的第一段加速。', '{}', NOW()::TEXT, NOW()::TEXT),
|
||||||
|
('compression_breakout_4h_v1', '4H压缩蓄力突破', '', 'active', 'paper_only', '4H静K蓄力、底部抬高或压缩放量后的突破策略,偏向捕捉1周以内主升前段。', '{}', NOW()::TEXT, NOW()::TEXT),
|
||||||
|
('intraday_momentum_15m_v1', '15m日内动量延续', '', 'active', 'paper_only', '短周期放量突破与1H背景共振的日内动量策略,只做当前触发,不做纯观察追高。', '{}', NOW()::TEXT, NOW()::TEXT)
|
||||||
|
ON CONFLICT(strategy_code) DO UPDATE SET
|
||||||
|
strategy_name=EXCLUDED.strategy_name,
|
||||||
|
status=EXCLUDED.status,
|
||||||
|
mode=EXCLUDED.mode,
|
||||||
|
description=EXCLUDED.description,
|
||||||
|
updated_at=NOW()::TEXT;
|
||||||
5
app/db/migrations/0018_strategy_equal_status_labels.sql
Normal file
5
app/db/migrations/0018_strategy_equal_status_labels.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
UPDATE strategy_catalog
|
||||||
|
SET strategy_name='综合确认策略',
|
||||||
|
description='迁移期兼容综合策略:统一承载旧的综合筛选与确认逻辑,与其他策略平等运行。',
|
||||||
|
updated_at=NOW()::TEXT
|
||||||
|
WHERE strategy_code='main_composite_v1';
|
||||||
@ -1956,11 +1956,12 @@ def list_paper_orders(limit: int = 50, offset: int = 0, status: str = "", strate
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def list_paper_trade_events(limit: int = 80, offset: int = 0, symbol: str = "", event_type: str = "") -> dict:
|
def list_paper_trade_events(limit: int = 80, offset: int = 0, symbol: str = "", event_type: str = "", strategy_code: str = "") -> dict:
|
||||||
limit = max(1, min(_safe_int(limit, 80), 200))
|
limit = max(1, min(_safe_int(limit, 80), 200))
|
||||||
offset = max(0, _safe_int(offset, 0))
|
offset = max(0, _safe_int(offset, 0))
|
||||||
symbol = str(symbol or "").strip().upper()
|
symbol = str(symbol or "").strip().upper()
|
||||||
event_type = str(event_type or "").strip()
|
event_type = str(event_type or "").strip()
|
||||||
|
strategy_code = str(strategy_code or "").strip()
|
||||||
where = []
|
where = []
|
||||||
params = []
|
params = []
|
||||||
if symbol:
|
if symbol:
|
||||||
@ -1969,11 +1970,19 @@ def list_paper_trade_events(limit: int = 80, offset: int = 0, symbol: str = "",
|
|||||||
if event_type:
|
if event_type:
|
||||||
where.append("e.event_type=%s")
|
where.append("e.event_type=%s")
|
||||||
params.append(event_type)
|
params.append(event_type)
|
||||||
|
if strategy_code:
|
||||||
|
where.append("COALESCE(NULLIF(e.strategy_code, ''), t.strategy_code)=%s")
|
||||||
|
params.append(normalize_strategy_code(strategy_code))
|
||||||
where_sql = "WHERE " + " AND ".join(where) if where else ""
|
where_sql = "WHERE " + " AND ".join(where) if where else ""
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
try:
|
try:
|
||||||
total = conn.execute(
|
total = conn.execute(
|
||||||
f"SELECT COUNT(*) FROM paper_trade_events e {where_sql}",
|
f"""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM paper_trade_events e
|
||||||
|
LEFT JOIN paper_trades t ON t.id = e.trade_id
|
||||||
|
{where_sql}
|
||||||
|
""",
|
||||||
tuple(params),
|
tuple(params),
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
|
|||||||
@ -28,7 +28,7 @@ def derive_minimal_state_fields(status, action_status, entry_plan=None):
|
|||||||
action = normalize_action_status(action_status, status)
|
action = normalize_action_status(action_status, status)
|
||||||
if action == "可即刻买入":
|
if action == "可即刻买入":
|
||||||
execution_status = "buy_now"
|
execution_status = "buy_now"
|
||||||
reason = "主链路确认当前入场窗口"
|
reason = "策略确认当前入场窗口"
|
||||||
elif action == "等回踩":
|
elif action == "等回踩":
|
||||||
execution_status = "wait_pullback"
|
execution_status = "wait_pullback"
|
||||||
reason = "等待回踩触发,未触发前不计推荐收益"
|
reason = "等待回踩触发,未触发前不计推荐收益"
|
||||||
@ -169,7 +169,7 @@ def execution_fields_from_persisted_state(item, entry_plan=None):
|
|||||||
return "invalid", "🔴 已失效,勿追", reason
|
return "invalid", "🔴 已失效,勿追", reason
|
||||||
if execution_status == "buy_now":
|
if execution_status == "buy_now":
|
||||||
stop = str(entry_plan.get("stop_loss", "")) if entry_plan else ""
|
stop = str(entry_plan.get("stop_loss", "")) if entry_plan else ""
|
||||||
return "buy_now", "🟢 现在可买", "推荐时就是可即刻买入;主链路确认当前仍在入场窗口" + ((",风险边界 " + stop) if stop else "")
|
return "buy_now", "🟢 现在可买", "推荐时就是可即刻买入;策略确认当前仍在入场窗口" + ((",风险边界 " + stop) if stop else "")
|
||||||
if execution_status == "wait_pullback":
|
if execution_status == "wait_pullback":
|
||||||
gate = entry_plan.get("entry_quality_gate") or {}
|
gate = entry_plan.get("entry_quality_gate") or {}
|
||||||
if gate.get("reasons"):
|
if gate.get("reasons"):
|
||||||
|
|||||||
@ -13,7 +13,7 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
from app.db.paper_trading import get_paper_trading_summary
|
from app.db.paper_trading import get_paper_trading_summary
|
||||||
from app.db.schema import get_conn
|
from app.db.schema import get_conn
|
||||||
from app.db.strategy_insights import get_strategy_insights
|
from app.db.strategy_insights import get_strategy_evaluation, get_strategy_insights
|
||||||
|
|
||||||
|
|
||||||
def _safe_int(value, default=0):
|
def _safe_int(value, default=0):
|
||||||
@ -387,6 +387,7 @@ def get_review_center_dashboard(days=30):
|
|||||||
paper = _paper_review(conn, since, days)
|
paper = _paper_review(conn, since, days)
|
||||||
evidence = _evidence_review(conn, since)
|
evidence = _evidence_review(conn, since)
|
||||||
iteration = _iteration_review(conn, since)
|
iteration = _iteration_review(conn, since)
|
||||||
|
strategy_evaluation = get_strategy_evaluation(days=days)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@ -400,6 +401,7 @@ def get_review_center_dashboard(days=30):
|
|||||||
"策略迭代只发布经过样本约束和灰度闸门验证的规则。",
|
"策略迭代只发布经过样本约束和灰度闸门验证的规则。",
|
||||||
],
|
],
|
||||||
"opportunity": opportunity,
|
"opportunity": opportunity,
|
||||||
|
"strategy_evaluation": strategy_evaluation,
|
||||||
"paper_trading": paper,
|
"paper_trading": paper,
|
||||||
"evidence": evidence,
|
"evidence": evidence,
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
|
|||||||
@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from app.core.strategy_registry import normalize_strategy_code, strategy_label
|
from app.core.strategy_registry import normalize_strategy_code, registered_strategy_codes, strategy_definition, strategy_label
|
||||||
from app.db.schema import get_conn
|
from app.db.schema import get_conn
|
||||||
|
|
||||||
|
|
||||||
@ -31,6 +32,297 @@ def safe_dict_json(value):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_float(value, default=0.0):
|
||||||
|
try:
|
||||||
|
if value is None or value == "":
|
||||||
|
return default
|
||||||
|
return float(value)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_int(value, default=0):
|
||||||
|
try:
|
||||||
|
if value is None or value == "":
|
||||||
|
return default
|
||||||
|
return int(value)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _pct(part, total):
|
||||||
|
return round(float(part or 0) / float(total or 0) * 100, 2) if total else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_strategy_decision(metrics: dict) -> dict:
|
||||||
|
"""Turn strategy metrics into an explicit lifecycle recommendation.
|
||||||
|
|
||||||
|
This is advisory only. It does not mutate strategy configs; release/pause
|
||||||
|
should still go through the strategy iteration gate.
|
||||||
|
"""
|
||||||
|
signal_count = _safe_int(metrics.get("signal_count"))
|
||||||
|
opportunity_count = _safe_int(metrics.get("opportunity_count"))
|
||||||
|
trade_count = _safe_int(metrics.get("trade_count"))
|
||||||
|
closed_count = _safe_int(metrics.get("closed_trade_count"))
|
||||||
|
win_rate = _safe_float(metrics.get("win_rate_pct"))
|
||||||
|
avg_pnl = _safe_float(metrics.get("avg_realized_pnl_pct"))
|
||||||
|
realized = _safe_float(metrics.get("realized_pnl_usdt"))
|
||||||
|
worst = _safe_float(metrics.get("worst_pnl_pct"))
|
||||||
|
fill_rate = _safe_float(metrics.get("order_fill_rate_pct"))
|
||||||
|
trade_conversion = _safe_float(metrics.get("trade_conversion_pct"))
|
||||||
|
|
||||||
|
score = 50.0
|
||||||
|
score += min(signal_count, 40) * 0.25
|
||||||
|
score += min(opportunity_count, 40) * 0.2
|
||||||
|
score += min(trade_conversion, 40) * 0.25
|
||||||
|
score += (win_rate - 50) * 0.35 if closed_count else 0
|
||||||
|
score += max(-12, min(12, avg_pnl)) * 1.8
|
||||||
|
score += 6 if realized > 0 else (-6 if realized < 0 else 0)
|
||||||
|
score += 4 if fill_rate >= 40 else 0
|
||||||
|
score -= 8 if worst <= -8 else 0
|
||||||
|
score = round(max(0, min(100, score)), 1)
|
||||||
|
|
||||||
|
reasons = []
|
||||||
|
next_actions = []
|
||||||
|
decision = "observe"
|
||||||
|
decision_label = "继续观察"
|
||||||
|
|
||||||
|
if signal_count < 5 and opportunity_count < 5:
|
||||||
|
decision = "collect_samples"
|
||||||
|
decision_label = "样本不足"
|
||||||
|
reasons.append("信号和机会样本不足,暂不判断优劣")
|
||||||
|
next_actions.append("继续收集样本,不要直接调高权重")
|
||||||
|
elif trade_count == 0:
|
||||||
|
decision = "review_entry_gate"
|
||||||
|
decision_label = "检查入场闸门"
|
||||||
|
reasons.append("已有发现样本,但还没有进入策略交易")
|
||||||
|
next_actions.append("检查 RR、买点距离、全局风控和挂单成交条件")
|
||||||
|
elif closed_count < 5:
|
||||||
|
decision = "gray"
|
||||||
|
decision_label = "灰度观察"
|
||||||
|
reasons.append("已有交易样本,但平仓数量不足 5 笔")
|
||||||
|
next_actions.append("保持 paper-only,等更多已平仓样本")
|
||||||
|
elif win_rate >= 55 and avg_pnl > 0 and realized > 0:
|
||||||
|
decision = "promote"
|
||||||
|
decision_label = "优先保留"
|
||||||
|
reasons.append("胜率、平均收益和已实现收益同时为正")
|
||||||
|
next_actions.append("允许维持或小幅提升策略权重,但仍需观察回撤")
|
||||||
|
elif win_rate < 35 or avg_pnl <= -2 or realized < 0 and worst <= -6:
|
||||||
|
decision = "pause"
|
||||||
|
decision_label = "暂停/降权"
|
||||||
|
reasons.append("胜率或平均收益不达标,且存在较差回撤")
|
||||||
|
next_actions.append("暂停新增真实跟单,只保留观察或降低仓位")
|
||||||
|
elif fill_rate < 15 and opportunity_count >= 10:
|
||||||
|
decision = "tune_entry"
|
||||||
|
decision_label = "优化入场"
|
||||||
|
reasons.append("机会样本不少,但挂单成交或执行转化偏低")
|
||||||
|
next_actions.append("复查挂单价格、有效期、回踩距离和成交触发")
|
||||||
|
else:
|
||||||
|
decision = "keep"
|
||||||
|
decision_label = "保留运行"
|
||||||
|
reasons.append("当前表现没有触发暂停或升级条件")
|
||||||
|
next_actions.append("继续按当前门槛运行并积累样本")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"decision": decision,
|
||||||
|
"decision_label": decision_label,
|
||||||
|
"evaluation_score": score,
|
||||||
|
"reasons": reasons,
|
||||||
|
"next_actions": next_actions,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_strategy_evaluation(days: int = 30) -> dict:
|
||||||
|
days = max(1, min(_safe_int(days, 30), 365))
|
||||||
|
since = (datetime.now() - timedelta(days=days)).isoformat()
|
||||||
|
codes = registered_strategy_codes()
|
||||||
|
metrics = {}
|
||||||
|
for code in codes:
|
||||||
|
definition = strategy_definition(code)
|
||||||
|
metrics[code] = {
|
||||||
|
"strategy_code": code,
|
||||||
|
"strategy_name": definition.strategy_name,
|
||||||
|
"description": definition.description,
|
||||||
|
"mode": definition.mode,
|
||||||
|
"status": definition.status,
|
||||||
|
"signal_count": 0,
|
||||||
|
"candidate_signal_count": 0,
|
||||||
|
"observe_signal_count": 0,
|
||||||
|
"avg_signal_confidence": 0.0,
|
||||||
|
"opportunity_count": 0,
|
||||||
|
"actionable_count": 0,
|
||||||
|
"buy_now_count": 0,
|
||||||
|
"wait_pullback_count": 0,
|
||||||
|
"observe_count": 0,
|
||||||
|
"order_count": 0,
|
||||||
|
"filled_order_count": 0,
|
||||||
|
"canceled_order_count": 0,
|
||||||
|
"trade_count": 0,
|
||||||
|
"open_trade_count": 0,
|
||||||
|
"closed_trade_count": 0,
|
||||||
|
"win_count": 0,
|
||||||
|
"loss_count": 0,
|
||||||
|
"realized_pnl_usdt": 0.0,
|
||||||
|
"pnl_pct_values": [],
|
||||||
|
"best_pnl_pct": None,
|
||||||
|
"worst_pnl_pct": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def bucket(code):
|
||||||
|
normalized = normalize_strategy_code(code)
|
||||||
|
if normalized not in metrics:
|
||||||
|
definition = strategy_definition(normalized)
|
||||||
|
metrics[normalized] = {
|
||||||
|
"strategy_code": normalized,
|
||||||
|
"strategy_name": definition.strategy_name,
|
||||||
|
"description": definition.description,
|
||||||
|
"mode": definition.mode,
|
||||||
|
"status": definition.status,
|
||||||
|
"signal_count": 0,
|
||||||
|
"candidate_signal_count": 0,
|
||||||
|
"observe_signal_count": 0,
|
||||||
|
"avg_signal_confidence": 0.0,
|
||||||
|
"opportunity_count": 0,
|
||||||
|
"actionable_count": 0,
|
||||||
|
"buy_now_count": 0,
|
||||||
|
"wait_pullback_count": 0,
|
||||||
|
"observe_count": 0,
|
||||||
|
"order_count": 0,
|
||||||
|
"filled_order_count": 0,
|
||||||
|
"canceled_order_count": 0,
|
||||||
|
"trade_count": 0,
|
||||||
|
"open_trade_count": 0,
|
||||||
|
"closed_trade_count": 0,
|
||||||
|
"win_count": 0,
|
||||||
|
"loss_count": 0,
|
||||||
|
"realized_pnl_usdt": 0.0,
|
||||||
|
"pnl_pct_values": [],
|
||||||
|
"best_pnl_pct": None,
|
||||||
|
"worst_pnl_pct": None,
|
||||||
|
}
|
||||||
|
return metrics[normalized]
|
||||||
|
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
for row in conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT strategy_code,
|
||||||
|
COUNT(*) AS signal_count,
|
||||||
|
SUM(CASE WHEN signal_status='candidate' THEN 1 ELSE 0 END) AS candidate_count,
|
||||||
|
SUM(CASE WHEN signal_status='observe' THEN 1 ELSE 0 END) AS observe_count,
|
||||||
|
AVG(confidence) AS avg_confidence
|
||||||
|
FROM strategy_signals
|
||||||
|
WHERE created_at >= %s
|
||||||
|
GROUP BY strategy_code
|
||||||
|
""",
|
||||||
|
(since,),
|
||||||
|
).fetchall():
|
||||||
|
b = bucket(row.get("strategy_code"))
|
||||||
|
b["signal_count"] = _safe_int(row.get("signal_count"))
|
||||||
|
b["candidate_signal_count"] = _safe_int(row.get("candidate_count"))
|
||||||
|
b["observe_signal_count"] = _safe_int(row.get("observe_count"))
|
||||||
|
b["avg_signal_confidence"] = round(_safe_float(row.get("avg_confidence")), 2)
|
||||||
|
|
||||||
|
for row in conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT strategy_code,
|
||||||
|
COUNT(*) AS opportunity_count,
|
||||||
|
SUM(CASE WHEN execution_status IN ('buy_now','wait_pullback') THEN 1 ELSE 0 END) AS actionable_count,
|
||||||
|
SUM(CASE WHEN execution_status='buy_now' THEN 1 ELSE 0 END) AS buy_now_count,
|
||||||
|
SUM(CASE WHEN execution_status='wait_pullback' THEN 1 ELSE 0 END) AS wait_pullback_count,
|
||||||
|
SUM(CASE WHEN execution_status='observe' THEN 1 ELSE 0 END) AS observe_count
|
||||||
|
FROM recommendation
|
||||||
|
WHERE rec_time >= %s
|
||||||
|
GROUP BY strategy_code
|
||||||
|
""",
|
||||||
|
(since,),
|
||||||
|
).fetchall():
|
||||||
|
b = bucket(row.get("strategy_code"))
|
||||||
|
for key in ("opportunity_count", "actionable_count", "buy_now_count", "wait_pullback_count", "observe_count"):
|
||||||
|
b[key] = _safe_int(row.get(key))
|
||||||
|
|
||||||
|
for row in conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT strategy_code,
|
||||||
|
COUNT(*) AS order_count,
|
||||||
|
SUM(CASE WHEN status='filled' THEN 1 ELSE 0 END) AS filled_count,
|
||||||
|
SUM(CASE WHEN status IN ('canceled','rejected','expired') THEN 1 ELSE 0 END) AS canceled_count
|
||||||
|
FROM paper_orders
|
||||||
|
WHERE created_at >= %s
|
||||||
|
GROUP BY strategy_code
|
||||||
|
""",
|
||||||
|
(since,),
|
||||||
|
).fetchall():
|
||||||
|
b = bucket(row.get("strategy_code"))
|
||||||
|
b["order_count"] = _safe_int(row.get("order_count"))
|
||||||
|
b["filled_order_count"] = _safe_int(row.get("filled_count"))
|
||||||
|
b["canceled_order_count"] = _safe_int(row.get("canceled_count"))
|
||||||
|
|
||||||
|
for row in conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT strategy_code, status, realized_pnl_pct, realized_pnl_usdt, pnl_pct
|
||||||
|
FROM paper_trades
|
||||||
|
WHERE opened_at >= %s
|
||||||
|
""",
|
||||||
|
(since,),
|
||||||
|
).fetchall():
|
||||||
|
b = bucket(row.get("strategy_code"))
|
||||||
|
status = row.get("status") or ""
|
||||||
|
b["trade_count"] += 1
|
||||||
|
if status == "open":
|
||||||
|
b["open_trade_count"] += 1
|
||||||
|
if status == "closed":
|
||||||
|
b["closed_trade_count"] += 1
|
||||||
|
pnl_pct = _safe_float(row.get("realized_pnl_pct"))
|
||||||
|
pnl_usdt = _safe_float(row.get("realized_pnl_usdt"))
|
||||||
|
b["realized_pnl_usdt"] += pnl_usdt
|
||||||
|
b["pnl_pct_values"].append(pnl_pct)
|
||||||
|
if pnl_pct > 0:
|
||||||
|
b["win_count"] += 1
|
||||||
|
elif pnl_pct < 0:
|
||||||
|
b["loss_count"] += 1
|
||||||
|
b["best_pnl_pct"] = pnl_pct if b["best_pnl_pct"] is None else max(b["best_pnl_pct"], pnl_pct)
|
||||||
|
b["worst_pnl_pct"] = pnl_pct if b["worst_pnl_pct"] is None else min(b["worst_pnl_pct"], pnl_pct)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
strategies = []
|
||||||
|
for item in metrics.values():
|
||||||
|
values = item.pop("pnl_pct_values", [])
|
||||||
|
item["realized_pnl_usdt"] = round(item["realized_pnl_usdt"], 4)
|
||||||
|
item["avg_realized_pnl_pct"] = round(sum(values) / len(values), 4) if values else 0.0
|
||||||
|
item["win_rate_pct"] = _pct(item["win_count"], item["closed_trade_count"])
|
||||||
|
item["actionable_rate_pct"] = _pct(item["actionable_count"], item["opportunity_count"])
|
||||||
|
item["trade_conversion_pct"] = _pct(item["trade_count"], item["opportunity_count"])
|
||||||
|
item["order_fill_rate_pct"] = _pct(item["filled_order_count"], item["order_count"])
|
||||||
|
item["candidate_signal_rate_pct"] = _pct(item["candidate_signal_count"], item["signal_count"])
|
||||||
|
item["best_pnl_pct"] = item["best_pnl_pct"] if item["best_pnl_pct"] is not None else 0.0
|
||||||
|
item["worst_pnl_pct"] = item["worst_pnl_pct"] if item["worst_pnl_pct"] is not None else 0.0
|
||||||
|
item.update(evaluate_strategy_decision(item))
|
||||||
|
strategies.append(item)
|
||||||
|
|
||||||
|
strategies.sort(key=lambda x: (x["evaluation_score"], x["realized_pnl_usdt"], x["closed_trade_count"], x["signal_count"]), reverse=True)
|
||||||
|
decisions = {}
|
||||||
|
for item in strategies:
|
||||||
|
decisions[item["decision"]] = decisions.get(item["decision"], 0) + 1
|
||||||
|
return {
|
||||||
|
"definition": "策略评价按 strategy_code 独立统计发现、执行、成交、收益和回撤,并给出保留/灰度/暂停等建议;建议不直接改配置,仍需经过策略迭代闸门。",
|
||||||
|
"days": days,
|
||||||
|
"generated_at": datetime.now().isoformat(timespec="seconds"),
|
||||||
|
"summary": {
|
||||||
|
"strategy_count": len(strategies),
|
||||||
|
"active_signal_strategy_count": sum(1 for x in strategies if x["signal_count"] > 0),
|
||||||
|
"traded_strategy_count": sum(1 for x in strategies if x["trade_count"] > 0),
|
||||||
|
"promote_count": decisions.get("promote", 0),
|
||||||
|
"pause_count": decisions.get("pause", 0),
|
||||||
|
"gray_count": decisions.get("gray", 0),
|
||||||
|
"collect_samples_count": decisions.get("collect_samples", 0),
|
||||||
|
},
|
||||||
|
"decision_distribution": [{"name": k, "count": v} for k, v in sorted(decisions.items(), key=lambda x: (-x[1], x[0]))],
|
||||||
|
"strategies": strategies,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_strategy_insights():
|
def get_strategy_insights():
|
||||||
"""Strategy attribution based on opportunity and paper-trading conversion.
|
"""Strategy attribution based on opportunity and paper-trading conversion.
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,11 @@ from app.core.market_regime import classify_market_regime
|
|||||||
from app.db.onchain_db import get_onchain_factor_context
|
from app.db.onchain_db import get_onchain_factor_context
|
||||||
from app.db.strategy_signal_queries import insert_strategy_signal
|
from app.db.strategy_signal_queries import insert_strategy_signal
|
||||||
from app.services.market_overview import get_crypto_market_overview
|
from app.services.market_overview import get_crypto_market_overview
|
||||||
|
from app.strategies.altcoin_breakout import (
|
||||||
|
build_compression_breakout_4h_signal,
|
||||||
|
build_intraday_momentum_15m_signal,
|
||||||
|
build_volume_ignition_1h_signal,
|
||||||
|
)
|
||||||
from app.strategies.box_retest_4h import build_box_retest_1h_signal, build_box_retest_4h_signal
|
from app.strategies.box_retest_4h import build_box_retest_1h_signal, build_box_retest_4h_signal
|
||||||
from app.config.config_loader import _get_section as _get_cfg_section
|
from app.config.config_loader import _get_section as _get_cfg_section
|
||||||
from app.core.pa_engine import (
|
from app.core.pa_engine import (
|
||||||
@ -85,10 +90,13 @@ def _strategy_context_for_recommendation(symbol: str, result: dict, entry_plan:
|
|||||||
"""Build and persist a standard strategy signal when an independent strategy matches."""
|
"""Build and persist a standard strategy signal when an independent strategy matches."""
|
||||||
bp_1h = result.get("box_breakout_pullback_1h") or (result.get("market_context") or {}).get("box_breakout_pullback_1h") or {}
|
bp_1h = result.get("box_breakout_pullback_1h") or (result.get("market_context") or {}).get("box_breakout_pullback_1h") or {}
|
||||||
bp_4h = result.get("box_breakout_pullback_4h") or (result.get("market_context") or {}).get("box_breakout_pullback_4h") or {}
|
bp_4h = result.get("box_breakout_pullback_4h") or (result.get("market_context") or {}).get("box_breakout_pullback_4h") or {}
|
||||||
if not bp_1h.get("detected") and not bp_4h.get("detected"):
|
|
||||||
return {}
|
|
||||||
market_regime = result.get("market_regime") or (result.get("market_context") or {}).get("market_regime") or {}
|
market_regime = result.get("market_regime") or (result.get("market_context") or {}).get("market_regime") or {}
|
||||||
signal_candidates = []
|
signal_candidates = []
|
||||||
|
signal_candidates.extend([
|
||||||
|
build_volume_ignition_1h_signal(symbol=symbol, result=result, entry_plan=entry_plan or {}),
|
||||||
|
build_compression_breakout_4h_signal(symbol=symbol, result=result, entry_plan=entry_plan or {}),
|
||||||
|
build_intraday_momentum_15m_signal(symbol=symbol, result=result, entry_plan=entry_plan or {}),
|
||||||
|
])
|
||||||
if bp_1h.get("detected"):
|
if bp_1h.get("detected"):
|
||||||
signal_candidates.append(
|
signal_candidates.append(
|
||||||
build_box_retest_1h_signal(
|
build_box_retest_1h_signal(
|
||||||
@ -2074,8 +2082,8 @@ def main(compact: bool = False, verbose: bool = False, limit: int | None = None,
|
|||||||
detail={**cand_detail, **result},
|
detail={**cand_detail, **result},
|
||||||
)
|
)
|
||||||
result["state_update"] = state_result
|
result["state_update"] = state_result
|
||||||
# 飞书只是通知层:确认阶段不再绕过 recommendation 主链路直接推送。
|
# 飞书只是通知层:确认阶段不再绕过 recommendation 状态机直接推送。
|
||||||
# 先完成 create_recommendation + DB 主状态派生,再用同一条主链路结果决定是否通知。
|
# 先完成 create_recommendation + DB 状态派生,再用同一条状态结果决定是否通知。
|
||||||
|
|
||||||
# 🟢 只做做多!方向永远多头
|
# 🟢 只做做多!方向永远多头
|
||||||
rec_direction = get_strategy_direction()
|
rec_direction = get_strategy_direction()
|
||||||
|
|||||||
@ -504,7 +504,7 @@ def _fallback_answer(intent: str, message: str, context: dict) -> dict:
|
|||||||
evidence.append(f"技术面:{tech_summary.get('headline', '')}")
|
evidence.append(f"技术面:{tech_summary.get('headline', '')}")
|
||||||
if recommendations:
|
if recommendations:
|
||||||
r = recommendations[0]
|
r = recommendations[0]
|
||||||
evidence.append(f"主链路:{r.get('execution_label') or r.get('action_status') or r.get('execution_status')},原因:{r.get('execution_reason') or r.get('state_reason') or '--'}")
|
evidence.append(f"策略状态:{r.get('execution_label') or r.get('action_status') or r.get('execution_status')},原因:{r.get('execution_reason') or r.get('state_reason') or '--'}")
|
||||||
if sentiment:
|
if sentiment:
|
||||||
evidence.append(f"舆情:近 72h 有 {len(sentiment)} 条相关事件,最新为「{sentiment[0].get('title', '')[:60]}」。")
|
evidence.append(f"舆情:近 72h 有 {len(sentiment)} 条相关事件,最新为「{sentiment[0].get('title', '')[:60]}」。")
|
||||||
if isinstance(onchain, dict) and (onchain.get("events") or onchain.get("metrics")):
|
if isinstance(onchain, dict) and (onchain.get("events") or onchain.get("metrics")):
|
||||||
|
|||||||
@ -321,8 +321,8 @@ def track_prices():
|
|||||||
# PA增强:动态跟踪信号分析
|
# PA增强:动态跟踪信号分析
|
||||||
tracking_signals = analyze_tracking_signals(symbol, rec, current_price)
|
tracking_signals = analyze_tracking_signals(symbol, rec, current_price)
|
||||||
|
|
||||||
# 主链路状态迁移:tracker 只提交“候选状态 + 当前价”,最终状态由 DB 主链路统一落库。
|
# 统一状态迁移:tracker 只提交“候选状态 + 当前价”,最终状态由 DB 状态机统一落库。
|
||||||
# 飞书推送只能消费主链路返回的最终状态,不能再自行判断。
|
# 飞书推送只能消费统一状态机返回的最终状态,不能再自行判断。
|
||||||
terminal_action = {
|
terminal_action = {
|
||||||
"hit_tp2": "止盈2",
|
"hit_tp2": "止盈2",
|
||||||
"stopped_out": "止损",
|
"stopped_out": "止损",
|
||||||
|
|||||||
@ -908,7 +908,7 @@ def _extract_rules_from_review():
|
|||||||
"score_adjust": 2 if len(combo) >= 2 else 1,
|
"score_adjust": 2 if len(combo) >= 2 else 1,
|
||||||
"source": "review_pattern",
|
"source": "review_pattern",
|
||||||
}
|
}
|
||||||
# 新体系:先进入候选规则池,不直接污染主策略。达到发布门槛后再升级为 active。
|
# 新体系:先进入候选规则池,不直接污染已发布策略。达到发布门槛后再升级为 active。
|
||||||
rule["candidate_id"] = upsert_strategy_rule_candidate(
|
rule["candidate_id"] = upsert_strategy_rule_candidate(
|
||||||
source="review_pattern",
|
source="review_pattern",
|
||||||
rule_type=rule.get("type", "bonus"),
|
rule_type=rule.get("type", "bonus"),
|
||||||
@ -1215,7 +1215,7 @@ def _build_dual_attribution(results, effect_summary):
|
|||||||
confidence_score=round(min(90, 45 + cnt * 10), 1),
|
confidence_score=round(min(90, 45 + cnt * 10), 1),
|
||||||
sample_size=cnt,
|
sample_size=cnt,
|
||||||
status="candidate",
|
status="candidate",
|
||||||
notes="失败归因生成:先入候选池,不立即改主策略",
|
notes="失败归因生成:先入候选池,不立即改已发布策略",
|
||||||
source_ref=f"failure:{ftype}",
|
source_ref=f"failure:{ftype}",
|
||||||
)
|
)
|
||||||
candidate_rules.append({"id": cid, "type": "penalty", "signal": ftype, "description": desc, "confidence_score": round(min(90, 45 + cnt * 10), 1), "sample_size": cnt, "status": "candidate"})
|
candidate_rules.append({"id": cid, "type": "penalty", "signal": ftype, "description": desc, "confidence_score": round(min(90, 45 + cnt * 10), 1), "sample_size": cnt, "status": "candidate"})
|
||||||
|
|||||||
176
app/strategies/altcoin_breakout.py
Normal file
176
app/strategies/altcoin_breakout.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
"""Altcoin breakout strategy profiles.
|
||||||
|
|
||||||
|
These builders turn existing evidence into complete strategy signals. A factor
|
||||||
|
can support a strategy, but the strategy still owns trigger freshness, entry
|
||||||
|
quality and risk semantics.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.core.factor_roles import CONFIRMATION, ENTRY, PREREQUISITE, RISK, TRIGGER
|
||||||
|
from app.core.strategy_contract import StrategySignal, current_strategy_version
|
||||||
|
from app.core.strategy_registry import (
|
||||||
|
COMPRESSION_BREAKOUT_4H_STRATEGY,
|
||||||
|
INTRADAY_MOMENTUM_15M_STRATEGY,
|
||||||
|
VOLUME_IGNITION_1H_STRATEGY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_float(value, default=0.0) -> float:
|
||||||
|
try:
|
||||||
|
if value is None or value == "":
|
||||||
|
return default
|
||||||
|
return float(value)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _signals_text(result: dict) -> str:
|
||||||
|
return " ".join(str(x or "") for x in (result or {}).get("signals") or [])
|
||||||
|
|
||||||
|
|
||||||
|
def _trigger_context(result: dict) -> dict:
|
||||||
|
return (result or {}).get("trigger_context") or ((result or {}).get("market_context") or {}).get("trigger_context") or {}
|
||||||
|
|
||||||
|
|
||||||
|
def _has_current_trigger(result: dict) -> bool:
|
||||||
|
trigger = _trigger_context(result)
|
||||||
|
text = _signals_text(result)
|
||||||
|
return bool(trigger.get("current_triggers")) or "15min即刻入场" in text or "15min强突破" in text or "15min 强突破" in text
|
||||||
|
|
||||||
|
|
||||||
|
def _status_for_entry(result: dict, entry_plan: dict | None = None, *, require_current_trigger: bool = False, allow_wait: bool = True) -> tuple[str, list[str]]:
|
||||||
|
reasons = []
|
||||||
|
entry_action = str((entry_plan or {}).get("entry_action") or ((result or {}).get("entry_plan") or {}).get("entry_action") or (result or {}).get("entry_action") or "").strip()
|
||||||
|
if require_current_trigger and not _has_current_trigger(result):
|
||||||
|
return "observe", ["缺少当前低周期触发"]
|
||||||
|
if entry_action in ("可即刻买入", "即刻买入"):
|
||||||
|
return "candidate", reasons
|
||||||
|
if entry_action == "等回踩" and allow_wait:
|
||||||
|
return "candidate", reasons
|
||||||
|
if entry_action:
|
||||||
|
reasons.append(f"当前入场动作 {entry_action} 不满足策略执行条件")
|
||||||
|
return "observe", reasons
|
||||||
|
|
||||||
|
|
||||||
|
def build_volume_ignition_1h_signal(*, symbol: str, result: dict, entry_plan: dict | None = None) -> StrategySignal | None:
|
||||||
|
text = _signals_text(result)
|
||||||
|
has_vp = "量价齐飞" in text or ("连续" in text and "放量" in text)
|
||||||
|
has_breakout = "1H" in text and ("突破" in text or "起爆" in text)
|
||||||
|
if not (has_vp or has_breakout):
|
||||||
|
return None
|
||||||
|
status, reasons = _status_for_entry(result, entry_plan, require_current_trigger=False, allow_wait=True)
|
||||||
|
score = _safe_float(result.get("score"))
|
||||||
|
confidence = min(100.0, max(0.0, score * 7 + (12 if _has_current_trigger(result) else 0)))
|
||||||
|
trigger = {
|
||||||
|
"factor_code": "vp_fly_1h_current" if has_vp else "ignition_1h_current",
|
||||||
|
"factor_label": "1H放量突破启动",
|
||||||
|
"has_current_trigger": _has_current_trigger(result),
|
||||||
|
"trigger_status": _trigger_context(result).get("trigger_status") or "",
|
||||||
|
"entry_action": (entry_plan or {}).get("entry_action") or "",
|
||||||
|
}
|
||||||
|
return StrategySignal(
|
||||||
|
strategy_code=VOLUME_IGNITION_1H_STRATEGY,
|
||||||
|
strategy_version=current_strategy_version(),
|
||||||
|
symbol=symbol,
|
||||||
|
direction="long",
|
||||||
|
status=status,
|
||||||
|
confidence=confidence,
|
||||||
|
score=score,
|
||||||
|
trigger=trigger,
|
||||||
|
factor_roles={
|
||||||
|
"vp_fly_1h_current": TRIGGER,
|
||||||
|
"volume_consecutive_1h": CONFIRMATION,
|
||||||
|
"breakout_15m_current": ENTRY,
|
||||||
|
"pullback_15m_confirm": ENTRY,
|
||||||
|
"false_breakout": RISK,
|
||||||
|
"risk_reward_bad": RISK,
|
||||||
|
},
|
||||||
|
entry_plan=entry_plan or {},
|
||||||
|
risk_plan={
|
||||||
|
"invalid_if": ["放量后不能延续", "15m假突破", "跌回启动K低点", "RR不足"],
|
||||||
|
"risk_reasons": reasons,
|
||||||
|
},
|
||||||
|
decision_log={"module": VOLUME_IGNITION_1H_STRATEGY, "decision": status, "reasons": reasons},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_compression_breakout_4h_signal(*, symbol: str, result: dict, entry_plan: dict | None = None) -> StrategySignal | None:
|
||||||
|
text = _signals_text(result)
|
||||||
|
has_compression = any(key in text for key in ("静K", "压缩", "布林收窄", "底部抬高"))
|
||||||
|
has_breakout_context = any(key in text for key in ("突破", "起爆", "量价齐飞", "回踩"))
|
||||||
|
if not (has_compression and has_breakout_context):
|
||||||
|
return None
|
||||||
|
status, reasons = _status_for_entry(result, entry_plan, require_current_trigger=False, allow_wait=True)
|
||||||
|
score = _safe_float(result.get("score"))
|
||||||
|
confidence = min(100.0, max(0.0, score * 6 + (10 if "底部" in text else 0) + (8 if _has_current_trigger(result) else 0)))
|
||||||
|
return StrategySignal(
|
||||||
|
strategy_code=COMPRESSION_BREAKOUT_4H_STRATEGY,
|
||||||
|
strategy_version=current_strategy_version(),
|
||||||
|
symbol=symbol,
|
||||||
|
direction="long",
|
||||||
|
status=status,
|
||||||
|
confidence=confidence,
|
||||||
|
score=score,
|
||||||
|
trigger={
|
||||||
|
"factor_code": "compression_surge_4h",
|
||||||
|
"factor_label": "4H压缩蓄力突破",
|
||||||
|
"has_current_trigger": _has_current_trigger(result),
|
||||||
|
"trigger_status": _trigger_context(result).get("trigger_status") or "",
|
||||||
|
},
|
||||||
|
factor_roles={
|
||||||
|
"static_accum_4h": PREREQUISITE,
|
||||||
|
"higher_lows_4h": PREREQUISITE,
|
||||||
|
"compression_surge_4h": TRIGGER,
|
||||||
|
"ignition_4h_current": CONFIRMATION,
|
||||||
|
"vp_fly_1h_current": CONFIRMATION,
|
||||||
|
"pullback_15m_confirm": ENTRY,
|
||||||
|
"false_breakout": RISK,
|
||||||
|
},
|
||||||
|
entry_plan=entry_plan or {},
|
||||||
|
risk_plan={
|
||||||
|
"invalid_if": ["突破后跌回压缩区间", "回踩放量跌破", "无量反抽失败", "市场风险升高"],
|
||||||
|
"risk_reasons": reasons,
|
||||||
|
},
|
||||||
|
decision_log={"module": COMPRESSION_BREAKOUT_4H_STRATEGY, "decision": status, "reasons": reasons},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_intraday_momentum_15m_signal(*, symbol: str, result: dict, entry_plan: dict | None = None) -> StrategySignal | None:
|
||||||
|
text = _signals_text(result)
|
||||||
|
has_short_tf = any(key in text for key in ("15min", "15m", "短周期", "5m/15m"))
|
||||||
|
if not has_short_tf:
|
||||||
|
return None
|
||||||
|
status, reasons = _status_for_entry(result, entry_plan, require_current_trigger=True, allow_wait=False)
|
||||||
|
score = _safe_float(result.get("score"))
|
||||||
|
confidence = min(100.0, max(0.0, score * 6 + 18))
|
||||||
|
return StrategySignal(
|
||||||
|
strategy_code=INTRADAY_MOMENTUM_15M_STRATEGY,
|
||||||
|
strategy_version=current_strategy_version(),
|
||||||
|
symbol=symbol,
|
||||||
|
direction="long",
|
||||||
|
status=status,
|
||||||
|
confidence=confidence,
|
||||||
|
score=score,
|
||||||
|
trigger={
|
||||||
|
"factor_code": "short_tf_15m_ignition",
|
||||||
|
"factor_label": "15m日内动量延续",
|
||||||
|
"has_current_trigger": _has_current_trigger(result),
|
||||||
|
"trigger_status": _trigger_context(result).get("trigger_status") or "",
|
||||||
|
},
|
||||||
|
factor_roles={
|
||||||
|
"short_tf_5m_ignition": PREREQUISITE,
|
||||||
|
"short_tf_15m_ignition": TRIGGER,
|
||||||
|
"short_tf_resonance": CONFIRMATION,
|
||||||
|
"vp_fly_1h_current": CONFIRMATION,
|
||||||
|
"breakout_15m_current": ENTRY,
|
||||||
|
"false_breakout": RISK,
|
||||||
|
"trend_exhaustion": RISK,
|
||||||
|
},
|
||||||
|
entry_plan=entry_plan or {},
|
||||||
|
risk_plan={
|
||||||
|
"invalid_if": ["15m跌回突破K", "短周期量能衰减", "快速冲高回落", "RR不足"],
|
||||||
|
"risk_reasons": reasons,
|
||||||
|
},
|
||||||
|
decision_log={"module": INTRADAY_MOMENTUM_15M_STRATEGY, "decision": status, "reasons": reasons},
|
||||||
|
)
|
||||||
@ -11,6 +11,7 @@ from app.db.paper_trading import (
|
|||||||
reset_paper_trading_data,
|
reset_paper_trading_data,
|
||||||
send_paper_trading_report,
|
send_paper_trading_report,
|
||||||
)
|
)
|
||||||
|
from app.db.strategy_insights import get_strategy_evaluation
|
||||||
from app.web.shared import require_admin
|
from app.web.shared import require_admin
|
||||||
|
|
||||||
|
|
||||||
@ -29,6 +30,12 @@ async def api_paper_trading_performance(days: int = 30, altcoin_session: str = C
|
|||||||
return get_paper_trading_performance(days=days)
|
return get_paper_trading_performance(days=days)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/paper-trading/strategies")
|
||||||
|
async def api_paper_trading_strategies(days: int = 30, altcoin_session: str = Cookie(default="")):
|
||||||
|
require_admin(altcoin_session)
|
||||||
|
return get_strategy_evaluation(days=days)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/paper-trading/trades")
|
@router.get("/api/paper-trading/trades")
|
||||||
async def api_paper_trading_trades(
|
async def api_paper_trading_trades(
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
@ -59,10 +66,11 @@ async def api_paper_trading_events(
|
|||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
symbol: str = "",
|
symbol: str = "",
|
||||||
event_type: str = "",
|
event_type: str = "",
|
||||||
|
strategy_code: str = "",
|
||||||
altcoin_session: str = Cookie(default=""),
|
altcoin_session: str = Cookie(default=""),
|
||||||
):
|
):
|
||||||
require_admin(altcoin_session)
|
require_admin(altcoin_session)
|
||||||
return list_paper_trade_events(limit=limit, offset=offset, symbol=symbol, event_type=event_type)
|
return list_paper_trade_events(limit=limit, offset=offset, symbol=symbol, event_type=event_type, strategy_code=strategy_code)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/paper-trading/report")
|
@router.post("/api/paper-trading/report")
|
||||||
|
|||||||
@ -12,6 +12,7 @@ from app.db.review_queries import (
|
|||||||
get_strategy_rule_candidates,
|
get_strategy_rule_candidates,
|
||||||
refresh_strategy_candidate_performance,
|
refresh_strategy_candidate_performance,
|
||||||
)
|
)
|
||||||
|
from app.db.strategy_insights import get_strategy_evaluation
|
||||||
from app.services.llm_insights import get_latest_review_memo
|
from app.services.llm_insights import get_latest_review_memo
|
||||||
from app.db.schema import get_conn
|
from app.db.schema import get_conn
|
||||||
from app.db.altcoin_db import _derive_execution_fields
|
from app.db.altcoin_db import _derive_execution_fields
|
||||||
@ -72,6 +73,12 @@ async def api_strategy_insights(altcoin_session: str = Cookie(default="")):
|
|||||||
return get_strategy_insights()
|
return get_strategy_insights()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/strategy/evaluation")
|
||||||
|
async def api_strategy_evaluation(days: int = 30, altcoin_session: str = Cookie(default="")):
|
||||||
|
require_api_user_with_subscription(altcoin_session)
|
||||||
|
return get_strategy_evaluation(days=days)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/strategy/lifecycle")
|
@router.get("/api/strategy/lifecycle")
|
||||||
async def api_strategy_lifecycle(days: int = 30, altcoin_session: str = Cookie(default="")):
|
async def api_strategy_lifecycle(days: int = 30, altcoin_session: str = Cookie(default="")):
|
||||||
require_api_user_with_subscription(altcoin_session)
|
require_api_user_with_subscription(altcoin_session)
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## 1. 为什么要改
|
## 1. 为什么要改
|
||||||
|
|
||||||
当前系统已经有 `strategy_version`、`FactorScorer`、`factor_score_breakdown` 和 paper trading 归因,但主链路仍更像一个“大而全评分器”:
|
当前系统已经有 `strategy_version`、`FactorScorer`、`factor_score_breakdown` 和 paper trading 归因,但综合确认策略仍更像一个“大而全评分器”:
|
||||||
|
|
||||||
- 很多因子在同一个确认函数里叠加,容易把单根行情重复加分。
|
- 很多因子在同一个确认函数里叠加,容易把单根行情重复加分。
|
||||||
- 推荐和 paper trading 更容易知道“综合分高不高”,但不容易知道“到底是哪套策略赚了钱”。
|
- 推荐和 paper trading 更容易知道“综合分高不高”,但不容易知道“到底是哪套策略赚了钱”。
|
||||||
@ -23,7 +23,7 @@
|
|||||||
- 策略输出必须走统一契约,不能每个策略自定义一套不可比的 JSON。
|
- 策略输出必须走统一契约,不能每个策略自定义一套不可比的 JSON。
|
||||||
- 推荐、挂单、持仓、事件日志、复盘都必须保留策略血缘。
|
- 推荐、挂单、持仓、事件日志、复盘都必须保留策略血缘。
|
||||||
- 因子角色必须显式声明,未知因子不能默认成为交易触发。
|
- 因子角色必须显式声明,未知因子不能默认成为交易触发。
|
||||||
- 旧主链路可以兼容为 `main_composite_v1`,但新数据不允许没有策略来源。
|
- 历史综合确认策略可以兼容为 `main_composite_v1`,但它与其他策略平等运行;新数据不允许没有策略来源。
|
||||||
- 策略中文名、描述、启用状态要集中维护,不能散落在页面和业务代码里。
|
- 策略中文名、描述、启用状态要集中维护,不能散落在页面和业务代码里。
|
||||||
- 多策略架构上线后,收益评价必须按策略拆分;总收益只能作为账户层结果,不能替代策略评价。
|
- 多策略架构上线后,收益评价必须按策略拆分;总收益只能作为账户层结果,不能替代策略评价。
|
||||||
|
|
||||||
@ -179,12 +179,12 @@
|
|||||||
- 建立 `app/core/strategy_contract.py`,定义策略输出结构。
|
- 建立 `app/core/strategy_contract.py`,定义策略输出结构。
|
||||||
- 建立 `app/core/factor_roles.py`,统一因子角色分类。
|
- 建立 `app/core/factor_roles.py`,统一因子角色分类。
|
||||||
- 给 `recommendation` / `paper_trades` / `paper_orders` 补 `strategy_code` 和 `strategy_signal_id`。
|
- 给 `recommendation` / `paper_trades` / `paper_orders` 补 `strategy_code` 和 `strategy_signal_id`。
|
||||||
- 在确认层先把现有主链路标为 `main_composite_v1`。
|
- 在确认层先把现有综合确认策略标为 `main_composite_v1`。
|
||||||
- 把 `box_breakout_pullback_4h` 标记为 `box_retest_4h_v1` 的核心触发候选,但仍通过完整策略条件判断。
|
- 把 `box_breakout_pullback_4h` 标记为 `box_retest_4h_v1` 的核心触发候选,但仍通过完整策略条件判断。
|
||||||
|
|
||||||
### P1:拆出第一个独立策略
|
### P1:拆出第一个独立策略
|
||||||
|
|
||||||
目标:让 `box_retest_4h_v1` 独立运行,与原主链路并行。
|
目标:让 `box_retest_4h_v1` 独立运行,与其他策略平等并行。
|
||||||
|
|
||||||
- 新增 `app/strategies/box_retest_4h.py`。
|
- 新增 `app/strategies/box_retest_4h.py`。
|
||||||
- 让它消费统一交易宇宙和 4H K线。
|
- 让它消费统一交易宇宙和 4H K线。
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
今晚目标不是一次性完成所有长期架构,而是交付一个能真实运行的第一阶段:
|
今晚目标不是一次性完成所有长期架构,而是交付一个能真实运行的第一阶段:
|
||||||
|
|
||||||
- 每条推荐、挂单、持仓都能追溯到策略。
|
- 每条推荐、挂单、持仓都能追溯到策略。
|
||||||
- 现有主链路不被打断,统一标记为 `main_composite_v1`。
|
- 现有综合确认策略不被打断,统一标记为 `main_composite_v1`。
|
||||||
- `box_retest_4h_v1` 进入独立策略雏形,不再只是综合确认层里的一个因子。
|
- `box_retest_4h_v1` 进入独立策略雏形,不再只是综合确认层里的一个因子。
|
||||||
- paper trading 和复盘能按策略看表现。
|
- paper trading 和复盘能按策略看表现。
|
||||||
- 前端至少能看到策略来源,明早可以观察跑出来的数据。
|
- 前端至少能看到策略来源,明早可以观察跑出来的数据。
|
||||||
@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
允许的兼容:
|
允许的兼容:
|
||||||
|
|
||||||
- 旧主链路继续运行,但必须标记为 `main_composite_v1`。
|
- 旧综合确认策略继续运行,但必须标记为 `main_composite_v1`,并与其他策略平等。
|
||||||
- 第一版 `box_retest_4h_v1` 可以先 paper-only / observe-only。
|
- 第一版 `box_retest_4h_v1` 可以先 paper-only / observe-only。
|
||||||
- 复杂仲裁可以先做最小规则,但接口要为后续多策略扩展留好。
|
- 复杂仲裁可以先做最小规则,但接口要为后续多策略扩展留好。
|
||||||
|
|
||||||
@ -219,7 +219,7 @@ risk_reward_bad -> risk
|
|||||||
entry_quality_gate -> risk
|
entry_quality_gate -> risk
|
||||||
```
|
```
|
||||||
|
|
||||||
注意:同一因子在不同策略里可以有不同角色,但默认映射用于主链路兼容。
|
注意:同一因子在不同策略里可以有不同角色;默认映射只用于兼容综合确认策略。
|
||||||
|
|
||||||
该模块必须保持纯函数,不依赖 DB,不读运行时配置,避免策略基础设施反向依赖业务层。
|
该模块必须保持纯函数,不依赖 DB,不读运行时配置,避免策略基础设施反向依赖业务层。
|
||||||
|
|
||||||
@ -252,7 +252,7 @@ decision_log: dict
|
|||||||
|
|
||||||
- 提供 `to_json_dict()`,统一序列化。
|
- 提供 `to_json_dict()`,统一序列化。
|
||||||
- 提供 `from_confirm_result()`,兼容现有确认链路。
|
- 提供 `from_confirm_result()`,兼容现有确认链路。
|
||||||
- 提供 `default_main_composite_signal()`,给旧主链路生成标准策略快照。
|
- 提供 `default_main_composite_signal()`,给旧综合确认策略生成标准策略快照。
|
||||||
- 不在这里调用数据库,不在这里拉行情。
|
- 不在这里调用数据库,不在这里拉行情。
|
||||||
|
|
||||||
### 3.2.1 新增 `app/db/strategy_signal_queries.py`
|
### 3.2.1 新增 `app/db/strategy_signal_queries.py`
|
||||||
@ -337,7 +337,7 @@ app/core/strategy_registry.py
|
|||||||
初始策略:
|
初始策略:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
main_composite_v1 -> 综合确认主链路
|
main_composite_v1 -> 综合确认策略
|
||||||
box_retest_4h_v1 -> 4H箱体突破回踩
|
box_retest_4h_v1 -> 4H箱体突破回踩
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -403,15 +403,15 @@ app/strategies/orchestrator.py
|
|||||||
|
|
||||||
- `StrategySignal(strategy_code='box_retest_4h_v1')`
|
- `StrategySignal(strategy_code='box_retest_4h_v1')`
|
||||||
|
|
||||||
### 4.4 与现有主链路关系
|
### 4.4 与现有综合确认策略关系
|
||||||
|
|
||||||
第一阶段不能直接删除现有主链路里的 `box_breakout_pullback_4h` 加分,否则会改变当前推荐行为过大。
|
第一阶段不能直接删除现有综合确认策略里的 `box_breakout_pullback_4h` 加分,否则会改变当前推荐行为过大。
|
||||||
|
|
||||||
处理方式:
|
处理方式:
|
||||||
|
|
||||||
- 主链路继续把它作为结构因子参与确认,策略来源仍为 `main_composite_v1`。
|
- 综合确认策略继续把它作为结构因子参与确认,策略来源仍为 `main_composite_v1`。
|
||||||
- 独立策略并行生成 `box_retest_4h_v1` 信号。
|
- 独立策略并行生成 `box_retest_4h_v1` 信号。
|
||||||
- 如果同一 symbol 同时被主链路和箱体策略命中,短期由仲裁器保留更明确的策略快照,或在推荐 `strategy_snapshot_json` 中记录 secondary strategies。
|
- 如果同一 symbol 同时被综合确认策略和箱体策略命中,短期由仲裁器保留更明确的策略快照,或在推荐 `strategy_snapshot_json` 中记录 secondary strategies。
|
||||||
- 明早先观察是否出现重复推荐,再决定是否让箱体策略接管该类信号。
|
- 明早先观察是否出现重复推荐,再决定是否让箱体策略接管该类信号。
|
||||||
|
|
||||||
## 5. API 与 UI 改造
|
## 5. API 与 UI 改造
|
||||||
@ -552,11 +552,11 @@ docker compose exec alphax-web python -m app.cli paper-trader
|
|||||||
- 不做复杂策略投票。
|
- 不做复杂策略投票。
|
||||||
- 不做多空完整重构。
|
- 不做多空完整重构。
|
||||||
- 不做大规模 UI 重设计。
|
- 不做大规模 UI 重设计。
|
||||||
- 不删除现有主链路。
|
- 不删除现有综合确认策略。
|
||||||
|
|
||||||
今晚可以做:
|
今晚可以做:
|
||||||
|
|
||||||
- 保留主链路运行。
|
- 保留综合确认策略运行。
|
||||||
- 补策略血缘。
|
- 补策略血缘。
|
||||||
- 补第一版策略标准结构。
|
- 补第一版策略标准结构。
|
||||||
- 让 `box_retest_4h_v1` 作为独立候选跑起来。
|
- 让 `box_retest_4h_v1` 作为独立候选跑起来。
|
||||||
@ -568,7 +568,7 @@ docker compose exec alphax-web python -m app.cli paper-trader
|
|||||||
|
|
||||||
1. 新增 migration 和回填。
|
1. 新增 migration 和回填。
|
||||||
2. 新增 `strategy_registry`、`factor_roles`、`strategy_contract` 纯核心模块。
|
2. 新增 `strategy_registry`、`factor_roles`、`strategy_contract` 纯核心模块。
|
||||||
3. 新增 `strategy_signal_queries`,不要碰业务主链路。
|
3. 新增 `strategy_signal_queries`,不要碰业务状态机。
|
||||||
4. 修改 `create_recommendation()` 写入策略血缘。
|
4. 修改 `create_recommendation()` 写入策略血缘。
|
||||||
5. 修改 paper orders / paper trades 继承策略血缘。
|
5. 修改 paper orders / paper trades 继承策略血缘。
|
||||||
6. 修改读模型/API,让前端能看到策略来源。
|
6. 修改读模型/API,让前端能看到策略来源。
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
- 建立 `app/core/strategy_registry.py`,集中维护策略代码、中文名、描述、默认模式,避免策略名散落硬编码。
|
- 建立 `app/core/strategy_registry.py`,集中维护策略代码、中文名、描述、默认模式,避免策略名散落硬编码。
|
||||||
- 建立 `app/db/strategy_signal_queries.py` 和 `strategy_signals` 表,保存标准策略信号。
|
- 建立 `app/db/strategy_signal_queries.py` 和 `strategy_signals` 表,保存标准策略信号。
|
||||||
- 给 `recommendation`、`paper_trades`、`paper_orders` 增加 `strategy_code`、`strategy_signal_id`、`strategy_snapshot_json`、`factor_roles_json` 等策略血缘字段。
|
- 给 `recommendation`、`paper_trades`、`paper_orders` 增加 `strategy_code`、`strategy_signal_id`、`strategy_snapshot_json`、`factor_roles_json` 等策略血缘字段。
|
||||||
- 现有综合链路先标记为 `main_composite_v1`,避免无策略来源的推荐继续进入账本。
|
- 现有综合确认策略先标记为 `main_composite_v1`,它与其他策略平等,避免无策略来源的推荐继续进入账本。
|
||||||
- 先把 `box_retest_4h_v1` 作为第一个独立策略候选拆出来:`box_breakout_pullback_4h` 只能是核心触发因子,仍要经过市场环境、交易宇宙、确认、入场、风控和失效条件。
|
- 先把 `box_retest_4h_v1` 作为第一个独立策略候选拆出来:`box_breakout_pullback_4h` 只能是核心触发因子,仍要经过市场环境、交易宇宙、确认、入场、风控和失效条件。
|
||||||
- 复盘中心增加按 `strategy_code` 聚合的胜率、收益、最大回撤、盈亏比和持仓时长。
|
- 复盘中心增加按 `strategy_code` 聚合的胜率、收益、最大回撤、盈亏比和持仓时长。
|
||||||
- 新数据不得出现空 `strategy_code`;旧数据通过 migration 回填为 `main_composite_v1`,但不能继续产生无来源样本。
|
- 新数据不得出现空 `strategy_code`;旧数据通过 migration 回填为 `main_composite_v1`,但不能继续产生无来源样本。
|
||||||
@ -163,7 +163,7 @@
|
|||||||
## 下一步执行建议
|
## 下一步执行建议
|
||||||
|
|
||||||
1. 先完成多策略第一阶段的策略血缘字段和标准策略输出结构。
|
1. 先完成多策略第一阶段的策略血缘字段和标准策略输出结构。
|
||||||
2. 把现有主链路标记为 `main_composite_v1`,确保 recommendation 和 paper trading 不再丢失策略来源。
|
2. 把现有综合确认策略标记为 `main_composite_v1`,确保 recommendation 和 paper trading 不再丢失策略来源。
|
||||||
3. 拆出 `box_retest_4h_v1` 作为第一个独立策略候选,先 observe/paper-only 跑样本。
|
3. 拆出 `box_retest_4h_v1` 作为第一个独立策略候选,先 observe/paper-only 跑样本。
|
||||||
4. 部署运行一段时间,观察 `market_regime`、`score_components`、`factor_score_breakdown.groups`、观察/挂单推进率是否能解释真实回撤。
|
4. 部署运行一段时间,观察 `market_regime`、`score_components`、`factor_score_breakdown.groups`、观察/挂单推进率是否能解释真实回撤。
|
||||||
5. 按线上样本校准 `entry_score` 门槛、group cap 和 high-risk 门槛。
|
5. 按线上样本校准 `entry_score` 门槛、group cap 和 high-risk 门槛。
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
- `app/analysis/`
|
- `app/analysis/`
|
||||||
- `app/web/`
|
- `app/web/`
|
||||||
|
|
||||||
### 2. 运行主链路
|
### 2. 运行服务链路
|
||||||
|
|
||||||
- `app/services/altcoin_screener.py`
|
- `app/services/altcoin_screener.py`
|
||||||
- `app/services/altcoin_confirm.py`
|
- `app/services/altcoin_confirm.py`
|
||||||
|
|||||||
@ -890,7 +890,7 @@ function renderRecCard(r) {
|
|||||||
return '<span class="sig '+cls+'">'+displaySignalText(s)+'</span>';
|
return '<span class="sig '+cls+'">'+displaySignalText(s)+'</span>';
|
||||||
}).join('');
|
}).join('');
|
||||||
var score = r.rec_score||0, st = scoreTier(score), ver = r.strategy_version||'';
|
var score = r.rec_score||0, st = scoreTier(score), ver = r.strategy_version||'';
|
||||||
var strategyLabel = r.strategy_name || ({main_composite_v1:'综合确认主链路',box_retest_1h_v1:'1H箱体突破回踩',box_retest_4h_v1:'4H箱体突破回踩'}[r.strategy_code||''] || r.strategy_code || '');
|
var strategyLabel = r.strategy_name || ({main_composite_v1:'综合确认策略',box_retest_1h_v1:'1H箱体突破回踩',box_retest_4h_v1:'4H箱体突破回踩',volume_ignition_1h_v1:'1H放量突破启动',compression_breakout_4h_v1:'4H压缩蓄力突破',intraday_momentum_15m_v1:'15m日内动量延续'}[r.strategy_code||''] || r.strategy_code || '');
|
||||||
var hasQualityGate = ep.entry_quality_gate && Array.isArray(ep.entry_quality_gate.reasons) && ep.entry_quality_gate.reasons.length;
|
var hasQualityGate = ep.entry_quality_gate && Array.isArray(ep.entry_quality_gate.reasons) && ep.entry_quality_gate.reasons.length;
|
||||||
var entryLabel = isWait ? '回踩参考' : (hasQualityGate ? '失效参考' : '参考价位');
|
var entryLabel = isWait ? '回踩参考' : (hasQualityGate ? '失效参考' : '参考价位');
|
||||||
var entryRef = (isWait || hasQualityGate) ? (ep.entry_price || r.entry_price || 0) : (r.entry_price || ep.entry_price || 0);
|
var entryRef = (isWait || hasQualityGate) ? (ep.entry_price || r.entry_price || 0) : (r.entry_price || ep.entry_price || 0);
|
||||||
|
|||||||
@ -53,7 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-head"><div class="panel-title">单币档案</div><div class="panel-note" id="detailNote">选择币种</div></div>
|
<div class="panel-head"><div class="panel-title">单币档案</div><div class="panel-note" id="detailNote">选择币种</div></div>
|
||||||
<div class="detail-body" id="detailBody"><div class="empty">从映射信号流或资产列表中选择一个币,查看链上事件与主链路关系。</div></div>
|
<div class="detail-body" id="detailBody"><div class="empty">从映射信号流或资产列表中选择一个币,查看链上事件与策略信号关系。</div></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout" style="grid-template-columns:1fr;margin-top:14px">
|
<div class="layout" style="grid-template-columns:1fr;margin-top:14px">
|
||||||
@ -63,7 +63,7 @@
|
|||||||
<select class="select" id="chainSel" onchange="reloadTokens(0)"><option value="">全部链</option><option value="ethereum">Ethereum</option><option value="bsc">BSC</option></select>
|
<select class="select" id="chainSel" onchange="reloadTokens(0)"><option value="">全部链</option><option value="ethereum">Ethereum</option><option value="bsc">BSC</option></select>
|
||||||
<select class="select" id="signalSel" onchange="reloadTokens(0)"><option value="">全部信号</option><option value="large_token_transfer">大额转账</option><option value="whale_accumulation">鲸鱼增持</option><option value="holder_growth">持有人增长</option><option value="exchange_outflow">交易所流出</option><option value="exchange_inflow_risk">交易所流入</option><option value="holder_concentration_risk">持仓集中风险</option></select>
|
<select class="select" id="signalSel" onchange="reloadTokens(0)"><option value="">全部信号</option><option value="large_token_transfer">大额转账</option><option value="whale_accumulation">鲸鱼增持</option><option value="holder_growth">持有人增长</option><option value="exchange_outflow">交易所流出</option><option value="exchange_inflow_risk">交易所流入</option><option value="holder_concentration_risk">持仓集中风险</option></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrap"><table class="table"><thead><tr><th>币种</th><th>链</th><th>重要性</th><th>风险分</th><th>映射事件</th><th>最近事件</th><th>数据源</th><th>主链路</th></tr></thead><tbody id="tokenTable"><tr><td colspan="8" class="loading">加载中...</td></tr></tbody></table></div>
|
<div class="table-wrap"><table class="table"><thead><tr><th>币种</th><th>链</th><th>重要性</th><th>风险分</th><th>映射事件</th><th>最近事件</th><th>数据源</th><th>策略状态</th></tr></thead><tbody id="tokenTable"><tr><td colspan="8" class="loading">加载中...</td></tr></tbody></table></div>
|
||||||
<div class="pager"><button id="prevBtn" onclick="page(-1)">上一页</button><span id="pageInfo">--</span><button id="nextBtn" onclick="page(1)">下一页</button></div>
|
<div class="pager"><button id="prevBtn" onclick="page(-1)">上一页</button><span id="pageInfo">--</span><button id="nextBtn" onclick="page(1)">下一页</button></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@ -94,7 +94,7 @@ async function reloadRawEvents(offset){state.rawOffset=offset||0;$('rawFeed').in
|
|||||||
async function reloadTokens(offset){state.offset=offset||0;$('tokenTable').innerHTML='<tr><td colspan="8" class="loading">加载中...</td></tr>';try{var qs='hours='+$('hoursSel').value+'&limit='+state.limit+'&offset='+state.offset+'&chain='+encodeURIComponent($('chainSel').value)+'&signal='+encodeURIComponent($('signalSel').value);var d=await (await fetch(API+'/api/onchain/tokens?'+qs)).json();state.total=d.total||0;var items=d.items||[];if(!items.length){$('tokenTable').innerHTML='<tr><td colspan="8" class="empty">暂无链上异动 token</td></tr>'}else{$('tokenTable').innerHTML=items.map(function(t){return '<tr onclick="loadDetail(\''+esc(t.symbol)+'\')"><td class="sym">'+esc(t.symbol)+'</td><td>'+esc(t.chain)+'</td><td class="num">'+Number(t.onchain_score||0).toFixed(0)+'</td><td class="num">'+Number(t.risk_score||0).toFixed(0)+'</td><td class="num">'+Number(t.event_count||t.mapped_event_count||0).toFixed(0)+'</td><td>'+esc(t.latest_event_at?fmtTime(t.latest_event_at):'--')+'</td><td>'+esc(t.source||'nodereal')+'</td><td>'+recLabel(t.recommendation)+'</td></tr>'}).join('');if(!state.selected&&items[0])loadDetail(items[0].symbol)}updatePager()}catch(e){$('tokenTable').innerHTML='<tr><td colspan="8" class="empty">加载失败</td></tr>'}}
|
async function reloadTokens(offset){state.offset=offset||0;$('tokenTable').innerHTML='<tr><td colspan="8" class="loading">加载中...</td></tr>';try{var qs='hours='+$('hoursSel').value+'&limit='+state.limit+'&offset='+state.offset+'&chain='+encodeURIComponent($('chainSel').value)+'&signal='+encodeURIComponent($('signalSel').value);var d=await (await fetch(API+'/api/onchain/tokens?'+qs)).json();state.total=d.total||0;var items=d.items||[];if(!items.length){$('tokenTable').innerHTML='<tr><td colspan="8" class="empty">暂无链上异动 token</td></tr>'}else{$('tokenTable').innerHTML=items.map(function(t){return '<tr onclick="loadDetail(\''+esc(t.symbol)+'\')"><td class="sym">'+esc(t.symbol)+'</td><td>'+esc(t.chain)+'</td><td class="num">'+Number(t.onchain_score||0).toFixed(0)+'</td><td class="num">'+Number(t.risk_score||0).toFixed(0)+'</td><td class="num">'+Number(t.event_count||t.mapped_event_count||0).toFixed(0)+'</td><td>'+esc(t.latest_event_at?fmtTime(t.latest_event_at):'--')+'</td><td>'+esc(t.source||'nodereal')+'</td><td>'+recLabel(t.recommendation)+'</td></tr>'}).join('');if(!state.selected&&items[0])loadDetail(items[0].symbol)}updatePager()}catch(e){$('tokenTable').innerHTML='<tr><td colspan="8" class="empty">加载失败</td></tr>'}}
|
||||||
function updatePager(){var page=Math.floor(state.offset/state.limit)+1,totalPages=Math.max(1,Math.ceil((state.total||0)/state.limit));$('pageInfo').textContent='第 '+page+' / '+totalPages+' 页,共 '+state.total+' 个';$('prevBtn').disabled=state.offset<=0;$('nextBtn').disabled=state.offset+state.limit>=state.total}
|
function updatePager(){var page=Math.floor(state.offset/state.limit)+1,totalPages=Math.max(1,Math.ceil((state.total||0)/state.limit));$('pageInfo').textContent='第 '+page+' / '+totalPages+' 页,共 '+state.total+' 个';$('prevBtn').disabled=state.offset<=0;$('nextBtn').disabled=state.offset+state.limit>=state.total}
|
||||||
function page(step){var next=state.offset+step*state.limit;if(next<0||next>=state.total)return;reloadTokens(next)}
|
function page(step){var next=state.offset+step*state.limit;if(next<0||next>=state.total)return;reloadTokens(next)}
|
||||||
async function loadDetail(symbol){state.selected=symbol;$('detailNote').textContent=symbol;$('detailBody').innerHTML='<div class="loading">加载详情...</div>';try{var d=await (await fetch(API+'/api/onchain/tokens/'+encodeURIComponent(symbol)+'?hours=168')).json();var latest=(d.metrics||[])[0]||{};var rec=d.recommendation;var rawCount=Number(d.raw_event_count||latest.event_count||latest.mapped_event_count||0);var metrics='<div class="metric-grid">'+[['重要性',Number(latest.onchain_score||0).toFixed(0)],['风险分',Number(latest.risk_score||0).toFixed(0)],['映射事件',rawCount.toFixed(0)],['数据源',latest.source||'nodereal']].map(function(x){return '<div class="metric"><span>'+x[0]+'</span><b>'+x[1]+'</b></div>'}).join('')+'</div>';var recHtml=rec?'<div class="hint">主链路状态:'+esc(rec.action_status||rec.execution_status||'观察')+' · 推荐 #'+esc(rec.id)+'</div>':'<div class="hint">尚未形成主链路推荐;若链上信号质量足够,会先进入技术检查。</div>';var standardEvents=(d.events||[]).slice(0,10).map(function(e){var cls=e.direction==='risk'?'risk':e.direction==='positive'?'pos':'blue';return '<div class="event"><div class="event-top"><div class="event-title">'+esc(e.signal_label||e.signal_code)+'</div><span class="badge '+cls+'">'+esc(e.severity||e.direction)+'</span></div><div class="event-meta">'+fmtTime(e.detected_at)+' · '+esc(e.chain)+' · '+fmtUsd(e.value_usd)+'<br>'+esc(e.wallet_label||e.counterparty_label||e.source||'链上事件')+'</div></div>'});var rawEvents=(d.raw_events||[]).slice(0,20).map(function(e){var amount=e.display_amount_label||fmtAmount(e.display_amount||e.total_amount||e.amount);var link=e.url?' · <a class="raw-link" href="'+esc(e.url)+'" target="_blank" rel="noopener">查看来源</a>':'';var route=(e.from_short&&e.to_short)?'<br>路径:'+esc(e.from_short)+' → '+esc(e.to_short):'';return '<div class="event"><div class="event-top"><div class="event-title">'+esc(e.human_summary||e.event_label||'NodeReal 原始事件')+'</div><span class="badge mapped">已映射</span></div><div class="event-meta">'+fmtTime(e.detected_at)+' · '+esc(e.chain)+' · '+esc(amount)+link+route+'<br>'+esc(e.pipeline_note||'已映射,可进入后续链上信号分析。')+'</div></div>'});var events=standardEvents.concat(rawEvents).join('')||'<div class="empty">暂无事件明细</div>';$('detailBody').innerHTML='<div class="detail-title">'+esc(d.symbol)+'</div><div class="detail-sub">'+(d.mappings||[]).length+' 个合约映射 · 近 7 天</div>'+metrics+recHtml+'<div class="event-feed">'+events+'</div>'}catch(e){$('detailBody').innerHTML='<div class="empty">详情加载失败</div>'}}
|
async function loadDetail(symbol){state.selected=symbol;$('detailNote').textContent=symbol;$('detailBody').innerHTML='<div class="loading">加载详情...</div>';try{var d=await (await fetch(API+'/api/onchain/tokens/'+encodeURIComponent(symbol)+'?hours=168')).json();var latest=(d.metrics||[])[0]||{};var rec=d.recommendation;var rawCount=Number(d.raw_event_count||latest.event_count||latest.mapped_event_count||0);var metrics='<div class="metric-grid">'+[['重要性',Number(latest.onchain_score||0).toFixed(0)],['风险分',Number(latest.risk_score||0).toFixed(0)],['映射事件',rawCount.toFixed(0)],['数据源',latest.source||'nodereal']].map(function(x){return '<div class="metric"><span>'+x[0]+'</span><b>'+x[1]+'</b></div>'}).join('')+'</div>';var recHtml=rec?'<div class="hint">策略状态:'+esc(rec.action_status||rec.execution_status||'观察')+' · 推荐 #'+esc(rec.id)+'</div>':'<div class="hint">尚未形成策略推荐;若链上信号质量足够,会先进入技术检查。</div>';var standardEvents=(d.events||[]).slice(0,10).map(function(e){var cls=e.direction==='risk'?'risk':e.direction==='positive'?'pos':'blue';return '<div class="event"><div class="event-top"><div class="event-title">'+esc(e.signal_label||e.signal_code)+'</div><span class="badge '+cls+'">'+esc(e.severity||e.direction)+'</span></div><div class="event-meta">'+fmtTime(e.detected_at)+' · '+esc(e.chain)+' · '+fmtUsd(e.value_usd)+'<br>'+esc(e.wallet_label||e.counterparty_label||e.source||'链上事件')+'</div></div>'});var rawEvents=(d.raw_events||[]).slice(0,20).map(function(e){var amount=e.display_amount_label||fmtAmount(e.display_amount||e.total_amount||e.amount);var link=e.url?' · <a class="raw-link" href="'+esc(e.url)+'" target="_blank" rel="noopener">查看来源</a>':'';var route=(e.from_short&&e.to_short)?'<br>路径:'+esc(e.from_short)+' → '+esc(e.to_short):'';return '<div class="event"><div class="event-top"><div class="event-title">'+esc(e.human_summary||e.event_label||'NodeReal 原始事件')+'</div><span class="badge mapped">已映射</span></div><div class="event-meta">'+fmtTime(e.detected_at)+' · '+esc(e.chain)+' · '+esc(amount)+link+route+'<br>'+esc(e.pipeline_note||'已映射,可进入后续链上信号分析。')+'</div></div>'});var events=standardEvents.concat(rawEvents).join('')||'<div class="empty">暂无事件明细</div>';$('detailBody').innerHTML='<div class="detail-title">'+esc(d.symbol)+'</div><div class="detail-sub">'+(d.mappings||[]).length+' 个合约映射 · 近 7 天</div>'+metrics+recHtml+'<div class="event-feed">'+events+'</div>'}catch(e){$('detailBody').innerHTML='<div class="empty">详情加载失败</div>'}}
|
||||||
function reloadAll(){state.offset=0;state.rawOffset=0;state.selected='';loadOverview();reloadRawEvents(0);reloadTokens(0)}
|
function reloadAll(){state.offset=0;state.rawOffset=0;state.selected='';loadOverview();reloadRawEvents(0);reloadTokens(0)}
|
||||||
reloadAll();
|
reloadAll();
|
||||||
setInterval(reloadAll,300000);
|
setInterval(reloadAll,300000);
|
||||||
|
|||||||
@ -14,6 +14,9 @@
|
|||||||
<p>这里展示策略信号进入交易账本后的表现:只有系统把可买信号转入持仓或挂单后,才会进入收益统计。推荐历史和观察池不会直接产生收益率。</p>
|
<p>这里展示策略信号进入交易账本后的表现:只有系统把可买信号转入持仓或挂单后,才会进入收益统计。推荐历史和观察池不会直接产生收益率。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
<select class="select" id="strategyFilter" title="按策略筛选" onchange="onStrategyFilterChange()">
|
||||||
|
<option value="">全部策略</option>
|
||||||
|
</select>
|
||||||
<button class="btn" id="sendReportBtn" onclick="sendReport()">发送策略交易报告</button>
|
<button class="btn" id="sendReportBtn" onclick="sendReport()">发送策略交易报告</button>
|
||||||
<button class="btn" onclick="loadAll()">刷新</button>
|
<button class="btn" onclick="loadAll()">刷新</button>
|
||||||
</div>
|
</div>
|
||||||
@ -132,7 +135,11 @@ function setTradeTab(tab){['open','orders','completed','events'].forEach(functio
|
|||||||
async function api(url){var r=await fetch(url);var d=await r.json().catch(function(){return{}});if(!r.ok)throw new Error(d.detail||d.error||'请求失败');return d}
|
async function api(url){var r=await fetch(url);var d=await r.json().catch(function(){return{}});if(!r.ok)throw new Error(d.detail||d.error||'请求失败');return d}
|
||||||
async function postApi(url){var r=await fetch(url,{method:'POST'});var d=await r.json().catch(function(){return{}});if(!r.ok)throw new Error(d.detail||d.error||'请求失败');return d}
|
async function postApi(url){var r=await fetch(url,{method:'POST'});var d=await r.json().catch(function(){return{}});if(!r.ok)throw new Error(d.detail||d.error||'请求失败');return d}
|
||||||
async function deleteApi(url){var r=await fetch(url,{method:'DELETE'});var d=await r.json().catch(function(){return{}});if(!r.ok)throw new Error((d.detail&&d.detail.reason)||d.detail||d.error||'请求失败');return d}
|
async function deleteApi(url){var r=await fetch(url,{method:'DELETE'});var d=await r.json().catch(function(){return{}});if(!r.ok)throw new Error((d.detail&&d.detail.reason)||d.detail||d.error||'请求失败');return d}
|
||||||
async function loadAll(){await Promise.all([loadSummary(),loadPerformance(),loadOrders(),loadOpenTrades(openOffset),loadCompleted(),loadEvents(eventOffset)])}
|
function selectedStrategy(){return $('strategyFilter')?($('strategyFilter').value||''):''}
|
||||||
|
function strategyQuery(){var code=selectedStrategy();return code?'&strategy_code='+encodeURIComponent(code):''}
|
||||||
|
async function loadAll(){await Promise.all([loadStrategies(),loadSummary(),loadPerformance(),loadOrders(),loadOpenTrades(openOffset),loadCompleted(),loadEvents(eventOffset)])}
|
||||||
|
function onStrategyFilterChange(){openOffset=0;eventOffset=0;loadAll()}
|
||||||
|
async function loadStrategies(){try{var d=await api('/api/paper-trading/strategies?days=120');var sel=$('strategyFilter');var current=sel.value||'';var rows=(d.strategies||[]).filter(function(x){return (x.signal_count||0)||(x.opportunity_count||0)||(x.trade_count||0)||(x.order_count||0)});sel.innerHTML='<option value="">全部策略</option>'+rows.map(function(x){return '<option value="'+esc(x.strategy_code)+'">'+esc(x.strategy_name||x.strategy_code)+' · '+esc(x.decision_label||'观察')+'</option>'}).join('');sel.value=Array.prototype.some.call(sel.options,function(o){return o.value===current})?current:''}catch(e){}}
|
||||||
async function sendReport(){var btn=$('sendReportBtn'),note=$('reportNote');btn.disabled=true;btn.textContent='发送中...';note.style.display='block';note.textContent='正在汇总当前交易数据并发送飞书报告...';try{var d=await postApi('/api/paper-trading/report?days=30');note.textContent=d.ok?'报告已发送到飞书。':'报告生成完成,但飞书发送未成功:'+String(d.push_result||'未知原因');await loadSummary()}catch(e){note.textContent='发送失败:'+e.message}finally{btn.disabled=false;btn.textContent='发送策略交易报告'}}
|
async function sendReport(){var btn=$('sendReportBtn'),note=$('reportNote');btn.disabled=true;btn.textContent='发送中...';note.style.display='block';note.textContent='正在汇总当前交易数据并发送飞书报告...';try{var d=await postApi('/api/paper-trading/report?days=30');note.textContent=d.ok?'报告已发送到飞书。':'报告生成完成,但飞书发送未成功:'+String(d.push_result||'未知原因');await loadSummary()}catch(e){note.textContent='发送失败:'+e.message}finally{btn.disabled=false;btn.textContent='发送策略交易报告'}}
|
||||||
async function resetLedger(){var scope=$('resetScope').value||'all';var label=$('resetScope').selectedOptions[0]?$('resetScope').selectedOptions[0].textContent:scope;if(!confirm('确认重置“'+label+'”?这个操作会删除策略交易账本里的对应数据,不能从页面恢复。'))return;try{var d=await postApi('/api/paper-trading/reset?scope='+encodeURIComponent(scope));var del=d.deleted||{};alert('已重置:持仓/历史 '+(del.trades||0)+' 条,挂单 '+(del.orders||0)+' 条,日志 '+(del.events||0)+' 条。');await loadAll()}catch(e){alert('重置失败:'+e.message)}}
|
async function resetLedger(){var scope=$('resetScope').value||'all';var label=$('resetScope').selectedOptions[0]?$('resetScope').selectedOptions[0].textContent:scope;if(!confirm('确认重置“'+label+'”?这个操作会删除策略交易账本里的对应数据,不能从页面恢复。'))return;try{var d=await postApi('/api/paper-trading/reset?scope='+encodeURIComponent(scope));var del=d.deleted||{};alert('已重置:持仓/历史 '+(del.trades||0)+' 条,挂单 '+(del.orders||0)+' 条,日志 '+(del.events||0)+' 条。');await loadAll()}catch(e){alert('重置失败:'+e.message)}}
|
||||||
async function deleteTrade(id,symbol,status){if(!confirm('确认删除 '+symbol+' 的'+(status==='open'?'持仓':'历史仓位')+'记录?相关操作日志也会一起删除。'))return;try{await deleteApi('/api/paper-trading/trades/'+encodeURIComponent(id));await loadAll()}catch(e){alert('删除失败:'+e.message)}}
|
async function deleteTrade(id,symbol,status){if(!confirm('确认删除 '+symbol+' 的'+(status==='open'?'持仓':'历史仓位')+'记录?相关操作日志也会一起删除。'))return;try{await deleteApi('/api/paper-trading/trades/'+encodeURIComponent(id));await loadAll()}catch(e){alert('删除失败:'+e.message)}}
|
||||||
@ -153,7 +160,7 @@ function renderPerformance(d){var points=d.points||[];if(!points.length){$('perf
|
|||||||
'<span class="perf-pill">最大回撤 '+fmt(dd,2)+'%</span>',
|
'<span class="perf-pill">最大回撤 '+fmt(dd,2)+'%</span>',
|
||||||
'<span class="perf-pill '+(pnl>=0?'pos':'neg')+'">总收益 '+(pnl>0?'+':'')+fmt(pnl,2)+'U</span>'
|
'<span class="perf-pill '+(pnl>=0?'pos':'neg')+'">总收益 '+(pnl>0?'+':'')+fmt(pnl,2)+'U</span>'
|
||||||
].join('');AlphaXCharts.renderEquity($('performanceChart'),d)}
|
].join('');AlphaXCharts.renderEquity($('performanceChart'),d)}
|
||||||
async function loadOrders(){$('orderRows').innerHTML='<tr><td colspan="11" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/orders?limit=50&offset=0&status=pending');renderOrders(d.items||[])}catch(e){$('orderRows').innerHTML='<tr><td colspan="11" class="empty">'+esc(e.message)+'</td></tr>'}}
|
async function loadOrders(){$('orderRows').innerHTML='<tr><td colspan="11" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/orders?limit=50&offset=0&status=pending'+strategyQuery());renderOrders(d.items||[])}catch(e){$('orderRows').innerHTML='<tr><td colspan="11" class="empty">'+esc(e.message)+'</td></tr>'}}
|
||||||
function renderOrders(items){if(!items.length){$('orderRows').innerHTML='<tr><td colspan="11" class="empty">暂无等待触价的策略挂单</td></tr>';return}$('orderRows').innerHTML=items.map(function(x){var latest=x.latest_price||x.current_price_at_create||0,dist=Number(x.distance_to_target_pct||0);return '<tr>'+
|
function renderOrders(items){if(!items.length){$('orderRows').innerHTML='<tr><td colspan="11" class="empty">暂无等待触价的策略挂单</td></tr>';return}$('orderRows').innerHTML=items.map(function(x){var latest=x.latest_price||x.current_price_at_create||0,dist=Number(x.distance_to_target_pct||0);return '<tr>'+
|
||||||
'<td><div class="sym">'+esc(x.symbol)+'</div><div class="muted">#'+esc(x.id)+' · Rec '+esc(x.recommendation_id)+'</div></td>'+
|
'<td><div class="sym">'+esc(x.symbol)+'</div><div class="muted">#'+esc(x.id)+' · Rec '+esc(x.recommendation_id)+'</div></td>'+
|
||||||
'<td><span class="badge open">'+esc(x.status==='pending'?'等待成交':x.status)+'</span></td>'+
|
'<td><span class="badge open">'+esc(x.status==='pending'?'等待成交':x.status)+'</span></td>'+
|
||||||
@ -168,11 +175,11 @@ function renderOrders(items){if(!items.length){$('orderRows').innerHTML='<tr><td
|
|||||||
'<td><button class="row-action" type="button" onclick="deleteOrder('+Number(x.id)+',\''+esc(String(x.symbol||'')).replace(/'/g,''')+'\')">删除</button></td>'+
|
'<td><button class="row-action" type="button" onclick="deleteOrder('+Number(x.id)+',\''+esc(String(x.symbol||'')).replace(/'/g,''')+'\')">删除</button></td>'+
|
||||||
'</tr>'}).join('')}
|
'</tr>'}).join('')}
|
||||||
function protectionCell(x){var trail=Number(x.trailing_stop||0);var trailHtml=trail>0?'<span class="trail-line">移动止盈 $'+fmt(trail,6)+'</span>':'<span class="trail-line off">移动止盈未启动</span>';return '<div class="riskline"><span>TP $'+fmt(x.tp1,6)+'</span><span>SL $'+fmt(x.stop_loss,6)+'</span>'+trailHtml+'</div>'}
|
function protectionCell(x){var trail=Number(x.trailing_stop||0);var trailHtml=trail>0?'<span class="trail-line">移动止盈 $'+fmt(trail,6)+'</span>':'<span class="trail-line off">移动止盈未启动</span>';return '<div class="riskline"><span>TP $'+fmt(x.tp1,6)+'</span><span>SL $'+fmt(x.stop_loss,6)+'</span>'+trailHtml+'</div>'}
|
||||||
function strategyName(x){return (x&&x.strategy_name)||({main_composite_v1:'综合确认主链路',box_retest_1h_v1:'1H箱体突破回踩',box_retest_4h_v1:'4H箱体突破回踩'}[(x&&x.strategy_code)||'']||((x&&x.strategy_code)||'--'))}
|
function strategyName(x){return (x&&x.strategy_name)||({main_composite_v1:'综合确认策略',box_retest_1h_v1:'1H箱体突破回踩',box_retest_4h_v1:'4H箱体突破回踩',volume_ignition_1h_v1:'1H放量突破启动',compression_breakout_4h_v1:'4H压缩蓄力突破',intraday_momentum_15m_v1:'15m日内动量延续'}[(x&&x.strategy_code)||'']||((x&&x.strategy_code)||'--'))}
|
||||||
async function loadOpenTrades(nextOffset){openOffset=Math.max(0,nextOffset||0);$('openRows').innerHTML='<tr><td colspan="14" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/trades?limit='+LIMIT+'&offset='+openOffset+'&status=open');openTotal=d.total||0;renderTradeRows('openRows',d.items||[],'暂无持仓中的策略交易');renderOpenPager()}catch(e){$('openRows').innerHTML='<tr><td colspan="14" class="empty">'+esc(e.message)+'</td></tr>'}}
|
async function loadOpenTrades(nextOffset){openOffset=Math.max(0,nextOffset||0);$('openRows').innerHTML='<tr><td colspan="14" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/trades?limit='+LIMIT+'&offset='+openOffset+'&status=open'+strategyQuery());openTotal=d.total||0;renderTradeRows('openRows',d.items||[],'暂无持仓中的策略交易');renderOpenPager()}catch(e){$('openRows').innerHTML='<tr><td colspan="14" class="empty">'+esc(e.message)+'</td></tr>'}}
|
||||||
async function loadCompleted(){await Promise.all([loadCompletedTrades(),loadCompletedOrders()])}
|
async function loadCompleted(){await Promise.all([loadCompletedTrades(),loadCompletedOrders()])}
|
||||||
async function loadCompletedTrades(){$('completedTradeRows').innerHTML='<tr><td colspan="14" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/trades?limit=80&offset=0&status=closed');renderTradeRows('completedTradeRows',d.items||[],'暂无已平仓策略交易')}catch(e){$('completedTradeRows').innerHTML='<tr><td colspan="14" class="empty">'+esc(e.message)+'</td></tr>'}}
|
async function loadCompletedTrades(){$('completedTradeRows').innerHTML='<tr><td colspan="14" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/trades?limit=80&offset=0&status=closed'+strategyQuery());renderTradeRows('completedTradeRows',d.items||[],'暂无已平仓策略交易')}catch(e){$('completedTradeRows').innerHTML='<tr><td colspan="14" class="empty">'+esc(e.message)+'</td></tr>'}}
|
||||||
async function loadCompletedOrders(){$('completedOrderRows').innerHTML='<tr><td colspan="11" class="loading">加载中...</td></tr>';try{var sets=await Promise.all(['filled','expired','canceled','rejected'].map(function(s){return api('/api/paper-trading/orders?limit=50&offset=0&status='+s)}));var items=[];sets.forEach(function(d){items=items.concat(d.items||[])});items.sort(function(a,b){return String(b.updated_at||b.created_at).localeCompare(String(a.updated_at||a.created_at))});renderCompletedOrders(items)}catch(e){$('completedOrderRows').innerHTML='<tr><td colspan="11" class="empty">'+esc(e.message)+'</td></tr>'}}
|
async function loadCompletedOrders(){$('completedOrderRows').innerHTML='<tr><td colspan="11" class="loading">加载中...</td></tr>';try{var sets=await Promise.all(['filled','expired','canceled','rejected'].map(function(s){return api('/api/paper-trading/orders?limit=50&offset=0&status='+s+strategyQuery())}));var items=[];sets.forEach(function(d){items=items.concat(d.items||[])});items.sort(function(a,b){return String(b.updated_at||b.created_at).localeCompare(String(a.updated_at||a.created_at))});renderCompletedOrders(items)}catch(e){$('completedOrderRows').innerHTML='<tr><td colspan="11" class="empty">'+esc(e.message)+'</td></tr>'}}
|
||||||
function renderTradeRows(targetId,items,emptyText){if(!items.length){$(targetId).innerHTML='<tr><td colspan="14" class="empty">'+esc(emptyText||'暂无策略交易')+'</td></tr>';return}$(targetId).innerHTML=items.map(function(x){var st=x.status==='open'?'持仓中':'已平仓';var latest=x.latest_price||x.current_price||0;var pnlUsdt=x.status==='closed'?x.realized_pnl_usdt:x.unrealized_pnl_usdt;return '<tr>'+
|
function renderTradeRows(targetId,items,emptyText){if(!items.length){$(targetId).innerHTML='<tr><td colspan="14" class="empty">'+esc(emptyText||'暂无策略交易')+'</td></tr>';return}$(targetId).innerHTML=items.map(function(x){var st=x.status==='open'?'持仓中':'已平仓';var latest=x.latest_price||x.current_price||0;var pnlUsdt=x.status==='closed'?x.realized_pnl_usdt:x.unrealized_pnl_usdt;return '<tr>'+
|
||||||
'<td><div class="sym">'+esc(x.symbol)+'</div><div class="muted">#'+esc(x.id)+' · Rec '+esc(x.recommendation_id)+'</div></td>'+
|
'<td><div class="sym">'+esc(x.symbol)+'</div><div class="muted">#'+esc(x.id)+' · Rec '+esc(x.recommendation_id)+'</div></td>'+
|
||||||
'<td><span class="badge '+esc(x.status)+'">'+st+'</span></td>'+
|
'<td><span class="badge '+esc(x.status)+'">'+st+'</span></td>'+
|
||||||
@ -205,7 +212,7 @@ function renderCompletedOrders(items){if(!items.length){$('completedOrderRows').
|
|||||||
function orderStatus(x){return {filled:'已成交',expired:'已过期',canceled:'已取消',rejected:'已拒绝'}[x.status]||x.status||'--'}
|
function orderStatus(x){return {filled:'已成交',expired:'已过期',canceled:'已取消',rejected:'已拒绝'}[x.status]||x.status||'--'}
|
||||||
function cancelReasonLabel(r){return {global_risk_rejected:'全局风控拒绝:市场/账户风险过高,未转持仓',stop_loss_leverage_risk_exceeded:'止损杠杆风险超限:按当前止损和杠杆亏损过大',portfolio_drawdown_pause:'账户回撤保护:暂停新增仓位',weak_entries_pause:'近期弱入场过多:暂停新增仓位',recommendation_invalid:'原推荐已失效:机会过期/归档后撤单',too_far_from_entry:'价格远离计划价:继续等待意义不大',expired:'挂单超时:超过有效期未成交',upgraded_to_buy_now:'信号升级为入场窗口:旧挂单已撤销',canceled:'已取消',filled:'已成交',rejected:'已拒绝'}[r]||r||'--'}
|
function cancelReasonLabel(r){return {global_risk_rejected:'全局风控拒绝:市场/账户风险过高,未转持仓',stop_loss_leverage_risk_exceeded:'止损杠杆风险超限:按当前止损和杠杆亏损过大',portfolio_drawdown_pause:'账户回撤保护:暂停新增仓位',weak_entries_pause:'近期弱入场过多:暂停新增仓位',recommendation_invalid:'原推荐已失效:机会过期/归档后撤单',too_far_from_entry:'价格远离计划价:继续等待意义不大',expired:'挂单超时:超过有效期未成交',upgraded_to_buy_now:'信号升级为入场窗口:旧挂单已撤销',canceled:'已取消',filled:'已成交',rejected:'已拒绝'}[r]||r||'--'}
|
||||||
function renderOpenPager(){var page=Math.floor(openOffset/LIMIT)+1,totalPages=Math.max(1,Math.ceil(openTotal/LIMIT));$('openPageInfo').textContent='第 '+page+' / '+totalPages+' 页 · 共 '+openTotal+' 条';$('openPager').innerHTML='<button '+(openOffset===0?'disabled':'')+' onclick="loadOpenTrades('+(openOffset-LIMIT)+')">上一页</button><span>第 '+page+' / '+totalPages+' 页</span><button '+((openOffset+LIMIT>=openTotal)?'disabled':'')+' onclick="loadOpenTrades('+(openOffset+LIMIT)+')">下一页</button>'}
|
function renderOpenPager(){var page=Math.floor(openOffset/LIMIT)+1,totalPages=Math.max(1,Math.ceil(openTotal/LIMIT));$('openPageInfo').textContent='第 '+page+' / '+totalPages+' 页 · 共 '+openTotal+' 条';$('openPager').innerHTML='<button '+(openOffset===0?'disabled':'')+' onclick="loadOpenTrades('+(openOffset-LIMIT)+')">上一页</button><span>第 '+page+' / '+totalPages+' 页</span><button '+((openOffset+LIMIT>=openTotal)?'disabled':'')+' onclick="loadOpenTrades('+(openOffset+LIMIT)+')">下一页</button>'}
|
||||||
async function loadEvents(nextOffset){eventOffset=Math.max(0,nextOffset||0);$('eventRows').innerHTML='<div class="loading">加载中...</div>';try{var sym=$('eventSymbol').value||'';var typ=$('eventType').value||'';var d=await api('/api/paper-trading/events?limit='+EVENT_LIMIT+'&offset='+eventOffset+'&symbol='+encodeURIComponent(sym)+'&event_type='+encodeURIComponent(typ));eventTotal=d.total||0;renderEvents(d.items||[]);renderEventPager()}catch(e){$('eventRows').innerHTML='<div class="empty">'+esc(e.message)+'</div>'}}
|
async function loadEvents(nextOffset){eventOffset=Math.max(0,nextOffset||0);$('eventRows').innerHTML='<div class="loading">加载中...</div>';try{var sym=$('eventSymbol').value||'';var typ=$('eventType').value||'';var d=await api('/api/paper-trading/events?limit='+EVENT_LIMIT+'&offset='+eventOffset+'&symbol='+encodeURIComponent(sym)+'&event_type='+encodeURIComponent(typ)+strategyQuery());eventTotal=d.total||0;renderEvents(d.items||[]);renderEventPager()}catch(e){$('eventRows').innerHTML='<div class="empty">'+esc(e.message)+'</div>'}}
|
||||||
function eventLabel(t){return {open:'开仓',close:'平仓',trailing_activate:'移动止盈激活',trailing_move:'移动止盈上移'}[t]||t||'动作'}
|
function eventLabel(t){return {open:'开仓',close:'平仓',trailing_activate:'移动止盈激活',trailing_move:'移动止盈上移'}[t]||t||'动作'}
|
||||||
function eventCls(t){if(t==='open')return'event-open';if(t==='close')return'event-close';if(String(t).indexOf('trailing')===0)return'event-trailing';return''}
|
function eventCls(t){if(t==='open')return'event-open';if(t==='close')return'event-close';if(String(t).indexOf('trailing')===0)return'event-trailing';return''}
|
||||||
function contextPill(text,cls){return '<span class="ctx-pill '+esc(cls||'')+'">'+esc(text)+'</span>'}
|
function contextPill(text,cls){return '<span class="ctx-pill '+esc(cls||'')+'">'+esc(text)+'</span>'}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -79,7 +79,7 @@ def _fetch_llm_row(db_path):
|
|||||||
return dict(row) if row else None
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
def test_disabled_llm_skips_and_does_not_change_mainline(monkeypatch, temp_db):
|
def test_disabled_llm_skips_and_does_not_change_strategy_state(monkeypatch, temp_db):
|
||||||
_insert_recommendation(temp_db)
|
_insert_recommendation(temp_db)
|
||||||
monkeypatch.setattr(llm_insights, "get_llm_module_enabled", lambda module: False)
|
monkeypatch.setattr(llm_insights, "get_llm_module_enabled", lambda module: False)
|
||||||
result = llm_insights.run(scope="recommendations", limit=10)
|
result = llm_insights.run(scope="recommendations", limit=10)
|
||||||
|
|||||||
@ -1,9 +1,23 @@
|
|||||||
from app.core.factor_roles import RISK, TRIGGER, factor_role, factor_roles_for_codes
|
from app.core.factor_roles import RISK, TRIGGER, factor_role, factor_roles_for_codes
|
||||||
from app.core.strategy_contract import StrategySignal, default_main_composite_signal
|
from app.core.strategy_contract import StrategySignal, default_main_composite_signal
|
||||||
from app.core.strategy_registry import BOX_RETEST_1H_STRATEGY, BOX_RETEST_4H_STRATEGY, MAIN_COMPOSITE_STRATEGY, strategy_label
|
from app.core.strategy_registry import (
|
||||||
|
BOX_RETEST_1H_STRATEGY,
|
||||||
|
BOX_RETEST_4H_STRATEGY,
|
||||||
|
COMPRESSION_BREAKOUT_4H_STRATEGY,
|
||||||
|
INTRADAY_MOMENTUM_15M_STRATEGY,
|
||||||
|
MAIN_COMPOSITE_STRATEGY,
|
||||||
|
VOLUME_IGNITION_1H_STRATEGY,
|
||||||
|
strategy_label,
|
||||||
|
)
|
||||||
from app.db.recommendation_commands import create_recommendation
|
from app.db.recommendation_commands import create_recommendation
|
||||||
from app.db.strategy_signal_queries import insert_strategy_signal
|
|
||||||
from app.db.paper_trading import _open_trade, _order_payload_from_rec
|
from app.db.paper_trading import _open_trade, _order_payload_from_rec
|
||||||
|
from app.db.strategy_signal_queries import insert_strategy_signal
|
||||||
|
from app.db.strategy_insights import evaluate_strategy_decision
|
||||||
|
from app.strategies.altcoin_breakout import (
|
||||||
|
build_compression_breakout_4h_signal,
|
||||||
|
build_intraday_momentum_15m_signal,
|
||||||
|
build_volume_ignition_1h_signal,
|
||||||
|
)
|
||||||
from app.strategies.box_retest_4h import build_box_retest_1h_signal
|
from app.strategies.box_retest_4h import build_box_retest_1h_signal
|
||||||
|
|
||||||
|
|
||||||
@ -27,9 +41,109 @@ def test_default_main_composite_strategy_signal_is_stable():
|
|||||||
).to_json_dict()
|
).to_json_dict()
|
||||||
|
|
||||||
assert signal["strategy_code"] == MAIN_COMPOSITE_STRATEGY
|
assert signal["strategy_code"] == MAIN_COMPOSITE_STRATEGY
|
||||||
assert signal["strategy_name"] == "综合确认主链路"
|
assert signal["strategy_name"] == "综合确认策略"
|
||||||
assert signal["factor_roles"]["vp_fly_1h_current"] == "trigger"
|
assert signal["factor_roles"]["vp_fly_1h_current"] == "trigger"
|
||||||
assert strategy_label(BOX_RETEST_1H_STRATEGY) == "1H箱体突破回踩"
|
assert strategy_label(BOX_RETEST_1H_STRATEGY) == "1H箱体突破回踩"
|
||||||
|
assert strategy_label(VOLUME_IGNITION_1H_STRATEGY) == "1H放量突破启动"
|
||||||
|
assert strategy_label(COMPRESSION_BREAKOUT_4H_STRATEGY) == "4H压缩蓄力突破"
|
||||||
|
assert strategy_label(INTRADAY_MOMENTUM_15M_STRATEGY) == "15m日内动量延续"
|
||||||
|
|
||||||
|
|
||||||
|
def test_volume_ignition_strategy_builds_independent_signal():
|
||||||
|
signal = build_volume_ignition_1h_signal(
|
||||||
|
symbol="VOL/USDT",
|
||||||
|
result={
|
||||||
|
"score": 8,
|
||||||
|
"signals": ["1H量价齐飞 · 连续放量", "15min即刻入场"],
|
||||||
|
"trigger_context": {"current_triggers": ["15m突破"], "trigger_status": "current"},
|
||||||
|
"entry_plan": {"entry_action": "可即刻买入"},
|
||||||
|
},
|
||||||
|
entry_plan={"entry_action": "可即刻买入", "entry_price": 1.0},
|
||||||
|
)
|
||||||
|
payload = signal.to_json_dict()
|
||||||
|
|
||||||
|
assert payload["strategy_code"] == VOLUME_IGNITION_1H_STRATEGY
|
||||||
|
assert payload["status"] == "candidate"
|
||||||
|
assert payload["trigger"]["factor_code"] == "vp_fly_1h_current"
|
||||||
|
assert payload["factor_roles"]["vp_fly_1h_current"] == "trigger"
|
||||||
|
|
||||||
|
|
||||||
|
def test_compression_breakout_strategy_requires_structure_and_breakout_context():
|
||||||
|
signal = build_compression_breakout_4h_signal(
|
||||||
|
symbol="QUIET/USDT",
|
||||||
|
result={"score": 6, "signals": ["4H静K压缩,突破箱体上沿"], "entry_plan": {"entry_action": "等回踩"}},
|
||||||
|
entry_plan={"entry_action": "等回踩", "entry_price": 1.0},
|
||||||
|
)
|
||||||
|
payload = signal.to_json_dict()
|
||||||
|
|
||||||
|
assert payload["strategy_code"] == COMPRESSION_BREAKOUT_4H_STRATEGY
|
||||||
|
assert payload["status"] == "candidate"
|
||||||
|
assert payload["factor_roles"]["compression_surge_4h"] == "trigger"
|
||||||
|
assert build_compression_breakout_4h_signal(
|
||||||
|
symbol="NOBOX/USDT",
|
||||||
|
result={"score": 6, "signals": ["1H量价齐飞"], "entry_plan": {"entry_action": "可即刻买入"}},
|
||||||
|
entry_plan={"entry_action": "可即刻买入"},
|
||||||
|
) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_intraday_momentum_strategy_requires_current_trigger():
|
||||||
|
stale = build_intraday_momentum_15m_signal(
|
||||||
|
symbol="FAST/USDT",
|
||||||
|
result={"score": 7, "signals": ["15m短周期启动"], "entry_plan": {"entry_action": "可即刻买入"}},
|
||||||
|
entry_plan={"entry_action": "可即刻买入"},
|
||||||
|
).to_json_dict()
|
||||||
|
fresh = build_intraday_momentum_15m_signal(
|
||||||
|
symbol="FAST/USDT",
|
||||||
|
result={
|
||||||
|
"score": 7,
|
||||||
|
"signals": ["15min强突破"],
|
||||||
|
"trigger_context": {"current_triggers": ["15m突破"], "trigger_status": "current"},
|
||||||
|
"entry_plan": {"entry_action": "可即刻买入"},
|
||||||
|
},
|
||||||
|
entry_plan={"entry_action": "可即刻买入"},
|
||||||
|
).to_json_dict()
|
||||||
|
|
||||||
|
assert stale["status"] == "observe"
|
||||||
|
assert "缺少当前低周期触发" in stale["risk_plan"]["risk_reasons"]
|
||||||
|
assert fresh["status"] == "candidate"
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_evaluation_recommends_promote_or_pause():
|
||||||
|
strong = evaluate_strategy_decision({
|
||||||
|
"signal_count": 24,
|
||||||
|
"opportunity_count": 16,
|
||||||
|
"trade_count": 8,
|
||||||
|
"closed_trade_count": 8,
|
||||||
|
"win_rate_pct": 62.5,
|
||||||
|
"avg_realized_pnl_pct": 3.2,
|
||||||
|
"realized_pnl_usdt": 180,
|
||||||
|
"worst_pnl_pct": -3.5,
|
||||||
|
"order_fill_rate_pct": 45,
|
||||||
|
"trade_conversion_pct": 50,
|
||||||
|
})
|
||||||
|
weak = evaluate_strategy_decision({
|
||||||
|
"signal_count": 24,
|
||||||
|
"opportunity_count": 16,
|
||||||
|
"trade_count": 8,
|
||||||
|
"closed_trade_count": 8,
|
||||||
|
"win_rate_pct": 25,
|
||||||
|
"avg_realized_pnl_pct": -2.5,
|
||||||
|
"realized_pnl_usdt": -120,
|
||||||
|
"worst_pnl_pct": -9,
|
||||||
|
"order_fill_rate_pct": 20,
|
||||||
|
"trade_conversion_pct": 50,
|
||||||
|
})
|
||||||
|
unfilled = evaluate_strategy_decision({
|
||||||
|
"signal_count": 18,
|
||||||
|
"opportunity_count": 12,
|
||||||
|
"trade_count": 0,
|
||||||
|
"closed_trade_count": 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert strong["decision"] == "promote"
|
||||||
|
assert strong["evaluation_score"] > weak["evaluation_score"]
|
||||||
|
assert weak["decision"] == "pause"
|
||||||
|
assert unfilled["decision"] == "review_entry_gate"
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_signal_insert_and_recommendation_lineage(pg_conn):
|
def test_strategy_signal_insert_and_recommendation_lineage(pg_conn):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user