"""Diagrama din `gen/strategii/mdsrt_exemplu.spec.txt` (TIP: PRET). Exemplul real din Q&A 15.07.2025 (`09_QA/24_2025-07-15.md`): SPY pe 5 min, double bottom cu al doilea picior mai jos (MDSRT pe 5 min, suportul = pivotul daily). Downtrend → picior 1 la 622.40 → revenire → picior 2 la 621.40 (rupe pivotul 622.00) → Buy Hammer care închide la 622.60, înapoi peste pivot. După trigger, un plan construit de mine (marcat) duce trade-ul până la TP1. Reale din curs: nivelurile 622.40 / 622.00 / 621.40 / 622.60 și cele 8 bare dintre picioare. Construcția mea: entry 622.60, SL 621.30, TP1 623.90 (1:1) — materialul nu le dă (transcrierea planului se întrerupe). Volumele sunt relative, alese de generator. Rulează: python3 gen/strategii/mdsrt_exemplu.py Scrie lângă el `mdsrt_exemplu.svg`. """ import re import sys from pathlib import Path sys.path.insert(0, "/workspace/atm-curs-dl") from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402 OUT = Path(__file__).resolve().parent / "mdsrt_exemplu.svg" GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" # ── nivelurile reale din curs + planul construit (marcat) ────────────────── PIVOT = 622.00 # pivot daily, suportul LEG1 = 622.40 # swing low picior 1 LEG2 = 621.40 # picior 2 (mai jos, rupe pivotul) ENTRY = 622.60 # close-ul Buy Hammer (construit) SL = 621.30 # 0.10 sub low-ul piciorului 2 (construit) TP1 = 623.90 # entry + risc = 1:1 (construit) # ── seria: 30 de close-uri, exact ca în specificație ─────────────────────── CLOSES = [ 627.00, 626.40, 626.70, 626.00, 625.40, 624.80, 624.40, 623.80, # 0-7 623.20, 622.80, # 8-9 622.40, # 10 = picior 1 (swing low 622.40) 622.70, 623.10, 623.50, 623.20, 622.90, 622.60, 622.30, # 11-17 622.60, # 18 = picior 2: low 621.40, Buy Hammer care închide 622.60 623.00, 623.40, 623.90, # 19-21, 21 = TP1 1:1 (construit) 624.20, 624.00, 624.40, 624.80, 625.20, 625.60, # 22-27 625.30, 625.70, # 28-29 ] assert len(CLOSES) == 30, len(CLOSES) # wick-uri (sus, jos): alternanță naturală; excepțiile sunt piciorul 1 și # hammer-ul de la piciorul 2 (cerute exact de specificație). WICKS = [ (0.10, 0.10), (0.12, 0.14), (0.15, 0.12), (0.18, 0.15), (0.20, 0.16), (0.18, 0.14), (0.16, 0.13), (0.15, 0.12), (0.14, 0.12), (0.12, 0.10), (0.15, 0.05), # 10 = picior 1: low 622.40 - 0.05 = 622.35 (0.12, 0.12), (0.15, 0.14), (0.18, 0.15), (0.16, 0.13), (0.14, 0.12), (0.12, 0.10), (0.10, 0.12), (0.10, 0.90), # 18 = hammer: high 622.70, low 621.40 (0.12, 0.12), (0.14, 0.12), (0.15, 0.13), (0.16, 0.15), (0.14, 0.12), (0.15, 0.13), (0.16, 0.14), (0.17, 0.14), (0.18, 0.15), (0.14, 0.12), (0.15, 0.13), ] assert len(WICKS) == 30, len(WICKS) # volum relativ (ales de generator; materialul nu dă volumul pentru exemplu): # scade pe downtrend, spike de absorbție pe hammer, higher low vs piciorul 1. VOLS = [ 58, 56, 54, 52, 50, 49, 48, 47, 46, 45, # 0-9 44, # 10 = picior 1 43, 42, 41, 40, 39, 38, 39, # 11-17 100, # 18 = hammer, maximul seriei 62, 58, 54, # 19-21 52, 50, 49, 48, 47, 46, 45, 44, # 22-29 ] assert len(VOLS) == 30, len(VOLS) cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=627.00) # ── assert-urile din specificație ────────────────────────────────────────── # 1. picior 1: low 622.35, close 622.40 assert abs(cds[10].l - 622.35) < 1e-9, cds[10].l assert cds[10].c == 622.40, cds[10].c # 2. picior 2 mai jos (621.40 < 622.35) și rupe pivotul daily 622.00 assert abs(cds[18].l - LEG2) < 1e-9, cds[18].l assert cds[18].l < cds[10].l, (cds[18].l, cds[10].l) assert cds[18].l < PIVOT, cds[18].l # 3. trigger-ul închide înapoi peste pivot assert cds[18].c == 622.60, cds[18].c assert cds[18].c > PIVOT, cds[18].c # 4. 8 candele între picioare (peste minimul de 5) assert 18 - 10 == 8 # 5. hammer: coadă lungă în jos (0.90) > corp (0.30) corp = abs(cds[18].o - cds[18].c) coada = min(cds[18].o, cds[18].c) - cds[18].l assert abs(corp - 0.30) < 1e-9, corp assert abs(coada - 0.90) < 1e-9, coada assert coada > corp, (coada, corp) # 6. entry = close-ul trigger-ului (construit), SL 0.10 sub low-ul piciorului 2 assert cds[18].c == ENTRY assert abs(cds[18].l - SL - 0.10) < 1e-9, (cds[18].l, SL) risk = ENTRY - SL assert abs(risk - 1.30) < 1e-9, risk # 7. TP1 simetric 1:1 assert abs((ENTRY - SL) - (TP1 - ENTRY)) < 1e-9 # 8. după trigger, primul close care atinge 623.90 e bara 21 (nu mai devreme). # Seria pornește la 627.00, deci „atinge" se citește de jos în sus, din zona # picioarelor: barele 10-20 rămân sub TP1, bara 21 îl atinge. assert all(c.c < TP1 for c in cds[10:21]), [c.c for c in cds[10:21]] assert max(c.h for c in cds[19:21]) < TP1, [c.h for c in cds[19:21]] assert cds[21].c == TP1, cds[21].c # 9. divergență: lower low pe preț, higher low pe volum assert cds[18].l < cds[10].l, (cds[18].l, cds[10].l) assert VOLS[18] > VOLS[10], (VOLS[18], VOLS[10]) assert VOLS[18] == max(VOLS) # 10. fiecare open == close-ul barei anterioare for a, b in zip(cds, cds[1:]): assert a.c == b.o, (a.c, b.o) # ── geometria, ca să așez marks-urile fără suprapunere de text ───────────── SCALE, GAP, LEFT, WIDTH, BODY_W, VOL_H = 62, 15, 44, 640, 9, 70 def cx(i: int) -> float: return LEFT + i * GAP levels = [ Level(PIVOT, "pivot daily 622.00", ORANGE, label_at="left"), Level(SL, "SL 621.30 (construit)", RED, label_at="left"), Level(ENTRY, "intrare 622.60 (construit)", GREEN, label_at="left"), Level(TP1, "TP1 623.90 = 1:1 (construit)", GREEN, label_at="left"), Level(LEG1, "picior 1 = 622.40", BLUE, label_at="right"), ] # dy-uri calculate manual ca etichetele „below" să nu calce linia pivotului # (bara 10) și ca „hammer" să nu calce eticheta intrării (bara 18). marks = [ Mark(10, "picior 1 = 622.40", BLUE, "below", dy=15), Mark(18, "picior 2 = 621.40 (mai jos) · Buy Hammer", RED, "below"), Mark(18, "hammer închide 622.60, înapoi peste pivot", GREEN, "above", dy=-20), ] ch = Chart( candles=cds, levels=levels, marks=marks, mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")], scale=SCALE, gap=GAP, body_w=BODY_W, left=LEFT, width=WIDTH, vol_h=VOL_H, vol_label="volum", title="SPY — 5 min (Q&A 15.07.2025; intrare/SL/TP construite)", caption="", ) svg = ch.render() # ── legenda de onestitate (assert 11): ce e real, ce e construit ─────────── legend = [ "Legendă: nivelurile 622.40 / 622.00 / 621.40 / 622.60 și cele 8 candele dintre", "picioare sunt reale, din curs. Intrarea 622.60, SL-ul 621.30 și TP-ul 623.90", "sunt construcția mea — materialul nu le dă.", ] m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg) W, H0 = int(m.group(1)), float(m.group(2)) legend_svg = [f'{t}' for i, t in enumerate(legend)] new_h = H0 + 16 + 17 * (len(legend) - 1) + 14 svg = re.sub(r'(viewBox="0 0 \d+ )[0-9.]+(")', lambda mm: mm.group(1) + f"{new_h:.0f}" + mm.group(2), svg, count=1) svg = svg.replace("\n", "\n " + "\n ".join(legend_svg) + "\n") assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown" assert "sunt reale, din curs" in svg and "construcția mea" in svg, \ "legenda nu spune explicit ce e real și ce e construit" # toate etichetele cerute de specificație sunt prezente for eticheta in ("pivot daily 622.00", "picior 1 = 622.40", "picior 2 = 621.40 (mai jos) · Buy Hammer", "hammer închide 622.60, înapoi peste pivot", "intrare 622.60 (construit)", "SL 621.30 (construit)", "TP1 623.90 = 1:1 (construit)"): assert eticheta in svg, f"lipsește eticheta: {eticheta!r}" assert "SPY — 5 min" in svg, "lipsește titlul" # 1:1 desenat la 1:1 (STYLE.md: raportul din preț = raportul din pixeli) ys = {p: ch._y(p) for p in (SL, ENTRY, TP1)} assert abs((ys[SL] - ys[ENTRY]) - (ys[ENTRY] - ys[TP1])) < 1e-6, ys # ── verificare: fiecare etichetă e în pânză și nu se suprapune alta ──────── m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg) W, H = int(m.group(1)), float(m.group(2)) texte = re.findall(r']*)>([^<]*)', svg) cutii = [] 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)) 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}") OUT.write_text(svg, encoding="utf-8") print(f"scris {OUT.name}: {len(cds)} candele | picior 1 {cds[10].l:.2f} (close " f"{cds[10].c:.2f}) | picior 2 {cds[18].l:.2f} (mai jos) | hammer închide " f"{cds[18].c:.2f} | intrare {ENTRY:.2f} | SL {SL:.2f} (risc {risk:.2f}) | " f"TP1 {TP1:.2f} = {(TP1 - ENTRY) / risk:.2f}:1 la bara 21 | " f"vol[18]={VOLS[18]} > vol[10]={VOLS[10]}")