diff --git a/app/routes.py b/app/routes.py index a40ef2e..5878813 100644 --- a/app/routes.py +++ b/app/routes.py @@ -928,27 +928,29 @@ def register_routes(app: Flask) -> None: flash("记录不存在或无权查看", "error") return redirect(url_for("records_page")) _annotate_trade_times(trade) - # 各周期不同前后窗口,避免 M1 过密、H1 过稀 + # 按目标根数取足够时间窗:M1=500, M5=200, M15=100, H1=50 open_ts = float( trade.get("open_time") or trade.get("created_at") or database.now() ) close_raw = trade.get("close_time") close_ts = float(close_raw) if close_raw not in (None, "") else open_ts - tf_pad = { - "M1": 90 * 60, # ±90 分钟 - "M5": 4 * 3600, # ±4 小时 - "M15": 8 * 3600, # ±8 小时 - "H1": 24 * 3600, # ±1 天 + # 秒/根 × 目标根数,再略留余量 + tf_span = { + "M1": 500 * 60, + "M5": 200 * 5 * 60, + "M15": 100 * 15 * 60, + "H1": 50 * 3600, } charts = {} for tf in ("M1", "M5", "M15", "H1"): - pad = tf_pad[tf] + span = tf_span[tf] + mid = (min(open_ts, close_ts) + max(open_ts, close_ts)) / 2.0 charts[tf] = database.get_bars( conn, trade["symbol"], tf, - min(open_ts, close_ts) - pad, - max(open_ts, close_ts) + pad, + mid - span / 2.0, + mid + span / 2.0, ) return render_template( "record_detail.html", diff --git a/app/static/css/app.css b/app/static/css/app.css index 2ce0321..b740c64 100644 --- a/app/static/css/app.css +++ b/app/static/css/app.css @@ -131,9 +131,9 @@ th { color: var(--muted); font-weight: 600; } min-height: 240px; } .chart-box h4 { margin: 0 0 8px; font-size: 13px; color: var(--muted); } -.chart-canvas { width: 100%; height: 200px; display: block; } +.chart-canvas { width: 100%; height: 240px; display: block; } .empty-chart { - height: 200px; + height: 240px; display: flex; align-items: center; justify-content: center; diff --git a/app/static/js/charts.js b/app/static/js/charts.js index 16b7ecf..1ea4473 100644 --- a/app/static/js/charts.js +++ b/app/static/js/charts.js @@ -1,6 +1,6 @@ -/** 复盘四周期蜡烛图:围绕成交裁剪窗口,入场/出场轻量标注 */ +/** 复盘四周期:固定根数裁剪;进出场只用圆点,价签放在图外图例 */ (function () { - var TF_MAX = { M1: 90, M5: 48, M15: 36, H1: 24 }; + var TF_MAX = { M1: 500, M5: 200, M15: 100, H1: 50 }; function num(v) { if (v == null || v === "") return null; @@ -22,10 +22,9 @@ return best; } - /** 只保留成交前后一段,避免 M1 挤满、H1 太空 */ function cropBars(bars, markers, tf) { if (!bars.length) return bars; - var maxN = TF_MAX[tf] || 60; + var maxN = TF_MAX[tf] || 100; if (bars.length <= maxN) return bars; var a = nearestBarIndex(bars, markers.openTs); @@ -35,7 +34,7 @@ var lo = Math.min(a, b); var hi = Math.max(a, b); var span = hi - lo + 1; - var pad = Math.max(8, Math.floor((maxN - span) / 2)); + var pad = Math.max(4, Math.floor((maxN - span) / 2)); var start = Math.max(0, lo - pad); var end = Math.min(bars.length, start + maxN); start = Math.max(0, end - maxN); @@ -49,16 +48,16 @@ var dpr = window.devicePixelRatio || 1; var cssW = canvas.clientWidth || 400; - var cssH = canvas.clientHeight || 200; + var cssH = canvas.clientHeight || 220; canvas.width = cssW * dpr; canvas.height = cssH * dpr; var ctx = canvas.getContext("2d"); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - var padL = 6; - var padR = 56; // 右侧留给价签 - var padT = 10; - var padB = 6; + var padL = 4; + var padR = 4; + var padT = 4; + var padB = 4; var min = Infinity; var max = -Infinity; @@ -75,7 +74,7 @@ max = Math.max(max, markers.closePrice); } if (!(max > min)) max = min + 1; - var padPrice = (max - min) * 0.08 || 0.8; + var padPrice = (max - min) * 0.06 || 0.5; min -= padPrice; max += padPrice; @@ -95,8 +94,7 @@ return padL + i * slot + slot * 0.5; }; - // K 线 - var bodyW = Math.max(1.5, Math.min(8, slot * 0.55)); + var bodyW = Math.max(1, Math.min(6, slot * 0.6)); bars.forEach(function (b, i) { var x = xOf(i); var up = b.close >= b.open; @@ -114,6 +112,7 @@ ctx.fillRect(x - bodyW / 2, top, bodyW, bh); }); + // 仅圆点,不画横线/竖线/价签(价签在图例外) var entryColor = "#4cc2ff"; var exitColor = "#f0b429"; var entryIdx = nearestBarIndex(bars, markers.openTs); @@ -124,109 +123,65 @@ markers.closePrice != null && exitIdx >= 0; - var openP = markers.openPrice; - var closeP = markers.closePrice; - var samePrice = - openP != null && - closeP != null && - Math.abs(openP - closeP) < 0.08; - - // 价格横线:价差很小时只画一条,避免两行字叠在一起 - function drawHLine(price, color) { - var y = yOf(price); - ctx.save(); - ctx.strokeStyle = color; - ctx.globalAlpha = 0.7; - ctx.lineWidth = 1; - ctx.setLineDash([4, 3]); - ctx.beginPath(); - ctx.moveTo(padL, y); - ctx.lineTo(padL + w, y); - ctx.stroke(); - ctx.restore(); - return y; - } - - function drawRightTag(y, text, color, dy) { - dy = dy || 0; - var ty = Math.max(padT + 10, Math.min(cssH - padB - 4, y + dy)); - ctx.save(); - ctx.font = "11px Microsoft YaHei UI, sans-serif"; - ctx.fillStyle = color; - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - ctx.fillText(text, padL + w + 4, ty); - ctx.restore(); - } - - if (samePrice && openP != null) { - var ySame = drawHLine(openP, entryColor); - drawRightTag(ySame, "入/出 " + openP.toFixed(2), entryColor, 0); - } else { - if (openP != null) { - var yIn = drawHLine(openP, entryColor); - drawRightTag(yIn, "入 " + openP.toFixed(2), entryColor, 0); - } - if (hasExit && closeP != null) { - var yOut = drawHLine(closeP, exitColor); - var dy = 0; - if (openP != null && Math.abs(yOf(openP) - yOut) < 14) { - dy = yOut < yOf(openP) ? -12 : 12; - } - drawRightTag(yOut, "出 " + closeP.toFixed(2), exitColor, dy); - } - } - - // 点:无竖线、无三角、无紫连线 —— 只留圆点 + 短字 - function drawDot(i, price, color, label, labelSide) { - if (i < 0 || price == null) return; + function drawDot(i, price, color) { + if (i < 0 || price == null) return { x: 0, y: 0 }; var x = xOf(i); var y = yOf(price); - ctx.save(); ctx.beginPath(); - ctx.arc(x, y, 4, 0, Math.PI * 2); + ctx.arc(x, y, 4.5, 0, Math.PI * 2); ctx.fillStyle = color; ctx.fill(); ctx.lineWidth = 1.5; - ctx.strokeStyle = "#0e0e18"; + ctx.strokeStyle = "#ffffff"; ctx.stroke(); - - ctx.font = "bold 10px Microsoft YaHei UI, sans-serif"; - ctx.fillStyle = color; - ctx.textBaseline = "bottom"; - if (labelSide === "left") { - ctx.textAlign = "right"; - ctx.fillText(label, x - 7, y - 4); - } else if (labelSide === "right") { - ctx.textAlign = "left"; - ctx.fillText(label, x + 7, y - 4); - } else { - ctx.textAlign = "center"; - ctx.fillText(label, x, y - 7); - } - ctx.restore(); + return { x: x, y: y }; } - var closeTogether = - hasExit && entryIdx >= 0 && Math.abs(exitIdx - entryIdx) <= 2; - - if (entryIdx >= 0 && openP != null) { - drawDot( - entryIdx, - openP, - entryColor, - "入", - closeTogether ? "left" : "center" - ); + var pe = null; + var px = null; + if (entryIdx >= 0 && markers.openPrice != null) { + pe = drawDot(entryIdx, markers.openPrice, entryColor); } if (hasExit) { - drawDot( - exitIdx, - closeP, - exitColor, - "出", - closeTogether ? "right" : "center" - ); + // 同一根或相邻:出场点略向右错开,避免叠成一团 + if (Math.abs(exitIdx - entryIdx) <= 1) { + var x = xOf(exitIdx) + Math.max(6, slot * 0.8); + var y = yOf(markers.closePrice); + if (pe && Math.abs(y - pe.y) < 6) { + y = pe.y - 8; + } + ctx.beginPath(); + ctx.arc(x, y, 4.5, 0, Math.PI * 2); + ctx.fillStyle = exitColor; + ctx.fill(); + ctx.lineWidth = 1.5; + ctx.strokeStyle = "#ffffff"; + ctx.stroke(); + px = { x: x, y: y }; + } else { + px = drawDot(exitIdx, markers.closePrice, exitColor); + } + } + + // 极短引线:点旁只写「入」「出」一字,不写价格 + ctx.font = "bold 10px Microsoft YaHei UI, sans-serif"; + if (pe) { + ctx.fillStyle = entryColor; + ctx.textAlign = "center"; + ctx.textBaseline = "bottom"; + ctx.fillText("入", pe.x, pe.y - 6); + } + if (px) { + ctx.fillStyle = exitColor; + ctx.textAlign = "center"; + ctx.textBaseline = "bottom"; + ctx.fillText("出", px.x, px.y - 6); + } + + // 更新图外图例里的实际显示根数 + var meta = canvas.parentElement && canvas.parentElement.querySelector(".chart-meta"); + if (meta) { + meta.textContent = bars.length + " 根"; } } @@ -244,7 +199,7 @@ document.querySelectorAll("canvas.chart-canvas").forEach(function (el) { try { var bars = JSON.parse(el.dataset.bars || "[]"); - var tf = (el.dataset.tf || el.id || "").replace(/^chart-/, "") || "M1"; + var tf = el.dataset.tf || "M1"; drawCandleChart(el, bars, readMarkers(el), tf); } catch (e) { console.warn(e); diff --git a/app/templates/record_detail.html b/app/templates/record_detail.html index 79f000e..1ad4667 100644 --- a/app/templates/record_detail.html +++ b/app/templates/record_detail.html @@ -63,12 +63,16 @@

四周期 K 线(M1 / M5 / M15 / H1)

- 客户端上报写入服务器;复盘读取。蓝=入场,黄=出场(价差很小时合并为一条「入/出」线)。图内只显示成交前后一段 K 线。 + 显示根数:M1 500 · M5 200 · M15 100 · H1 50(不足则有多少显多少)。 + ● 入场 {{ trade.open_price|fmt_price }} + {% if trade.close_price is not none %} + · ● 出场 {{ trade.close_price|fmt_price }} + {% endif %}

{% for tf in ['M1','M5','M15','H1'] %}
-

{{ tf }}

+

{{ tf }}

{% if charts[tf] %} None: if hub_client is None or not is_activated(): return + # 与复盘显示根数对齐:M1 500 / M5 200 / M15 100 / H1 50 tf_map = { - "M1": mt5.TIMEFRAME_M1, - "M5": mt5.TIMEFRAME_M5, - "M15": mt5.TIMEFRAME_M15, - "H1": mt5.TIMEFRAME_H1, + "M1": (mt5.TIMEFRAME_M1, 500), + "M5": (mt5.TIMEFRAME_M5, 200), + "M15": (mt5.TIMEFRAME_M15, 100), + "H1": (mt5.TIMEFRAME_H1, 50), } - for name, tf in tf_map.items(): - rates = mt5.copy_rates_from_pos(symbol, tf, 0, 400) + for name, (tf, count) in tf_map.items(): + rates = mt5.copy_rates_from_pos(symbol, tf, 0, count) if rates is None or len(rates) == 0: continue bars = [ diff --git a/releases/黄金市价辅助.exe b/releases/黄金市价辅助.exe index 5bb078f..fcfedbf 100644 Binary files a/releases/黄金市价辅助.exe and b/releases/黄金市价辅助.exe differ