Cate un agent grafician per diagrama, fiecare cu generatorul si SVG-ul lui. Graficele de pret trec prin svg_candles, cu assert-urile din specificatie scrise in generator — verifica structura, nu doar ca ruleaza: ca al doilea picior e mai jos, ca intrarea e close-ul barei de confirmare, ca 1:1 e exact. Verificare prin sondaj pe buy_doji_ansamblu, cu screenshot citit: serie continua de 32 de candele, panou de volum, MA(20), doji-ul si ieșirea pe trail etichetate, cifrele identice cu specificatia. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
255 lines
11 KiB
Python
255 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""Cum se masoara divergenta MDSRT — schema pe doua panouri (pret + indicator).
|
|
|
|
Pentru `strategii/07_mdsrt.md`, marcajul `<!-- DIAGRAMA: masurare -->` (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'<text x="{x:.1f}" y="{y:.1f}" font-size="{size}" '
|
|
f'font-weight="{weight}" fill="{fill}" text-anchor="{anchor}">'
|
|
f'{escape(s)}</text>')
|
|
|
|
|
|
def line(x1, y1, x2, y2, stroke=BLACK, width=1.4, dash=None):
|
|
d = f' stroke-dasharray="{dash}"' if dash else ""
|
|
return (f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
|
|
f'stroke="{stroke}" stroke-width="{width}"{d}/>')
|
|
|
|
|
|
def poly(points, stroke=BLACK, width=1.8):
|
|
pts = " ".join(f"{x:.1f},{y:.1f}" for x, y in points)
|
|
return (f'<polyline points="{pts}" fill="none" stroke="{stroke}" '
|
|
f'stroke-width="{width}" stroke-linejoin="round"/>')
|
|
|
|
|
|
def dot(x, y, color=BLACK, r=3.6):
|
|
return f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{r}" fill="{color}"/>'
|
|
|
|
|
|
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'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" '
|
|
f'width="{W}" height="{H}" role="img" '
|
|
f'aria-label="Cum se masoara divergenta: pretul face un lower low (L2 sub '
|
|
f'L1), indicatorul face un higher low (I2 peste I1); intre L1 si L2 trebuie '
|
|
f'minim 5 candele si cel mult 60 in spate, iar un low la 2-3 candele nu '
|
|
f'conteaza">',
|
|
'<g font-family="system-ui, sans-serif">',
|
|
f'<rect x="0" y="0" width="{W}" height="{H}" fill="{WHITE}"/>',
|
|
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'<rect x="{bx}" y="{by}" width="{bw}" height="{bh}" rx="8" '
|
|
f'fill="{WHITE}" stroke="{GRAY}" stroke-width="1.2"/>')
|
|
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'<rect x="{bx}" y="{by}" width="{bw}" height="{bh}" rx="8" '
|
|
f'fill="{WHITE}" stroke="{GRAY}" stroke-width="1.2"/>')
|
|
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('</g></svg>')
|
|
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")
|