feat: show mss answer overlay in training replay
This commit is contained in:
@@ -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. 推荐立即开始的第一步
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user