Review charts: M1/M5/M15/H1 bar counts 500/200/100/50; simplify markers.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+11
-9
@@ -928,27 +928,29 @@ def register_routes(app: Flask) -> None:
|
|||||||
flash("记录不存在或无权查看", "error")
|
flash("记录不存在或无权查看", "error")
|
||||||
return redirect(url_for("records_page"))
|
return redirect(url_for("records_page"))
|
||||||
_annotate_trade_times(trade)
|
_annotate_trade_times(trade)
|
||||||
# 各周期不同前后窗口,避免 M1 过密、H1 过稀
|
# 按目标根数取足够时间窗:M1=500, M5=200, M15=100, H1=50
|
||||||
open_ts = float(
|
open_ts = float(
|
||||||
trade.get("open_time") or trade.get("created_at") or database.now()
|
trade.get("open_time") or trade.get("created_at") or database.now()
|
||||||
)
|
)
|
||||||
close_raw = trade.get("close_time")
|
close_raw = trade.get("close_time")
|
||||||
close_ts = float(close_raw) if close_raw not in (None, "") else open_ts
|
close_ts = float(close_raw) if close_raw not in (None, "") else open_ts
|
||||||
tf_pad = {
|
# 秒/根 × 目标根数,再略留余量
|
||||||
"M1": 90 * 60, # ±90 分钟
|
tf_span = {
|
||||||
"M5": 4 * 3600, # ±4 小时
|
"M1": 500 * 60,
|
||||||
"M15": 8 * 3600, # ±8 小时
|
"M5": 200 * 5 * 60,
|
||||||
"H1": 24 * 3600, # ±1 天
|
"M15": 100 * 15 * 60,
|
||||||
|
"H1": 50 * 3600,
|
||||||
}
|
}
|
||||||
charts = {}
|
charts = {}
|
||||||
for tf in ("M1", "M5", "M15", "H1"):
|
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(
|
charts[tf] = database.get_bars(
|
||||||
conn,
|
conn,
|
||||||
trade["symbol"],
|
trade["symbol"],
|
||||||
tf,
|
tf,
|
||||||
min(open_ts, close_ts) - pad,
|
mid - span / 2.0,
|
||||||
max(open_ts, close_ts) + pad,
|
mid + span / 2.0,
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"record_detail.html",
|
"record_detail.html",
|
||||||
|
|||||||
@@ -131,9 +131,9 @@ th { color: var(--muted); font-weight: 600; }
|
|||||||
min-height: 240px;
|
min-height: 240px;
|
||||||
}
|
}
|
||||||
.chart-box h4 { margin: 0 0 8px; font-size: 13px; color: var(--muted); }
|
.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 {
|
.empty-chart {
|
||||||
height: 200px;
|
height: 240px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
+61
-106
@@ -1,6 +1,6 @@
|
|||||||
/** 复盘四周期蜡烛图:围绕成交裁剪窗口,入场/出场轻量标注 */
|
/** 复盘四周期:固定根数裁剪;进出场只用圆点,价签放在图外图例 */
|
||||||
(function () {
|
(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) {
|
function num(v) {
|
||||||
if (v == null || v === "") return null;
|
if (v == null || v === "") return null;
|
||||||
@@ -22,10 +22,9 @@
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 只保留成交前后一段,避免 M1 挤满、H1 太空 */
|
|
||||||
function cropBars(bars, markers, tf) {
|
function cropBars(bars, markers, tf) {
|
||||||
if (!bars.length) return bars;
|
if (!bars.length) return bars;
|
||||||
var maxN = TF_MAX[tf] || 60;
|
var maxN = TF_MAX[tf] || 100;
|
||||||
if (bars.length <= maxN) return bars;
|
if (bars.length <= maxN) return bars;
|
||||||
|
|
||||||
var a = nearestBarIndex(bars, markers.openTs);
|
var a = nearestBarIndex(bars, markers.openTs);
|
||||||
@@ -35,7 +34,7 @@
|
|||||||
var lo = Math.min(a, b);
|
var lo = Math.min(a, b);
|
||||||
var hi = Math.max(a, b);
|
var hi = Math.max(a, b);
|
||||||
var span = hi - lo + 1;
|
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 start = Math.max(0, lo - pad);
|
||||||
var end = Math.min(bars.length, start + maxN);
|
var end = Math.min(bars.length, start + maxN);
|
||||||
start = Math.max(0, end - maxN);
|
start = Math.max(0, end - maxN);
|
||||||
@@ -49,16 +48,16 @@
|
|||||||
|
|
||||||
var dpr = window.devicePixelRatio || 1;
|
var dpr = window.devicePixelRatio || 1;
|
||||||
var cssW = canvas.clientWidth || 400;
|
var cssW = canvas.clientWidth || 400;
|
||||||
var cssH = canvas.clientHeight || 200;
|
var cssH = canvas.clientHeight || 220;
|
||||||
canvas.width = cssW * dpr;
|
canvas.width = cssW * dpr;
|
||||||
canvas.height = cssH * dpr;
|
canvas.height = cssH * dpr;
|
||||||
var ctx = canvas.getContext("2d");
|
var ctx = canvas.getContext("2d");
|
||||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
|
||||||
var padL = 6;
|
var padL = 4;
|
||||||
var padR = 56; // 右侧留给价签
|
var padR = 4;
|
||||||
var padT = 10;
|
var padT = 4;
|
||||||
var padB = 6;
|
var padB = 4;
|
||||||
|
|
||||||
var min = Infinity;
|
var min = Infinity;
|
||||||
var max = -Infinity;
|
var max = -Infinity;
|
||||||
@@ -75,7 +74,7 @@
|
|||||||
max = Math.max(max, markers.closePrice);
|
max = Math.max(max, markers.closePrice);
|
||||||
}
|
}
|
||||||
if (!(max > min)) max = min + 1;
|
if (!(max > min)) max = min + 1;
|
||||||
var padPrice = (max - min) * 0.08 || 0.8;
|
var padPrice = (max - min) * 0.06 || 0.5;
|
||||||
min -= padPrice;
|
min -= padPrice;
|
||||||
max += padPrice;
|
max += padPrice;
|
||||||
|
|
||||||
@@ -95,8 +94,7 @@
|
|||||||
return padL + i * slot + slot * 0.5;
|
return padL + i * slot + slot * 0.5;
|
||||||
};
|
};
|
||||||
|
|
||||||
// K 线
|
var bodyW = Math.max(1, Math.min(6, slot * 0.6));
|
||||||
var bodyW = Math.max(1.5, Math.min(8, slot * 0.55));
|
|
||||||
bars.forEach(function (b, i) {
|
bars.forEach(function (b, i) {
|
||||||
var x = xOf(i);
|
var x = xOf(i);
|
||||||
var up = b.close >= b.open;
|
var up = b.close >= b.open;
|
||||||
@@ -114,6 +112,7 @@
|
|||||||
ctx.fillRect(x - bodyW / 2, top, bodyW, bh);
|
ctx.fillRect(x - bodyW / 2, top, bodyW, bh);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 仅圆点,不画横线/竖线/价签(价签在图例外)
|
||||||
var entryColor = "#4cc2ff";
|
var entryColor = "#4cc2ff";
|
||||||
var exitColor = "#f0b429";
|
var exitColor = "#f0b429";
|
||||||
var entryIdx = nearestBarIndex(bars, markers.openTs);
|
var entryIdx = nearestBarIndex(bars, markers.openTs);
|
||||||
@@ -124,109 +123,65 @@
|
|||||||
markers.closePrice != null &&
|
markers.closePrice != null &&
|
||||||
exitIdx >= 0;
|
exitIdx >= 0;
|
||||||
|
|
||||||
var openP = markers.openPrice;
|
function drawDot(i, price, color) {
|
||||||
var closeP = markers.closePrice;
|
if (i < 0 || price == null) return { x: 0, y: 0 };
|
||||||
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;
|
|
||||||
var x = xOf(i);
|
var x = xOf(i);
|
||||||
var y = yOf(price);
|
var y = yOf(price);
|
||||||
ctx.save();
|
|
||||||
ctx.beginPath();
|
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.fillStyle = color;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
ctx.strokeStyle = "#0e0e18";
|
ctx.strokeStyle = "#ffffff";
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
return { x: x, y: y };
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var closeTogether =
|
var pe = null;
|
||||||
hasExit && entryIdx >= 0 && Math.abs(exitIdx - entryIdx) <= 2;
|
var px = null;
|
||||||
|
if (entryIdx >= 0 && markers.openPrice != null) {
|
||||||
if (entryIdx >= 0 && openP != null) {
|
pe = drawDot(entryIdx, markers.openPrice, entryColor);
|
||||||
drawDot(
|
|
||||||
entryIdx,
|
|
||||||
openP,
|
|
||||||
entryColor,
|
|
||||||
"入",
|
|
||||||
closeTogether ? "left" : "center"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (hasExit) {
|
if (hasExit) {
|
||||||
drawDot(
|
// 同一根或相邻:出场点略向右错开,避免叠成一团
|
||||||
exitIdx,
|
if (Math.abs(exitIdx - entryIdx) <= 1) {
|
||||||
closeP,
|
var x = xOf(exitIdx) + Math.max(6, slot * 0.8);
|
||||||
exitColor,
|
var y = yOf(markers.closePrice);
|
||||||
"出",
|
if (pe && Math.abs(y - pe.y) < 6) {
|
||||||
closeTogether ? "right" : "center"
|
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) {
|
document.querySelectorAll("canvas.chart-canvas").forEach(function (el) {
|
||||||
try {
|
try {
|
||||||
var bars = JSON.parse(el.dataset.bars || "[]");
|
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);
|
drawCandleChart(el, bars, readMarkers(el), tf);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
|
|||||||
@@ -63,12 +63,16 @@
|
|||||||
<div class="card" style="margin-top:14px">
|
<div class="card" style="margin-top:14px">
|
||||||
<h3>四周期 K 线(M1 / M5 / M15 / H1)</h3>
|
<h3>四周期 K 线(M1 / M5 / M15 / H1)</h3>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
客户端上报写入服务器;复盘读取。蓝=入场,黄=出场(价差很小时合并为一条「入/出」线)。图内只显示成交前后一段 K 线。
|
显示根数:M1 500 · M5 200 · M15 100 · H1 50(不足则有多少显多少)。
|
||||||
|
<span style="color:#4cc2ff">● 入场 {{ trade.open_price|fmt_price }}</span>
|
||||||
|
{% if trade.close_price is not none %}
|
||||||
|
· <span style="color:#f0b429">● 出场 {{ trade.close_price|fmt_price }}</span>
|
||||||
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<div class="chart-grid" style="margin-top:10px">
|
<div class="chart-grid" style="margin-top:10px">
|
||||||
{% for tf in ['M1','M5','M15','H1'] %}
|
{% for tf in ['M1','M5','M15','H1'] %}
|
||||||
<div class="chart-box">
|
<div class="chart-box">
|
||||||
<h4>{{ tf }}</h4>
|
<h4>{{ tf }} <span class="chart-meta muted" style="font-weight:400"></span></h4>
|
||||||
{% if charts[tf] %}
|
{% if charts[tf] %}
|
||||||
<canvas class="chart-canvas" id="chart-{{ tf }}"
|
<canvas class="chart-canvas" id="chart-{{ tf }}"
|
||||||
data-tf="{{ tf }}"
|
data-tf="{{ tf }}"
|
||||||
|
|||||||
@@ -1661,14 +1661,15 @@ class OrderApp(tk.Tk):
|
|||||||
def _hub_upload_bars(self, symbol: str) -> None:
|
def _hub_upload_bars(self, symbol: str) -> None:
|
||||||
if hub_client is None or not is_activated():
|
if hub_client is None or not is_activated():
|
||||||
return
|
return
|
||||||
|
# 与复盘显示根数对齐:M1 500 / M5 200 / M15 100 / H1 50
|
||||||
tf_map = {
|
tf_map = {
|
||||||
"M1": mt5.TIMEFRAME_M1,
|
"M1": (mt5.TIMEFRAME_M1, 500),
|
||||||
"M5": mt5.TIMEFRAME_M5,
|
"M5": (mt5.TIMEFRAME_M5, 200),
|
||||||
"M15": mt5.TIMEFRAME_M15,
|
"M15": (mt5.TIMEFRAME_M15, 100),
|
||||||
"H1": mt5.TIMEFRAME_H1,
|
"H1": (mt5.TIMEFRAME_H1, 50),
|
||||||
}
|
}
|
||||||
for name, tf in tf_map.items():
|
for name, (tf, count) in tf_map.items():
|
||||||
rates = mt5.copy_rates_from_pos(symbol, tf, 0, 400)
|
rates = mt5.copy_rates_from_pos(symbol, tf, 0, count)
|
||||||
if rates is None or len(rates) == 0:
|
if rates is None or len(rates) == 0:
|
||||||
continue
|
continue
|
||||||
bars = [
|
bars = [
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user