Files
atm-curs-dl/gen/strategii/m2d_exemplu.py
Claude Agent c564bf1be5 diagrame: trei desene care aratau altceva decat spune regula
m2d_exemplu (Riot): eticheta zicea 'close peste bear trail = iesi' pe o bara
care se inchidea SUB linie - doar fitilul trecea, adica exact cazul pe care
capitolul il interzice de trei ori. Seria refacuta, cu assert care recalculeaza
trail-ul din serie si cere ca bara de iesire sa inchida peste el.

mbp_exemplu: candela M se inchidea la 660.50 sub banda de 660.90, iar
generatorul chiar afirma relatia inversa ('assert BAND > c'); desenul arata un
setup invalid ca valid. Plus stopul 662.71, care plutea 1.71 deasupra extremei
celor 4 bare - exact marja pe care capitolul o declara absenta la MBP.
Ambele reparate, cu assert pe fiecare.

atmi_orizont: trei etichete purtau 'fisier:linie' in desen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-22 20:08:48 +00:00

387 lines
16 KiB
Python
Raw 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.

"""Diagrama din `gen/strategii/m2d_exemplu.spec.txt` (TIP: PRET).
Exemplul real Riot din curs (`14_strategia_1_m2d.md:227-236`): SHORT pe TF mic
3 min, cu TF mare 15 min în Sell. Pullback-ul Buy de pe TF mic (M) e urmat de
primul Sell (A = trigger); SL = cel mai sus high din 4 candele = +0.85%,
TP1 = 1:1 = -0.85%, iar ieșirea se face pe violarea bear trail-ului la +4.55%
(risc/recompensă efectiv ≈ 1:5.3).
Reale din curs: procentele 0.85% / 1.42% / 4.55% și contextul calitativ.
Construită de mine: axa de preț absolută, intrarea fixată la 100.00 ca reper.
Legenda spune explicit lucrul ăsta.
Serie continuă de 30 de candele din lista de close-uri din specificație,
open = close-ul precedentului, wick-uri alese ca să scoată exact nivelurile
(candela 13: high 100.85 = SL), panou de volum dedesubt.
Bear trail-ul e calculat din serie cu regula capitolului (linia sare pe
high-ul barei care se închide sub low-ul precedentei; violare = o bară care
se închide PESTE linie) și desenat ca scară portocalie. Seria e construită
astfel încât fitilele să atingă linia de trei ori (barele 16, 20, 24) fără
s-o violeze, iar ieșirea să vină abia la bara 29, care închide peste linie.
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Chart, Level, Mark, from_closes # noqa: E402
OUT = Path(__file__).resolve().parent / "m2d_exemplu.svg"
# ── nivelurile cerute de specificație ──────────────────────────────────────
ENTRY = 100.00 # close-ul candelei de trigger 14
SL = 100.85 # max high pe candelele 11:15 = +0.85%
TP1 = 99.15 # entry - 0.85% (1:1)
EXIT = 95.45 # close-ul candelei 29 = +4.55%
# ── seria: 30 de close-uri, exact ca în specificație ───────────────────────
closes = [
97.6, 97.1, 96.7, 96.3, 95.9, 95.6, 95.9, # 0-6 coborâre
96.4, 96.9, 97.5, 98.2, 99.0, 99.7, 100.3, # 7-13 pullback Buy (M)
100.00, # 14 trigger Sell (A)
99.45, 99.80, 98.58, 98.10, 97.60, 97.95, 97.40, 96.90, 96.40, 96.75,
96.20, 95.70, 95.20, 95.00, 95.45, # 15-29 bear trail
]
assert len(closes) == 30, len(closes)
# wick-uri: candela 13 are nevoie de un wick sus până la 100.85 (SL-ul de 4
# candele); barele 16, 20 și 24 sunt retestări ale liniei de trail, deci au
# fitil lung în sus care atinge linia, dar se închid sub ea. Restul sunt mici,
# ca seria să arate ca un grafic real.
wicks = [
(0.15, 0.20), (0.12, 0.18), (0.15, 0.15), (0.10, 0.20), (0.12, 0.25),
(0.10, 0.30), (0.25, 0.15),
(0.20, 0.15), (0.15, 0.20), (0.20, 0.15), (0.20, 0.20), (0.20, 0.18),
(0.25, 0.15), (SL - 100.3, 0.15), # 13: high = 100.85
(0.15, 0.25), # 14: trigger Sell
(0.15, 0.15), # 15
(0.45, 0.10), # 16 retest trail
(0.15, 0.15), # 17
(0.15, 0.15), # 18
(0.15, 0.15), # 19
(0.40, 0.10), # 20 retest trail
(0.15, 0.15), # 21
(0.15, 0.15), # 22
(0.15, 0.15), # 23
(0.40, 0.10), # 24 retest trail
(0.15, 0.15), # 25
(0.15, 0.15), # 26
(0.15, 0.15), # 27
(0.05, 0.05), # 28: coboară trail-ul
(0.20, 0.10), # 29: close peste trail
]
assert len(wicks) == 30, len(wicks)
vols = [
48, 45, 42, 40, 44, 50, 46,
42, 44, 40, 43, 47, 52, 68,
85,
60, 55, 52, 48, 46, 44, 42, 40, 43, 41, 44, 42, 40, 45, 72,
]
assert len(vols) == 30, len(vols)
cds = from_closes(closes, wicks=wicks, vols=vols, start=98.0)
# ── assert-urile cerute de specificație ────────────────────────────────────
# 1. entry == close-ul candelei de trigger 14
assert round(cds[14].c, 2) == ENTRY == 100.00, cds[14].c
# 2. SL == max high pe candelele 11:15
max_h = max(c.h for c in cds[11:15])
assert round(max_h, 2) == SL == 100.85, max_h
# 3. SL = +0.85% față de entry
assert abs((SL - ENTRY) / ENTRY - 0.0085) < 1e-4
# 4. TP1 = entry − (SL − entry) = 1:1
assert TP1 == round(ENTRY - (SL - ENTRY), 2) == 99.15, TP1
assert abs((ENTRY - TP1) / ENTRY - 0.0085) < 1e-4
# 5. exit == close-ul candelei 29
assert round(cds[29].c, 2) == EXIT == 95.45, cds[29].c
# 6. R efectiv ≈ 1:5.3
assert abs((ENTRY - EXIT) / (SL - ENTRY) - 5.35) < 0.05
# 7. niciun close după trigger nu trece peste SL
assert max(c.c for c in cds[15:]) < SL, max(c.c for c in cds[15:])
# 8. fiecare open == close-ul barei anterioare
for a, b in zip(cds, cds[1:]):
assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial între bare"
# 9. candela 14 închide în jos după pullback-ul Buy (7-13)
assert cds[14].c < cds[13].c, (cds[13].c, cds[14].c)
assert any(cds[i].c > cds[i].o for i in range(7, 14)), "pullback Buy absent"
# 10. bara 17 e la 1.42% profit (procentul real din curs)
assert abs((ENTRY - cds[17].c) / ENTRY - 0.0142) < 1e-4, cds[17].c
# ── bear trail, recalculat din serie cu regula capitolului ─────────────────
# Linia sare pe high-ul fiecărei bare care se închide sub low-ul precedentei;
# violarea = prima bară care se închide PESTE linie (fitilul singur nu scoate).
def bear_trail(candles, trig):
"""linie[i] = nivelul trail-ului în vigoare la începutul barei i (None până
se formează linia); iesire = prima bară închisă peste linie."""
linie, linie_la, iesire = None, [], None
for i, c in enumerate(candles):
linie_la.append(linie)
if i > trig and linie is not None and c.c > linie:
iesire = i
break
if i > trig:
p = candles[i - 1]
if c.c < p.l:
linie = c.h
while len(linie_la) < len(candles):
linie_la.append(linie)
return linie_la, iesire
LINIE, EXIT_BAR = bear_trail(cds, 14)
# 11. ieșirea e pe bara 29, care se închide PESTE linia de trail (nu doar fitilul)
assert EXIT_BAR == 29, f"ieșirea trebuie să fie pe bara 29, nu {EXIT_BAR}"
TRAIL_EXIT = LINIE[29]
assert TRAIL_EXIT is not None and cds[29].c > TRAIL_EXIT, (cds[29].c, TRAIL_EXIT)
# 12. linia nu urcă niciodată (e trail, nu nivel fix)
pas = [v for v in LINIE[15:30] if v is not None]
assert all(b <= a + 1e-9 for a, b in zip(pas, pas[1:])), pas
# 13. nicio bară dinainte de ieșire nu închide peste linie
for i in range(15, 29):
if LINIE[i] is not None:
assert cds[i].c < LINIE[i], (i, cds[i].c, LINIE[i])
# 14. fitilele au atins linia de mai multe ori fără s-o violeze pe close
atingeri = [i for i in range(15, 29) if LINIE[i] is not None
and cds[i].h >= LINIE[i] - 1e-9 and cds[i].c < LINIE[i]]
assert len(atingeri) >= 2, f"fitilele ating linia doar la barele {atingeri}"
assert set(atingeri) >= {16, 20, 24}, f"retestările lipsesc: {atingeri}"
# ── geometria, ca să așez marks-urile „below" fără suprapunere ─────────────
SCALE = 42
levels = [
Level(SL, "SL 100.85 = +0.85%", "#c00"),
Level(ENTRY, "intrare 100.00 (trigger Sell)", "#333"),
Level(TP1, "TP1 99.15 = 1:1", "#1a5fb4"),
Level(EXIT, "ieșire 95.45 = +4.55% ≈ 1:5.3", "#188a3e"),
]
prices = [p for c in cds for p in (c.o, c.h, c.l, c.c)] + [lv.price for lv in levels]
pmax, pmin = max(prices), min(prices)
top = 34 + 22 # implicit + titlu, fără segment de trend
body_bottom = top + (pmax - pmin) * SCALE
def y(price: float) -> float:
return top + (pmax - price) * SCALE
# ── așezarea etichetelor: caut un y unde textul nu atinge vreun corp de
# candelă și nu se suprapune cu altă etichetă. Sub grafic sunt doar ~14 px
# liberi (body_bottom .. body_bottom+14), deci etichetele lungi trebuie
# coborâte/diferite pe rânduri. Fără asta, „A" și „M" trec peste candele.
GAP, LEFT, WIDTH, BODY_W = 15, 44, 700, 10
def cx(i: int) -> float:
return LEFT + i * GAP
def text_x(i: int, text: str) -> tuple[float, float]:
"""(x0, x1) al textului bold ancorat pe candela i, ca în svg_candles."""
x = cx(i)
w = len(text) * 7.44
if x < 90:
return x - 12, x - 12 + w
if x > WIDTH - 90:
return x + 12 - w, x + 12
return x - w / 2, x + w / 2
corpuri = []
for i, c in enumerate(cds):
y0, y1 = y(max(c.o, c.c)), y(min(c.o, c.c))
y1 = max(y1, y0 + 1.6)
corpuri.append((cx(i) - BODY_W / 2, cx(i) + BODY_W / 2, y0, y1))
linii = [y(lv.price) for lv in levels]
def liber_fara_puse(i: int, text: str, yv: float) -> bool:
x0, x1 = text_x(i, text)
ty0, ty1 = yv - 10, yv + 3
if x0 < -2 or x1 > WIDTH + 2 or yv < top + 12 or yv > body_bottom + 14:
return False
for bx0, bx1, by0, by1 in corpuri:
if x0 < bx1 - 2 and bx0 < x1 - 2 and ty0 < by1 - 2 and by0 < ty1 - 2:
return False
# nicio linie de nivel (SL/entry/TP1/exit) prin mijlocul textului
for ly in linii:
if ty0 - 1 <= ly <= ty1 + 1:
return False
return True
def conflict(a: tuple, b: tuple) -> bool:
"""Două etichete se bat dacă au aceeași bandă de y și se suprapun pe x."""
ax0, ax1, ay = a
bx0, bx1, by = b
return abs(ay - by) < 13 and ax0 < bx1 - 2 and bx0 < ax1 - 2
puse: list[tuple] = []
definiții = [
(0, "TF mare (15 min) în Sell", "#c00", "above"),
(7, "M: Buy Momentum (pullback)", "#0891b2", "above"),
(14, "A: primul Sell = trigger, intri short", "#c00", "below"),
(17, "deja peste TP1 (1.42% din curs)", "#188a3e", "below"),
(29, "close peste bear trail = ieși", "#188a3e", "below"),
]
def natural_y(i: int, where: str) -> float:
# „below": sub candelă, dar înainte de panoul de volum. Plafonul e cu 1 px
# sub limita de jos a candidaților, altfel când bara i are chiar minimul
# seriei nu mai rămâne niciun y întreg de testat și plasarea pică.
return (y(cds[i].h) - 10 if where == "above"
else min(y(cds[i].l) + 20, body_bottom + 13))
# candidați: toate y-urile care nu ating niciun corp de candelă, ordonate
# după cât de aproape sunt de poziția naturală (cu preferință pentru partea
# cerută de specificație: „above" în sus, „below" în jos)
candidati = []
for i, text, _color, where in definiții:
nat = natural_y(i, where)
ys = [yy for yy in range(int(top + 12), int(body_bottom + 14) + 1)
if liber_fara_puse(i, text, yy)]
if where == "below":
# o etichetă „below" rămâne sub candelă: nu urcă peste preț
ys = [yy for yy in ys if yy >= nat]
ys.sort(key=lambda yv: yv - nat)
else:
# „above" preferă în sus, dar dacă nu încape coboară sub serie
ys.sort(key=lambda yv: (abs(yv - nat), 0 if yv <= nat else 1))
candidati.append(ys)
assert ys, f"nicio poziție liberă pentru {text!r}"
# backtracking: plasez întâi etichetele cu cele mai puține opțiuni
ordine = sorted(range(len(definiții)), key=lambda k: len(candidati[k]))
y_final = [None] * len(definiții)
def rezolva(k: int) -> bool:
if k == len(ordine):
return True
idx = ordine[k]
ci = definiții[idx][0]
for yv in candidati[idx]:
box = (*text_x(ci, definiții[idx][1]), yv)
if all(not conflict(box, p) for p in puse):
puse.append(box)
y_final[idx] = yv
if rezolva(k + 1):
return True
puse.pop()
return False
assert rezolva(0), "nu găsesc o așezare fără suprapuneri pentru etichete"
marks = [Mark(i, text, color, where, dy=y_final[k] - natural_y(i, where))
for k, (i, text, color, where) in enumerate(definiții)]
assert all(y_final), y_final
ch = Chart(
candles=cds,
levels=levels,
marks=marks,
scale=SCALE, gap=GAP, body_w=BODY_W, left=LEFT, width=WIDTH, vol_h=70,
vol_label="volum",
title="RIOT — 3 min (TF mare 15 min în Sell)",
caption="Axa de preț e construită (intrarea = 100.00); procentele sunt reale din curs.",
)
svg = ch.render()
assert "<svg" in svg and svg.count("\n\n") == 0, "linie goală în SVG"
# ── linia de trail, desenată ca scară portocalie ───────────────────────────
# Nivelul desenat la bara i e cel în vigoare după bara i (linia sare pe bara
# care a închis sub low-ul precedentei). Eticheta stă la stânga primului
# palier, ca să nu treacă peste fitilul lung al barei 13.
def nivel_desen(i: int):
return LINIE[i + 1] if i + 1 < len(LINIE) else LINIE[i]
trail_parts, prev_y = [], None
for i in range(15, len(cds)):
v = nivel_desen(i)
if v is None:
continue
x0, x1, yv = cx(i) - GAP / 2, cx(i) + GAP / 2, y(v)
trail_parts.append(
f'<line x1="{x0:.1f}" y1="{yv:.1f}" x2="{x1:.1f}" y2="{yv:.1f}" '
f'stroke="#b06000" stroke-width="2"/>')
if prev_y is not None and abs(prev_y - yv) > 1e-9:
trail_parts.append(
f'<line x1="{x0:.1f}" y1="{prev_y:.1f}" x2="{x0:.1f}" y2="{yv:.1f}" '
f'stroke="#b06000" stroke-width="1.2" stroke-dasharray="2 2"/>')
prev_y = yv
trail_parts.append(
f'<text x="{cx(13) - 6:.1f}" y="{y(nivel_desen(15)) - 14:.1f}" fill="#b06000" '
f'text-anchor="end" style="font-weight:bold">bear trail</text>')
svg = svg.replace("\n</svg>", "\n " + "\n ".join(trail_parts) + "\n</svg>")
# ── verificare: fiecare etichetă e în pânză și nu se suprapune alta ────────
m = re.search(r'viewBox="0 0 (\d+) (\d+)"', svg)
W, H = int(m.group(1)), int(m.group(2))
texte = re.findall(r'<text x="([\d.]+)" y="([\d.]+)"([^>]*)>([^<]*)</text>', svg)
cutii, cutii_text = [], []
for xs, ys, attrs, continut in texte:
x, yv = float(xs), float(ys)
w = len(continut) * (7.44 if "bold" in attrs else 6.72)
if 'text-anchor="end"' in attrs:
x0 = x - w
elif 'text-anchor="middle"' in attrs:
x0 = x - w / 2
else:
x0 = x
assert -2 <= x0 and x0 + w <= W + 2, f"etichetă în afara pânzei: {continut!r}"
assert 0 <= yv <= H, f"etichetă în afara înălțimii: {continut!r}"
cutii.append((x0, x0 + w, yv, continut))
cutii_text.append((x0, x0 + w, yv - 10, yv + 3, continut))
for i in range(len(cutii)):
for j in range(i + 1, len(cutii)):
a, b = cutii[i], cutii[j]
if abs(a[2] - b[2]) < 13 and a[0] < b[1] - 2 and b[0] < a[1] - 2:
raise AssertionError(f"texte suprapuse: {a[3]!r} / {b[3]!r}")
# ── verificare: nicio etichetă peste un corp de candelă ────────────────────
corpuri = []
for mm in re.finditer(
r'<rect x="([\d.]+)" y="([\d.]+)" width="10" height="([\d.]+)" '
r'fill="#[0-9a-f]{3,6}"/>', svg):
x0, y0, hh = map(float, mm.groups())
corpuri.append((x0, x0 + 10, y0, y0 + hh))
for tx0, tx1, ty0, ty1, continut in cutii_text:
for cx0, cx1, cy0, cy1 in corpuri:
if tx0 < cx1 - 2 and cx0 < tx1 - 2 and ty0 < cy1 - 2 and cy0 < ty1 - 2:
raise AssertionError(
f"etichetă peste candelă: {continut!r} la y~{ty0:.0f} "
f"({tx0:.0f}-{tx1:.0f}) vs rect {cx0:.0f}-{cx1:.0f} "
f"y {cy0:.0f}-{cy1:.0f}")
# legenda mai spune explicit că axa e construită (cerință de fidelitate)
assert "construită" in svg and "procentele sunt reale" in svg, \
"lipsește nota despre axa construită"
OUT.write_text(svg, encoding="utf-8")
print(f"serie {len(cds)} candele | intrare {ENTRY:.2f} @ bara 14 | "
f"SL {SL:.2f} (+0.85%) | TP1 {TP1:.2f} (1:1) | "
f"ieșire {EXIT:.2f} (+4.55%, R {(ENTRY - EXIT) / (SL - ENTRY):.2f})")
print(f"scris {OUT}")