Files
atm-curs-dl/gen/strategii/m2d_ansamblu.py
Claude Agent 0ed607134d m2d: diagramele refacute; ansamblul primeste corespondenta de TF
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
2026-09-20 12:45:14 +00:00

391 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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}")