Ansamblul arata acum 15 min si 3 min ca descompunere reala (k=5, cu assert), cu caseta punctata, ghidaje si nivelul comun legat prin conector — sablonul din buy_doji_perechi_tf. Noi, pentru pasii care nu aveau diagrama: directie (15 min ramane in Buy vs. se inchide Sell), trigger (bara inchisa peste capat vs. atinsa doar cu fitilul, plus al doilea semnal marcat "nu mai intri"), stop (cele 4 bare si alternativa 2xATR, cu cifrele SPY din curs), iesire (TP1 plus bull trail, long si short, oglinda calculata nu desenata din ochi). Secventa era din polilinii desenate de mana — rescrisa pe serie. Stopul e fix pe extrema celor 4 bare, fara marja: asa spune cursul aici, spre deosebire de 01_buy_doji. Diferenta trebuie spusa in text. Textul capitolului e neatins — agentul a atins pragul de context. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
391 lines
18 KiB
Python
391 lines
18 KiB
Python
#!/usr/bin/env python3
|
||
"""M2D — tot trade-ul, pe perechea 15 min → 3 min, cu lupă.
|
||
|
||
Pentru `summaries/17_STRATEGII/03_m2d.md`, marcajul `<!-- DIAGRAMA: ansamblu -->`.
|
||
|
||
Tiparul e cel din `buy_doji_perechi_tf.py`: panoul din dreapta NU e o serie
|
||
care vine după cea din stânga — e **descompunerea reală** a ultimelor 4
|
||
candele de 15 minute. O candelă de 15 min = k = 5 candele de 3 min, cu
|
||
open, close, max(high) și min(low) care coincid, verificat bară cu bară.
|
||
|
||
Sursa de adevăr e seria fină (`FINE`, 3 min). Candelele de 15 min din zona
|
||
mărită se CALCULEAZĂ din ea prin agregare; barele de dinainte (`BASE`) sunt
|
||
date separat și se leagă de lupă prin close == open.
|
||
|
||
Toate cifrele din desen ies din serii. Regula de stop din tot capitolul:
|
||
stopul stă FIX pe extrema ultimelor 4 candele de pe TF mic, fără marjă.
|
||
|
||
Rulare: python3 gen/strategii/m2d_ansamblu.py
|
||
"""
|
||
from pathlib import Path
|
||
|
||
OUT = Path(__file__).with_suffix(".svg")
|
||
W = 720
|
||
K = 5 # 5 candele de 3 min într-una de 15 min
|
||
NZ = 4 # câte candele de 15 min sunt mărite
|
||
STOP_BARS = 4 # stopul = extrema ultimelor 4 bare de pe TF mic
|
||
PULLBACK_N = 9 # barele 0..8 = pullback-ul (M)
|
||
TRIG = 9 # bara de trigger (A) pe TF mic
|
||
|
||
# --- seria fină (3 min), 20 de bare: (open, high, low, close) --------------
|
||
FINE = [
|
||
(100.60, 100.70, 100.40, 100.45), (100.45, 100.50, 100.25, 100.30),
|
||
(100.30, 100.35, 100.15, 100.20), (100.20, 100.25, 100.05, 100.10),
|
||
(100.10, 100.15, 100.00, 100.05), (100.05, 100.10, 99.95, 100.00),
|
||
(100.00, 100.05, 99.92, 99.95), (99.95, 100.00, 99.90, 99.98),
|
||
(99.98, 100.08, 99.94, 100.02), # 8: capătul pullback-ului
|
||
(100.02, 100.45, 100.00, 100.40), # 9: A = trigger, intri
|
||
(100.40, 100.60, 100.35, 100.55), (100.55, 100.75, 100.50, 100.70),
|
||
(100.70, 101.00, 100.65, 100.95), # 12: TP1 atins
|
||
(100.95, 101.10, 100.88, 101.05), (101.05, 101.15, 100.95, 101.00),
|
||
(101.00, 101.40, 100.95, 101.35), (101.35, 101.55, 101.25, 101.45),
|
||
(101.45, 101.50, 101.20, 101.25),
|
||
(101.25, 101.30, 101.05, 101.10), # 18: close sub trail = ieși
|
||
(101.10, 101.15, 100.90, 100.95),
|
||
]
|
||
|
||
# --- barele de 15 min dinaintea lupei; ultima poartă semnalul Buy Momentum --
|
||
BASE = [
|
||
(98.00, 98.35, 97.85, 98.25), (98.25, 98.70, 98.15, 98.60),
|
||
(98.60, 99.10, 98.50, 99.00), (99.00, 99.45, 98.90, 99.30),
|
||
(99.30, 99.95, 99.20, 99.85),
|
||
(99.85, 100.75, 99.80, 100.60), # 5: Buy Momentum pe 15 min
|
||
]
|
||
SIG_B = 5 # candela de 15 min cu semnalul
|
||
|
||
|
||
def aggregate(fine, k, n):
|
||
out_ = []
|
||
for j in range(n):
|
||
part = fine[j * k:(j + 1) * k]
|
||
out_.append((part[0][0], max(b[1] for b in part),
|
||
min(b[2] for b in part), part[-1][3]))
|
||
return out_
|
||
|
||
|
||
def eq(a, b):
|
||
return abs(a - b) < 0.005
|
||
|
||
|
||
ZOOM = aggregate(FINE, K, NZ)
|
||
BIG = BASE + ZOOM
|
||
|
||
# ---------------------------------------------------------------- verificări
|
||
for name, bars in (("15 min", BIG), ("3 min", FINE)):
|
||
for i, (o, h, l, c) in enumerate(bars):
|
||
assert h >= max(o, c) and l <= min(o, c), f"{name}[{i}]: high/low incoerent"
|
||
if i:
|
||
assert eq(o, bars[i - 1][3]), f"{name}[{i}]: open != close precedent"
|
||
assert len(FINE) == K * NZ, len(FINE)
|
||
|
||
# (1) unde cad granițele candelelor de 15 min peste poveste — exact ce spune
|
||
# textul capitolului, și poate fi fals
|
||
span = len({i // K for i in range(PULLBACK_N)})
|
||
assert span == 2, f"pullback-ul se întinde peste {span} candele de 15 min, nu 2"
|
||
trig_big = TRIG // K
|
||
assert trig_big == 1, f"trigger-ul cade în candela mare {trig_big}"
|
||
assert eq(FINE[0][0], BASE[SIG_B][3]), "open-ul primei bare fine != close-ul semnalului"
|
||
|
||
# (2) pullback-ul (M): coboară, dar NU rupe low-ul candelei de 15 min din care
|
||
# a pornit mișcarea — definiția ancorată din mini-glosar
|
||
PULL = FINE[:PULLBACK_N]
|
||
ref_low = BASE[SIG_B][2]
|
||
pull_low = min(b[2] for b in PULL)
|
||
assert PULL[0][3] > PULL[-1][3], "pullback-ul nu coboară"
|
||
assert pull_low > ref_low, f"pullback-ul rupe low-ul de referință ({pull_low} <= {ref_low})"
|
||
|
||
# (3) trigger-ul (A). Săgeata vine dintr-un indicator proprietar, deci forma
|
||
# ei pe desen e construită de mine: bara de trigger e PRIMA din pullback
|
||
# care se închide peste high-urile ultimelor LOOK bare. Verific că e
|
||
# într-adevăr prima — altfel desenul ar contrazice regula „primul semnal".
|
||
LOOK = 3
|
||
|
||
|
||
def rupe(i):
|
||
return i >= LOOK and FINE[i][3] > max(b[1] for b in FINE[i - LOOK:i])
|
||
|
||
|
||
assert rupe(TRIG), "bara de trigger nu rupe high-urile barelor dinainte"
|
||
assert not any(rupe(i) for i in range(LOOK, TRIG)), \
|
||
"o bară dinaintea trigger-ului rupea deja — nu mai e primul semnal"
|
||
|
||
# (4) trade-ul
|
||
ENTRY = FINE[TRIG][3]
|
||
SL = min(b[2] for b in FINE[TRIG - STOP_BARS + 1:TRIG + 1])
|
||
RISK = round(ENTRY - SL, 2)
|
||
TP1 = round(ENTRY + RISK, 2)
|
||
assert eq(SL, pull_low), "stopul nu e minimul pullback-ului"
|
||
assert eq(SL, min(b[2] for b in FINE)), "stopul nu e minimul zonei mărite"
|
||
TP_BAR = next(i for i in range(TRIG + 1, len(FINE)) if FINE[i][1] >= TP1 - 1e-9)
|
||
assert TP_BAR == 12, TP_BAR
|
||
|
||
# (5) bull trail: low-ul ultimei bare care s-a închis peste high-ul precedentei;
|
||
# ieșirea = prima bară care se ÎNCHIDE sub linie
|
||
trail, EXIT_BAR = None, None
|
||
for i in range(1, len(FINE)):
|
||
if trail is not None and FINE[i][3] < trail:
|
||
EXIT_BAR = i
|
||
break
|
||
if i > TRIG and FINE[i][3] > FINE[i - 1][1]:
|
||
trail = FINE[i][2]
|
||
TRAIL = trail
|
||
EXIT_BAR = EXIT_BAR if EXIT_BAR is not None else len(FINE) - 1
|
||
EXIT = FINE[EXIT_BAR][3]
|
||
assert (TRAIL, EXIT_BAR) == (101.25, 18), (TRAIL, EXIT_BAR)
|
||
assert EXIT > ENTRY, "ieșirea pe trail e sub intrare"
|
||
R = (EXIT - ENTRY) / RISK
|
||
assert round(R, 1) == 1.4, R
|
||
# wick-uri sub linie nu scot: bara 17 coboară sub trail, dar se închide deasupra
|
||
assert FINE[17][2] < TRAIL <= FINE[17][3], "bara 17 nu mai ilustrează wick sub trail"
|
||
|
||
LEVEL = BASE[SIG_B][3] # nivelul comun celor două panouri
|
||
assert eq(LEVEL, FINE[0][0])
|
||
|
||
# ---------------------------------------------------------------- desen
|
||
GREEN, RED, INK, BLUE, GRAY = "#188a3e", "#c00", "#333", "#1a5fb4", "#888"
|
||
LINK, CYAN = "#aaa", "#0891b2"
|
||
PLOT_H = 168
|
||
BX0, BGAP, BW = 104, 15, 9
|
||
SX0, SGAP, SW = 372, 11, 7
|
||
FRAME_L = SX0 - 8
|
||
COL_X = 600
|
||
MIDX = FRAME_L - 20
|
||
PAD = 0.06
|
||
|
||
out, LABELS = [], []
|
||
|
||
|
||
def text(x, y, s, fill=INK, anchor="start", bold=False, size=12):
|
||
st = [f"font-size:{size}px"] + (["font-weight:bold"] if bold else [])
|
||
a = f' text-anchor="{anchor}"' if anchor != "start" else ""
|
||
w = len(s) * (5.7 if size >= 12 else size * 0.5)
|
||
x0 = x if anchor == "start" else (x - w / 2 if anchor == "middle" else x - w)
|
||
assert 4 <= x0 and x0 + w <= W - 4, f"text iese din viewBox: {s!r} ({x0:.0f}..{x0 + w:.0f})"
|
||
LABELS.append((x0, x0 + w, y, s))
|
||
out.append(f'<text x="{x:.1f}" y="{y:.1f}" fill="{fill}"{a} '
|
||
f'style="{";".join(st)}">{s}</text>')
|
||
|
||
|
||
def hline(x1, x2, y, color, dash, width=1.5):
|
||
out.append(f'<line x1="{x1:.1f}" y1="{y:.1f}" x2="{x2:.1f}" y2="{y:.1f}" '
|
||
f'stroke="{color}" stroke-width="{width}" stroke-dasharray="{dash}"/>')
|
||
|
||
|
||
def line(x1, y1, x2, y2, color, dash="", width=1.2, marker=""):
|
||
d = f' stroke-dasharray="{dash}"' if dash else ""
|
||
m = f' marker-end="url(#{marker})"' if marker else ""
|
||
out.append(f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
|
||
f'stroke="{color}" stroke-width="{width}"{d}{m}/>')
|
||
|
||
|
||
def rect(x0_, y0_, x1, y1, color, fill_op=0.10, dash=""):
|
||
d = f' stroke-dasharray="{dash}"' if dash else ""
|
||
out.append(f'<rect x="{x0_:.1f}" y="{y0_:.1f}" width="{x1 - x0_:.1f}" '
|
||
f'height="{y1 - y0_:.1f}" fill="{color}" fill-opacity="{fill_op}" '
|
||
f'stroke="{color}" stroke-width="1.2"{d}/>')
|
||
|
||
|
||
def candles(bars, x0, gap, yf, bw):
|
||
for i, (o, h, l, c) in enumerate(bars):
|
||
cx = x0 + i * gap
|
||
color = GREEN if c >= o else RED
|
||
line(cx, yf(h), cx, yf(l), "#1a1a1a", width=1.2)
|
||
top_ = yf(max(o, c))
|
||
hh = max(yf(min(o, c)) - top_, 1.6)
|
||
out.append(f'<rect x="{cx - bw / 2:.1f}" y="{top_:.1f}" width="{bw}" '
|
||
f'height="{hh:.1f}" fill="{color}"/>')
|
||
|
||
|
||
def stack(items, x_line):
|
||
y_prev = None
|
||
for y, lines, fill in sorted(items, key=lambda t: t[0]):
|
||
y_lab = y if y_prev is None or y >= y_prev + 16 else y_prev + 16
|
||
if y_lab - y > 2:
|
||
out.append(f'<path d="M{x_line + 2:.1f},{y - 4:.1f} H{x_line + 7:.1f} '
|
||
f'L{COL_X - 4:.1f},{y_lab - 4:.1f}" fill="none" stroke="{fill}" '
|
||
f'stroke-width="0.8" stroke-opacity="0.8"/>')
|
||
else:
|
||
line(x_line + 2, y - 4, COL_X - 4, y_lab - 4, fill, width=0.8)
|
||
for j, s in enumerate(lines):
|
||
text(COL_X, y_lab + j * 14, s, fill=fill, size=10)
|
||
y_prev = y_lab + (len(lines) - 1) * 14
|
||
|
||
|
||
def seg_dist(p, a, b):
|
||
(px, py), (ax, ay), (bx, by) = p, a, b
|
||
dx, dy = bx - ax, by - ay
|
||
t = 0.0 if dx == dy == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy)
|
||
/ (dx * dx + dy * dy)))
|
||
return ((px - ax - t * dx) ** 2 + (py - ay - t * dy) ** 2) ** 0.5
|
||
|
||
|
||
out.append('<defs><marker id="pt" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" '
|
||
'markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#c00"/></marker>'
|
||
'<marker id="pt-o" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" '
|
||
'markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#b06000"/></marker>'
|
||
'</defs>')
|
||
text(20, 24, "M2D long, tot trade-ul: 15 min dă direcția, 3 min dă intrarea",
|
||
bold=True, size=14)
|
||
text(20, 42, "Graficul din dreapta nu e altă bucată de piață: e aceeași, mărită. "
|
||
"O candelă de 15 min = 5 candele de 3 min.", fill="#555", size=11)
|
||
|
||
top = 80
|
||
text(20, top - 12, "15 min: direcția", fill="#555", size=11)
|
||
text(FRAME_L, top - 12, f"3 min: descompunerea ultimelor {NZ} candele de 15 min",
|
||
fill="#555", size=11)
|
||
|
||
zlo_, zhi_ = SL, max(b[1] for b in FINE)
|
||
pad = PAD * (zhi_ - zlo_)
|
||
lo, hi = zlo_ - pad, zhi_ + pad
|
||
|
||
bmax = max(b[1] for b in BIG)
|
||
bmin = min(min(b[2] for b in BIG), lo)
|
||
bs, ss = PLOT_H / (bmax - bmin), PLOT_H / (hi - lo)
|
||
yb = lambda p: top + (bmax - p) * bs # noqa: E731
|
||
ys = lambda p: top + (hi - p) * ss # noqa: E731
|
||
|
||
# ---- panoul de 15 min ------------------------------------------------------
|
||
sx_a, sx_b = BX0 + SIG_B * BGAP - 8, BX0 + SIG_B * BGAP + 8
|
||
rect(sx_a, yb(BASE[SIG_B][1]) - 5, sx_b, yb(BASE[SIG_B][2]) + 5, CYAN)
|
||
line(BX0 + SIG_B * BGAP, yb(BASE[SIG_B][2]) + 6, BX0 + SIG_B * BGAP, top + PLOT_H + 12,
|
||
CYAN, width=1)
|
||
text(BX0 + SIG_B * BGAP - 30, top + PLOT_H + 26, "Buy Momentum pe 15 min:",
|
||
fill=CYAN, anchor="middle", bold=True, size=11)
|
||
text(BX0 + SIG_B * BGAP - 30, top + PLOT_H + 39, "de aici ai voie doar long",
|
||
fill=CYAN, anchor="middle", size=11)
|
||
|
||
# caseta de lupă peste exact candelele de 15 min care se sparg în dreapta
|
||
lx0, lx1 = BX0 + len(BASE) * BGAP - 7, BX0 + (len(BIG) - 1) * BGAP + 8
|
||
ly0, ly1 = yb(hi), yb(lo)
|
||
rect(lx0, ly0, lx1, ly1, BLUE, fill_op=0.06, dash="4 3")
|
||
|
||
# minimul de referință al pullback-ului: trăiește pe TF-ul mare (pe panoul mic
|
||
# e sub scară), deci se marchează aici și se trimite acolo din etichetă
|
||
hline(BX0 - 14, lx1, yb(ref_low), "#555", "2 3", width=1.2)
|
||
text(24, yb(ref_low) + 12, "minimul din care a pornit", fill="#555", size=10)
|
||
text(24, yb(ref_low) + 26, f"mișcarea: {ref_low:.2f}", fill="#555", size=10)
|
||
|
||
hline(BX0 - 14, lx1, yb(LEVEL), LINK, "3 3", width=1.0)
|
||
candles(BIG, BX0, BGAP, yb, BW)
|
||
|
||
# ---- ghidajele de lupă -----------------------------------------------------
|
||
guides = [((lx1, ly0), (FRAME_L, top)), ((lx1, ly1), (FRAME_L, top + PLOT_H))]
|
||
for a_, b_ in guides:
|
||
line(a_[0], a_[1], b_[0], b_[1], BLUE, dash="5 3", width=1.1)
|
||
|
||
# ---- nivelul comun: conector între panouri, numărul scris o singură dată ---
|
||
ycb, ycs = yb(LEVEL), ys(LEVEL)
|
||
|
||
|
||
def clearance(mx):
|
||
conn = [((lx1, ycb), (mx, ycb)), ((mx, ycb), (mx, ycs)), ((mx, ycs), (FRAME_L, ycs))]
|
||
pts = [(sx + (ex - sx) * t / 24, sy + (ey - sy) * t / 24)
|
||
for (sx, sy), (ex, ey) in conn for t in range(25)]
|
||
return min(seg_dist(q, ga, gb) for q in pts if q[0] >= lx1 + 10 for ga, gb in guides)
|
||
|
||
|
||
mid = max(range(int(lx1) + 14, int(FRAME_L) - 13), key=clearance)
|
||
dmin = clearance(mid)
|
||
assert dmin > 10, f"conectorul trece la {dmin:.1f} px de un ghidaj"
|
||
out.append(f'<path d="M{lx1:.1f},{ycb:.1f} H{mid:.1f} V{ycs:.1f} H{FRAME_L:.1f}" '
|
||
f'fill="none" stroke="{LINK}" stroke-width="1.0" stroke-dasharray="3 3"/>')
|
||
tx = min(mid + 5, FRAME_L - 60)
|
||
text(tx, (ycb + ycs) / 2 + 4, f"{LEVEL:.2f}", fill="#777", size=10)
|
||
text(tx, (ycb + ycs) / 2 + 17, "același preț", fill="#999", size=10)
|
||
|
||
# ---- panoul de 3 min -------------------------------------------------------
|
||
y_e, y_sl, y_tp, y_tr = ys(ENTRY), ys(SL), ys(TP1), ys(TRAIL)
|
||
assert abs((y_sl - y_e) / (y_e - y_tp) - 1) < 1e-6, "SL-intrare != intrare-TP1 în px"
|
||
assert top + PLOT_H - y_sl > 6, "linia de stop e lipită de marginea cadrului"
|
||
frame_r = SX0 + (len(FINE) - 1) * SGAP + 8
|
||
rect(FRAME_L, top, frame_r, top + PLOT_H, BLUE, fill_op=0.03)
|
||
|
||
# zona de evidențiere peste pullback + trigger: acolo se decide intrarea
|
||
zx0 = SX0 - 7
|
||
zx1 = SX0 + TRIG * SGAP + 7
|
||
rect(zx0, ys(max(b[1] for b in FINE[:TRIG + 1])) - 5, zx1, ys(SL) + 5, GREEN)
|
||
|
||
hline(FRAME_L, frame_r, ycs, LINK, "3 3", width=1.0)
|
||
hline(FRAME_L, frame_r, y_tp, GREEN, "2 3")
|
||
hline(FRAME_L, frame_r, y_e, GREEN, "6 3")
|
||
hline(FRAME_L, frame_r, y_sl, RED, "6 3")
|
||
hline(FRAME_L, frame_r, y_tr, "#b06000", "2 2", width=1.2)
|
||
candles(FINE, SX0, SGAP, ys, SW)
|
||
|
||
stack([(y_tp + 4, [f"TP1 {TP1:.2f} = 1:1", "iese prima tranșă"], GREEN),
|
||
(y_tr + 4, [f"bull trail {TRAIL:.2f}", "urcă, nu coboară"], "#b06000"),
|
||
(y_e + 4, [f"intrare {ENTRY:.2f}", "close-ul trigger-ului"], GREEN),
|
||
(y_sl + 4, [f"stop {SL:.2f}", f"minimul a {STOP_BARS} bare"], RED)],
|
||
frame_r)
|
||
|
||
# paranteza pullback-ului, sub cadru
|
||
pxa, pxb = SX0 - 5, SX0 + (PULLBACK_N - 1) * SGAP + 5
|
||
out.append(f'<path d="M{pxa:.1f},{top + PLOT_H + 2:.1f} V{top + PLOT_H + 8:.1f} '
|
||
f'H{pxb:.1f} V{top + PLOT_H + 2:.1f}" fill="none" stroke="{RED}" stroke-width="1"/>')
|
||
text((pxa + pxb) / 2, top + PLOT_H + 22, f"M: pullback, {PULLBACK_N} bare",
|
||
fill=RED, anchor="middle", bold=True, size=11)
|
||
text((pxa + pxb) / 2, top + PLOT_H + 36, "fără să rupă minimul",
|
||
fill=RED, anchor="middle", size=11)
|
||
|
||
# ieșirea: săgeată în sus spre bara 18, cu explicația sub cadru, la dreapta
|
||
exx = SX0 + EXIT_BAR * SGAP
|
||
line(exx, top + PLOT_H + 14, exx, ys(FINE[EXIT_BAR][2]) + 5, "#b06000",
|
||
dash="3 3", width=1.1, marker="pt-o")
|
||
text(frame_r, top + PLOT_H + 22, "close sub trail",
|
||
fill="#b06000", anchor="end", bold=True, size=11)
|
||
text(frame_r, top + PLOT_H + 36, f"ieșire {EXIT:.2f} = {R:.1f}R",
|
||
fill="#b06000", anchor="end", size=11)
|
||
|
||
# trigger-ul: săgeată în sus spre bara 9, cu explicația dedesubt
|
||
tgx = SX0 + TRIG * SGAP
|
||
line(tgx, top + PLOT_H + 50, tgx, ys(FINE[TRIG][2]) + 5, GREEN, width=1.6, marker="pt")
|
||
text(pxa, top + PLOT_H + 58, "A: primul Buy după pullback — aici intri,",
|
||
fill=GREEN, bold=True, size=11)
|
||
text(pxa, top + PLOT_H + 72, "pentru că 15 min e încă în Buy.", fill=GREEN, size=11)
|
||
text(pxa, top + PLOT_H + 86, "Wick-urile nu contează: se citește close-ul.",
|
||
fill="#555", size=11)
|
||
|
||
LAB_N = len(LABELS) # etichetele de pe panouri; ce urmează e subsol
|
||
H = top + PLOT_H + 108
|
||
text(20, H - 6, f"Serie construită, fără ticker. Risc = {ENTRY:.2f} − {SL:.2f} = {RISK:.2f}; "
|
||
f"TP1 la aceeași distanță în sus; ieșirea pe trail la {R:.1f}R.",
|
||
fill="#555", size=11)
|
||
H += 12
|
||
|
||
# ---- prețul fiecărui panou rămâne pe panoul lui ---------------------------
|
||
COMUNE = {round(LEVEL, 2)}
|
||
MARE = {"ref_low": ref_low}
|
||
MIC = {k_: v for k_, v in (("entry", ENTRY), ("sl", SL), ("tp", TP1), ("trail", TRAIL))
|
||
if round(v, 2) not in COMUNE}
|
||
for x0_, _x1, _y, s_ in LABELS[:LAB_N]:
|
||
rau = MARE if x0_ >= MIDX else MIC
|
||
for kk, v in rau.items():
|
||
assert f"{v:.2f}" not in s_, \
|
||
f"eticheta {s_!r} de pe panoul {'mic' if x0_ >= MIDX else 'mare'} " \
|
||
f"conține prețul {kk}={v:.2f} al celuilalt panou"
|
||
cx_ = [x0_ for x0_, _x1, _y, s_ in LABELS[:LAB_N] if f"{LEVEL:.2f}" in s_]
|
||
assert len(cx_) == 1 and lx1 < cx_[0] < FRAME_L, \
|
||
f"{LEVEL:.2f} nu e scris exact o dată, în cotul conectorului ({cx_})"
|
||
for nume, v in (("intrarea", ENTRY), ("stopul", SL), ("TP1", TP1), ("trail-ul", TRAIL)):
|
||
assert any(f"{v:.2f}" in s_ and x0_ >= MIDX for x0_, _x1, _y, s_ in LABELS[:LAB_N]), \
|
||
f"{nume} nu apare pe panoul mic"
|
||
|
||
for a in range(len(LABELS)):
|
||
assert 0 < LABELS[a][2] < H, f"etichetă în afara înălțimii: {LABELS[a][3]!r}"
|
||
for b in range(a + 1, len(LABELS)):
|
||
xa0, xa1, ya_, sa = LABELS[a]
|
||
xb0, xb1, yb_, sb = LABELS[b]
|
||
assert not (abs(ya_ - yb_) < 13 and xa0 < xb1 - 4 and xb0 < xa1 - 4), \
|
||
f"etichete suprapuse: {sa!r} / {sb!r}"
|
||
|
||
svg = (f'<svg viewBox="0 0 {W} {H:.0f}" xmlns="http://www.w3.org/2000/svg" '
|
||
f'style="width:100%;height:auto;font:12px sans-serif">\n ' + "\n ".join(out) + "\n</svg>")
|
||
assert "\n\n" not in svg
|
||
assert ".md:" not in svg, "trimiteri la surse în SVG"
|
||
OUT.write_text(svg, encoding="utf-8")
|
||
print(f"scris {OUT} ({W}x{H:.0f}) | 15 min: {len(BIG)} candele, lupă pe ultimele {NZ} "
|
||
f"= {len(FINE)} bare de 3 min | nivel comun {LEVEL:.2f} | "
|
||
f"intrare {ENTRY:.2f} SL {SL:.2f} risc {RISK:.2f} TP1 {TP1:.2f} la bara {TP_BAR} | "
|
||
f"trail {TRAIL:.2f}, ieșire bara {EXIT_BAR} la {EXIT:.2f} = {R:.2f}R | "
|
||
f"pullback peste {span} candele de 15 min, trigger în candela mare {trig_big}")
|