#!/usr/bin/env python3 """Cum se masoara divergenta MDSRT — schema pe doua panouri (pret + indicator). Pentru `strategii/07_mdsrt.md`, marcajul `` (Nivel 1, §1.2 „Setup", dupa pasul 5). NU e grafic de pret cu candele: spec-ul cere o schema de regula, cu linii, fara axe de pret reale, fara ticker, fara cifre de pret. De aceea se scrie SVG de mana, doar stdlib — nu se folosesc svg_candles / pchart. Ce arata (toate etichetele si pragurile vin din spec / din Q&A): - panoul de sus: linia pretului intr-un downtrend; L1 primul swing low important, L2 al doilea, mai jos → lower low; - panoul de jos: linia indicatorului (Power / volum / RS) cu I1 si I2, al doilea mai sus → higher low = divergenta bull; - bracket orizontal intre L1 si L2: „minim 5 candele intre puncte"; - bracket orizontal in stanga lui L2, pana la 60 de pasi in spate: „maxim 60 de candele in spate"; - al treilea swing low mic, la 2-3 pasi de L2, taiat cu cruce rosie si etichetat „2-3 candele → NU e divergenta". Pragurile, cu sursa: - „ultimele 2 swing low-uri importante" + „60 de candele in spate si minim 5 candele intre divergente": `09_QA/24_2025-07-15.md:151-155`; - „Daca aveti pe 3 candele divergenta, nu e": `09_QA/23_2025-07-29.md:873-874`; - mecanismul pret-vs-volum (lower low pe pret, higher low pe indicator): `09_QA/19_2025-09-08.md:168-177`; - perechea de low-uri se muta pe masura ce apare un low nou, mai jos: `09_QA/24_2025-07-15.md:159-162`. Ruleaza: .venv/bin/python gen/strategii/mdsrt_masurare.py -> gen/strategii/mdsrt_masurare.svg """ from pathlib import Path from xml.sax.saxutils import escape OUT = Path(__file__).with_suffix(".svg") W, H = 700, 620 BLACK, GRAY, RED, GREEN, BLUE, ORANGE, WHITE = ( "#000000", "#777777", "#c00", "#188a3e", "#1a5fb4", "#b06000", "#ffffff") # ---- grila de timp: un pas = o candela. L2 e la 60 de pasi de la marginea # stanga a panoului, ca bracket-ul „maxim 60 de candele" sa aiba exact 60 de # pasi spre stanga si sa acopere si L1. X0, STEPW = 60.0, 7.0 L1_STEP, L2_STEP, L3_STEP = 54.0, 60.0, 62.5 X_L1, X_L2, X_L3 = X0 + L1_STEP * STEPW, X0 + L2_STEP * STEPW, X0 + L3_STEP * STEPW # ---- coordonatele verticale ale punctelor Y_L1, Y_L2, Y_L3 = 252.0, 278.0, 272.0 # pret: L2 mai jos (lower low) Y_I1, Y_I2 = 486.0, 468.0 # indicator: I2 mai sus (higher low) PRICE_BOX = (40.0, 76.0, 620.0, 250.0) # y 76 .. 326 IND_BOX = (40.0, 344.0, 620.0, 200.0) # y 344 .. 544 BR60_Y = 134.0 # bracket-ul de 60 de candele, sus BR5_Y = 300.0 # bracket-ul de 5 candele, sub cele doua low-uri TEXTS = [] # (x, y, text, size, weight, anchor) pentru verificarea finala def txt(x, y, s, size=13, weight="400", fill=BLACK, anchor="middle"): TEXTS.append((x, y, s, size, weight, anchor)) return (f'' f'{escape(s)}') def line(x1, y1, x2, y2, stroke=BLACK, width=1.4, dash=None): d = f' stroke-dasharray="{dash}"' if dash else "" return (f'') def poly(points, stroke=BLACK, width=1.8): pts = " ".join(f"{x:.1f},{y:.1f}" for x, y in points) return (f'') def dot(x, y, color=BLACK, r=3.6): return f'' def hbracket(x0, x1, y, color, tick=7.0): """Bracket orizontal de dimensiune: linie + capete verticale.""" return [ line(x0, y, x1, y, color, 1.4), line(x0, y - tick, x0, y + tick, color, 1.4), line(x1, y - tick, x1, y + tick, color, 1.4), ] def cross(cx, cy, color=RED, arm=10.0): """Crucea rosie care marcheaza al treilea low (NU e divergenta).""" return [ line(cx - arm, cy - arm, cx + arm, cy + arm, color, 2.0), line(cx - arm, cy + arm, cx + arm, cy - arm, color, 2.0), ] def build(): p = [ f'', '', f'', txt(W / 2, 34, "Divergența: cum o măsori corect", 19, "700", BLACK), txt(W / 2, 58, "Aceeași structură pe preț și pe indicator: două swing " "low-uri, două minime", 13, "400", GRAY), ] # ---------------- panoul de sus: pretul bx, by, bw, bh = PRICE_BOX p.append(f'') p.append(txt(bx + 16, by + 20, "PREȚ (schematic) — downtrend, lower low", 14, "700", BLACK, "start")) # bracket „maxim 60 de candele in spate": de la L2 spre stanga, 60 de pasi x60_left, x60_right = X0, X_L2 p.extend(hbracket(x60_left, x60_right, BR60_Y, BLACK)) p.append(txt((x60_left + x60_right) / 2, BR60_Y - 12, "maxim 60 de candele în spate", 13, "400", BLACK)) # linia pretului: downtrend cu L1, apoi L2 mai jos, apoi un low mic L3 price = [(0, 158), (6, 178), (12, 166), (18, 192), (24, 180), (30, 206), (36, 194), (42, 218), (48, 232), (L1_STEP, Y_L1), (57, 226), (L2_STEP, Y_L2), (61.3, 248), (L3_STEP, Y_L3), (65, 250)] p.append(poly([(X0 + s * STEPW, y) for s, y in price], BLACK, 1.8)) p.extend([dot(X_L1, Y_L1, BLACK), dot(X_L2, Y_L2, BLACK), dot(X_L3, Y_L3, BLACK)]) p.append(txt(X_L1 - 10, Y_L1 + 6, "L1", 13, "700", BLACK, "end")) p.append(txt(X_L2 - 12, Y_L2 + 8, "L2", 13, "700", BLACK, "end")) # bracket „minim 5 candele intre puncte": intre L1 si L2 p.extend(hbracket(X_L1, X_L2, BR5_Y, BLACK)) p.append(txt((X_L1 + X_L2) / 2, BR5_Y + 18, "minim 5 candele între puncte", 13, "400", BLACK)) # al treilea low: cruce rosie + „NU" + eticheta p.extend(cross(X_L3, Y_L3, RED, arm=10.0)) p.append(txt(X_L3, 234, "NU", 14, "700", RED)) p.append(txt(525, 246, "2-3 candele", 13, "700", RED, "start")) p.append(txt(525, 266, "NU e divergență", 13, "700", RED, "start")) # ---------------- panoul de jos: indicatorul bx, by, bw, bh = IND_BOX p.append(f'') p.append(txt(bx + 16, by + 20, "INDICATOR (Power / volum / RS) — higher low", 14, "700", BLUE, "start")) ind = [(0, 400), (6, 424), (12, 410), (18, 436), (24, 422), (30, 448), (36, 434), (42, 458), (48, 466), (L1_STEP, Y_I1), (57, 458), (L2_STEP, Y_I2), (65, 440)] p.append(poly([(X0 + s * STEPW, y) for s, y in ind], BLUE, 1.8)) p.extend([dot(X_L1, Y_I1, BLUE), dot(X_L2, Y_I2, BLUE)]) p.append(txt(X_L1 - 10, Y_I1 + 6, "I1", 13, "700", BLUE, "end")) p.append(txt(X_L2 + 10, Y_I2 + 6, "I2", 13, "700", BLUE, "start")) # ---------------- legenda de jos p.append(txt(W / 2, 570, "Prețul face un lower low (L2 sub L1), indicatorul " "face un higher low (I2 peste I1):", 13, "400", BLACK)) p.append(txt(W / 2, 590, "divergență bull. Dar dacă al doilea punct e la " "doar 2-3 candele, nu se numără.", 13, "400", BLACK)) p.append('') return "\n".join(p) + "\n" # ---------------------------------------------------------------- verificare def est_w(s, size, weight): factor = 0.62 if weight == "700" else 0.56 return len(s) * size * factor def verify(svg): # (1) geometria schemei, exact ce cere spec-ul assert Y_L2 > Y_L1, "L2 trebuie desenat mai jos decat L1 (lower low)" assert Y_I2 < Y_I1, "I2 trebuie desenat mai sus decat I1 (higher low)" step_l1_l2 = (X_L2 - X_L1) / STEPW step_l2_l3 = (X_L3 - X_L2) / STEPW assert step_l1_l2 >= 5.0 - 1e-9, f"prea putine candele L1-L2: {step_l1_l2}" assert 2.0 - 1e-9 <= step_l2_l3 <= 3.0 + 1e-9, f"L3 nu e la 2-3 pasi: {step_l2_l3}" # (4) bracket-ul de 60 de candele acopera de la L2 spre stanga si trece de L1 x60_left, x60_right = X0, X_L2 assert x60_right == X_L2, "bracket-ul de 60 porneste din L2" assert x60_left <= X_L1, "bracket-ul de 60 nu ajunge pana la L1" assert x60_right - x60_left >= (X_L2 - X_L1), "bracket prea scurt fata de L1" assert (x60_right - x60_left) / STEPW == 60.0, "nu sunt exact 60 de pasi" # (5) „NU" si crucea rosie chiar pe al treilea low assert "NU" in svg assert RED in svg and "L3" not in svg # L3 e marcat doar prin cruce, nu cu eticheta cross_lines = cross(X_L3, Y_L3) for ln in cross_lines: assert f'x1="{X_L3 - 10:.1f}"' in ln or f'x2="{X_L3 - 10:.1f}"' in ln assert f'y1="{Y_L3 - 10:.1f}"' in ln or f'y2="{Y_L3 - 10:.1f}"' in ln # etichetele obligatorii din spec sunt in desen for cheie in ("maxim 60 de candele în spate", "minim 5 candele între puncte", "2-3 candele", "NU e divergență", "lower low", "higher low", "L1", "L2", "I1", "I2"): assert cheie in svg, f"lipsește din desen: {cheie!r}" # (2) toate etichetele in panza for x, y, s, size, weight, anchor in TEXTS: ew = est_w(s, size, weight) left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle" else x) right = left + ew assert -2 <= left and right <= W + 2, f"text in afara panzei: {s!r}" assert 0 <= y <= H, f"text in afara inaltimii: {s!r} (y={y})" # (3) fara doua texte pe aceeasi pozitie for i in range(len(TEXTS)): xi, yi, si, szi, wi, ai = TEXTS[i] we = est_w(si, szi, wi) li = xi - we if ai == "end" else (xi - we / 2 if ai == "middle" else xi) ri = li + we for j in range(i + 1, len(TEXTS)): xj, yj, sj, szj, wj, aj = TEXTS[j] wej = est_w(sj, szj, wj) lj = xj - wej if aj == "end" else (xj - wej / 2 if aj == "middle" else xj) rj = lj + wej if abs(yi - yj) < 13 and li < rj - 4 and lj < ri - 4: raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}") # etichetele panourilor incap in panouri for x, y, s, size, weight, anchor in TEXTS: for (px, py, pw, ph) in (PRICE_BOX, IND_BOX): if py <= y <= py + ph and px - 2 <= x <= px + pw + 2: assert x >= px + 12, f"eticheta lipita de margine: {s!r}" assert est_w(s, size, weight) <= pw - 24, f"prea lata in panou: {s!r}" # fara linie goala in SVG assert "" not in svg.split("\n")[:-1], "SVG-ul are linie goala" assert "\n\n" not in svg, "SVG-ul are linie goala" return True SVG = build() assert verify(SVG), "verificare esuata" OUT.write_text(SVG, encoding="utf-8") print(f"scris {OUT} ({W}x{H}), verificare OK: {len(TEXTS)} etichete, " f"L1-L2={(X_L2 - X_L1) / STEPW:.1f} pasi, " f"L2-L3={(X_L3 - X_L2) / STEPW:.1f} pasi, " f"fereastra 60 = {(X_L2 - X0) / STEPW:.0f} pasi")