feat: show mss answer overlay in training replay

This commit is contained in:
yuqei
2026-04-30 00:55:12 +08:00
parent 089a81180e
commit b228c36beb
3 changed files with 46 additions and 0 deletions
@@ -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. 推荐立即开始的第一步
+21
View File
@@ -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;
+9
View File
@@ -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")