diff --git a/docs/AI_ICT_Training_Case_Engine_开发方案_v1.md b/docs/AI_ICT_Training_Case_Engine_开发方案_v1.md index 44232d5..2fbadae 100644 --- a/docs/AI_ICT_Training_Case_Engine_开发方案_v1.md +++ b/docs/AI_ICT_Training_Case_Engine_开发方案_v1.md @@ -615,6 +615,21 @@ Invalidation:20 3. 前端在 decision frame 禁用 Back/Reset。 4. 已完成案例仍不能继续推进 replay 控制。 +### M16:MSS 答案图层 + +完成: + +1. `mss_displacement` 提交揭示答案后,在 replay chart 上绘制 `broken_level`。 +2. 绘制方向标签,例如 `bullish break` / `bearish break`。 +3. 该答案图层只在 `answer_visible=true` 后出现。 +4. 保留隐藏答案原则,列表、详情和未提交 replay 不暴露 answer key。 + +验收: + +1. MSS + Displacement 案例提交后能在图上看到结构打破价位。 +2. 标签不会依赖第三方图表库,继续使用 Training Lab 当前 Canvas。 +3. 静态测试覆盖 `broken_level` 答案图层入口。 + ## 10. 第一版验收标准 第一版完成时必须满足: @@ -634,6 +649,7 @@ Invalidation:20 13. 高阶阶段能训练 MSS + Displacement 的结构接管判断。 14. 每次提交后能自动刷新阶段路径并给出下一题入口。 15. replay 支持逐根前进、回退和重置。 +16. 高阶 MSS 答案能在图表上直观看到结构打破价位。 ## 11. 推荐立即开始的第一步 diff --git a/src/web/training/training.js b/src/web/training/training.js index 3509d62..dccb20a 100644 --- a/src/web/training/training.js +++ b/src/web/training/training.js @@ -575,6 +575,10 @@ function drawAnswerOverlay() { if (answer.pool_price_low != null && answer.pool_price_high != null) { drawPriceZone(answer.pool_price_low, answer.pool_price_high, '#f2c14e', 0.2); } + if (answer.broken_level != null) { + drawPriceLine(answer.broken_level, '#ff7a59'); + drawPriceLabel(answer.broken_level, `${answer.direction || 'MSS'} break`, '#ff7a59'); + } } function drawPriceZone(low, high, color, alpha) { @@ -603,6 +607,23 @@ function drawPriceLine(price, color) { ctx.restore(); } +function drawPriceLabel(price, text, color) { + const model = state.chartModel; + if (!model) return; + const y = model.scaleY(price); + ctx.save(); + ctx.fillStyle = color; + ctx.font = '11px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; + const label = String(text || '').slice(0, 28); + const width = ctx.measureText(label).width + 12; + const x = Math.max(model.padX, el.chart.width - model.padX - width); + ctx.globalAlpha = 0.92; + ctx.fillRect(x, Math.max(model.padY, y - 18), width, 18); + ctx.fillStyle = '#111315'; + ctx.fillText(label, x + 6, Math.max(model.padY + 12, y - 5)); + ctx.restore(); +} + function drawSelectedCandle() { const model = state.chartModel; if (!model || state.selectedCandleIndex == null) return; diff --git a/tests/unit/test_api_static_server.py b/tests/unit/test_api_static_server.py index 62fc5dc..4dcfefe 100644 --- a/tests/unit/test_api_static_server.py +++ b/tests/unit/test_api_static_server.py @@ -51,6 +51,15 @@ class ApiStaticServerTests(unittest.TestCase): self.assertIsNotNone(asset_file) self.assertTrue(asset_file.exists()) + def test_training_lab_draws_mss_answer_overlay(self) -> None: + app_file = _resolve_static_file("/training/static/training.js") + self.assertIsNotNone(app_file) + app_js = app_file.read_text(encoding="utf-8") + + self.assertIn("broken_level", app_js) + self.assertIn("MSS", app_js) + self.assertIn("drawPriceLabel", app_js) + @unittest.skipIf(shutil.which("node") is None, "node is required for JS syntax checks") def test_dashboard_javascript_syntax_is_valid(self) -> None: app_file = _resolve_static_file("/static/app.js")