Raportul verificatorului e din 21.09, iar primele doua probleme fusesera
reparate intre timp in 424ecf2 - in text. Ramasesera insa in generatoare:
docstring-ul lui mdsrt_stop si comentariul STOP_BARS din mdsrt_ansamblu inca
spuneau ca N nu e dat de curs. Un capitol poate fi corect si sa-si contrazica
generatorul.
Reparate acum: tabelul A+ descria "coborare fara nivel dedesubt" pentru un
panou in care pivotul 94.00 chiar e trasat si doar nu tine; layout-ul lui
mdsrt_aplus, unde "picior 1: 94.55" era tiparita sub linia de 94.00 (deci se
citea ca ~93.5) si "close 92.00" statea la inaltimea lui 90.80; doua etichete
taiate de linii in stop si short; si retestul din glosar, 590.05 nu 590.00.
Panourile A+ sunt acum stivuite, 700 px in loc de 1080, deci textul nu se mai
scaleaza la 0,67x. Assert nou: y-ul etichetei "picior 1" trebuie sa cada la
±8 px de unde cade pretul ei fata de linia pivotului - exact regresia
raportata.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
277 lines
12 KiB
Python
277 lines
12 KiB
Python
"""MDSRT — setup A+ vs. setup ratat (două panouri, exemple construite).
|
|
|
|
Citește `gen/strategii/mdsrt_aplus.spec.txt` (marcajul `<!-- DIAGRAMA: aplus -->`).
|
|
Același schelet (downtrend -> al doilea picior -> trigger de candelă), context opus:
|
|
|
|
- sus (A+): downtrend ~15 candele până la pivotul monthly 94.00; piciorul 1
|
|
(bara 18) face low 94.55; piciorul 2 (bara 24, Buy Hammer) coboară la 93.20,
|
|
scalpând stopurile chiar sub nivel, dar închide 94.10 înapoi peste suport;
|
|
bara 25 sparge high-ul trigger-ului (94.30) și închide 94.40 = intrare,
|
|
SL 93.10 (sub low-ul piciorului 2), TP1 95.70 (1:1). Divergență bull:
|
|
low mai jos pe preț, volum mai mare la piciorul 2.
|
|
- jos (NU): pivotul monthly 94.00 e trasat și aici, dar coborârea trece prin el
|
|
fără să se oprească; bara 18 = primul low 94.60; bara 25 închide 92.00, sub
|
|
pivot ȘI sub primul low -> breakdown, nu setup; prețul continuă spre 90.80.
|
|
|
|
Materialul dă regulile, nu prețurile; nivelurile 94.00 / 94.55 / 93.20 /
|
|
94.40 / 93.10 / 95.70 sunt construite ca exemplu (spec, liniile 18-19).
|
|
Două `Chart`-uri stivuite în aceeași pânză, ca la `mdsrt_stop.py`: panourile
|
|
alăturate dădeau 1080 px, pe care capitolul îi scala la 0,67x.
|
|
|
|
Rulează: python3 gen/strategii/mdsrt_aplus.py
|
|
Scrie: gen/strategii/mdsrt_aplus.svg
|
|
"""
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, "/workspace/atm-curs-dl")
|
|
sys.path.insert(0, "/workspace/atm-curs-dl/gen")
|
|
from svg_candles import Arrow, Candle, Chart, Level, Mark, from_closes # noqa: E402
|
|
from eticheta_libera import eticheta_libera # noqa: E402
|
|
|
|
OUT = Path(__file__).resolve().parent / "mdsrt_aplus.svg"
|
|
|
|
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
|
|
|
|
SUPORT = 94.00
|
|
ENTRY = 94.40
|
|
SL = 93.10
|
|
TP1 = 95.70
|
|
LOW1, LOW2 = 94.55, 93.20
|
|
|
|
# ============================================================ PANOUL A+ (29 bare)
|
|
CLOSES_A = [
|
|
104.0, 103.2, 102.5, 102.9, 101.8, 101.0, 100.3, 100.7, 99.8, 99.2,
|
|
98.6, 98.0, 97.4, 96.8, 96.3, 95.8,
|
|
95.40, 95.60, 94.60, # 18 = picior 1 (low 94.55)
|
|
94.90, 95.20, 94.80, 94.40, 93.80,
|
|
94.10, # 24 = Buy Hammer / picior 2 (low 93.20)
|
|
94.40, # 25 = intrare (close 94.40 > high 94.30)
|
|
94.80, 95.30, 95.70, # 28 = TP1 1:1
|
|
]
|
|
assert len(CLOSES_A) == 29, len(CLOSES_A)
|
|
|
|
WICKS_A = [(0.15, 0.15)] * 29
|
|
WICKS_A[18] = (0.10, 0.05) # high 95.70, low 94.60 - 0.05 = 94.55
|
|
WICKS_A[24] = (0.20, 0.60) # high 94.30, low 93.80 - 0.60 = 93.20
|
|
WICKS_A[25] = (0.10, 0.05) # high 94.50, low 94.05
|
|
|
|
VOLS_A = [
|
|
58, 56, 54, 52, 55, 50, 48, 46, 49, 45,
|
|
43, 41, 44, 42, 40, 38,
|
|
36, 34, 40, 44, 46, 42, 44, 46,
|
|
78, # 24: volum mare la piciorul 2
|
|
70, 60, 52, 48,
|
|
]
|
|
assert len(VOLS_A) == 29, len(VOLS_A)
|
|
|
|
cds_a = from_closes(CLOSES_A, wicks=WICKS_A, vols=VOLS_A, start=104.6)
|
|
|
|
# ============================================================ PANOUL NU (29 bare)
|
|
CLOSES_N = [
|
|
104.0, 103.0, 102.2, 101.4, 100.6, 99.9, 99.2, 98.5, 97.9, 97.3,
|
|
96.7, 96.1, 95.6, 95.1, 94.8, 94.4,
|
|
94.20, 94.60, 94.60, # 18 = „primul low" 94.60
|
|
94.10, 93.70, 93.40, 93.10, 92.80, 92.40,
|
|
92.00, # 25 = close sub pivot ȘI sub primul low
|
|
91.60, 91.20, 90.80,
|
|
]
|
|
assert len(CLOSES_N) == 29, len(CLOSES_N)
|
|
|
|
WICKS_N = [(0.10, 0.10)] * 29
|
|
WICKS_N[25] = (0.05, 0.40) # high 92.45, low 92.00 - 0.40 = 91.60
|
|
|
|
VOLS_N = [
|
|
60, 58, 56, 54, 52, 50, 49, 47, 48, 46,
|
|
44, 43, 42, 41, 40, 42,
|
|
41, 39, 38, 41, 43, 45, 48, 52,
|
|
68, # 24: presiune în creștere pe coborâre
|
|
74, 70, 66, 62,
|
|
]
|
|
assert len(VOLS_N) == 29, len(VOLS_N)
|
|
|
|
cds_n = from_closes(CLOSES_N, wicks=WICKS_N, vols=VOLS_N, start=104.4)
|
|
|
|
# ============================================================ assert-urile din spec
|
|
# (1) piciorul 2 e mai jos decât piciorul 1
|
|
assert abs(cds_a[18].l - LOW1) < 1e-9, cds_a[18].l
|
|
assert abs(cds_a[24].l - LOW2) < 1e-9, cds_a[24].l
|
|
assert cds_a[24].l < cds_a[18].l
|
|
# (2) piciorul 2 rupe suportul 94.00
|
|
assert cds_a[24].l < SUPORT
|
|
# (3) distanța dintre swing low-uri >= 5 candele (minimul cerut)
|
|
assert 24 - 18 == 6
|
|
# (4) hammer-ul închide înapoi peste suport
|
|
assert abs(cds_a[24].c - 94.10) < 1e-9 and cds_a[24].c > SUPORT
|
|
# (5) condiția de „hammer": coada de jos > corp ȘI corp <= 0.5 * range
|
|
h = cds_a[24]
|
|
corp = abs(h.o - h.c)
|
|
coada = min(h.o, h.c) - h.l
|
|
rng = h.h - h.l
|
|
assert abs(corp - 0.30) < 1e-9, corp
|
|
assert coada > corp, (coada, corp)
|
|
assert corp <= 0.5 * rng, (corp, rng)
|
|
# (6) intrarea = close 25 > high-ul trigger-ului (high 24 = 94.30)
|
|
assert abs(cds_a[25].c - ENTRY) < 1e-9, cds_a[25].c
|
|
assert cds_a[25].c > cds_a[24].h
|
|
# (7) TP1 e exact 1:1 față de risc
|
|
assert abs((ENTRY - SL) - (TP1 - ENTRY)) < 1e-9
|
|
# (8) divergență bull: low mai jos pe preț, volum mai mare la piciorul 2
|
|
assert VOLS_A[24] > VOLS_A[18]
|
|
# (9) NU: close sub pivot ȘI sub primul low
|
|
assert abs(cds_n[25].c - 92.00) < 1e-9, cds_n[25].c
|
|
assert cds_n[25].c < SUPORT and cds_n[25].c < 94.60
|
|
# (10) NU: coborâre fără trigger
|
|
assert cds_n[25].c < cds_n[24].c < cds_n[23].c
|
|
# (11) NU: fără divergență (lower low pe preț și pe volum, conform assert-ului)
|
|
assert VOLS_N[24] > VOLS_N[18] and cds_n[24].l < cds_n[18].l
|
|
# (12) fiecare open = close-ul barei anterioare
|
|
for cds, tag in ((cds_a, "A+"), (cds_n, "NU")):
|
|
for a, b in zip(cds, cds[1:]):
|
|
assert abs(a.c - b.o) < 1e-9, f"{tag}: gol artificial între bare"
|
|
# (13) 29 de candele în fiecare panou
|
|
assert len(cds_a) == 29 and len(cds_n) == 29
|
|
|
|
print(f"A+ picior1 low {cds_a[18].l:.2f} picior2 low {cds_a[24].l:.2f} "
|
|
f"(< {SUPORT:.2f}) close {cds_a[24].c:.2f} intrare {cds_a[25].c:.2f} "
|
|
f"vol {VOLS_A[24]} > {VOLS_A[18]}")
|
|
print(f"NU primul low {cds_n[18].l:.2f} close 25 {cds_n[25].c:.2f} "
|
|
f"(< {SUPORT:.2f} și < 94.60) vol {VOLS_N[24]} > {VOLS_N[18]}")
|
|
|
|
# ============================================================ desenele celor două panouri
|
|
# Panourile stau unul sub altul, nu alăturate: la 1080 px lățime capitolul le
|
|
# scala la 0,67x și textul se citea la ~8 px. Stivuite, fiecare are toți cei
|
|
# 700 px ai coloanei, deci și loc în dreapta pentru etichetele de nivel.
|
|
W, VGAP = 700, 18
|
|
SCALE = 20.0
|
|
common: dict = dict(scale=SCALE, gap=15, body_w=8, left=150, width=W,
|
|
vol_h=60, vol_label="volum")
|
|
|
|
# Nivelurile trade-ului (intrare / SL / TP1) pornesc de la bara 25: liniile lor
|
|
# pe toată lățimea tăiau eticheta „pivot monthly 94.00" din stânga.
|
|
# Picioarele sunt săgeți, nu Mark: un Mark „below" ar fi pus „picior 1: 94.55"
|
|
# sub linia de 94.00, adică la înălțimea lui ~93.5.
|
|
ch_a = Chart(
|
|
candles=cds_a,
|
|
levels=[Level(SUPORT, "pivot monthly 94.00", ORANGE, label_at="left"),
|
|
Level(TP1, "TP1 95.70 (1:1)", BLUE, i0=25),
|
|
Level(ENTRY, "intrare 94.40", GREEN, i0=25),
|
|
Level(SL, "SL 93.10", RED, i0=25)],
|
|
arrows=[Arrow(18, "picior 1: 94.55", price=LOW1, where="left",
|
|
color=ORANGE, dy=-3),
|
|
Arrow(24, "picior 2: 93.20", price=LOW2, where="left", color=RED)],
|
|
title="A+ — al doilea picior la suport, divergență, trigger",
|
|
caption="Divergență bull: low mai jos pe preț, volum mai mare la piciorul 2.",
|
|
**common,
|
|
)
|
|
ch_n = Chart(
|
|
candles=cds_n,
|
|
levels=[Level(SUPORT, "pivot monthly 94.00", ORANGE, label_at="left"),
|
|
Level(94.60, "primul low 94.60", GRAY, i0=18),
|
|
Level(92.00, "close 92.00", RED, i0=25),
|
|
Level(90.80, "spre 90.80", RED, label_at="left")],
|
|
marks=[Mark(18, "primul low", ORANGE, "above", dy=-2)],
|
|
title="NU — close sub pivot și sub primul low = breakdown",
|
|
caption="Fără trigger: close sub pivot și sub primul low.",
|
|
**common,
|
|
)
|
|
|
|
|
|
def panou(ch, sus_de=0.0):
|
|
"""Randează un panou deja deplasat cu `sus_de` px, ca la `mdsrt_stop.py`."""
|
|
ch.top += sus_de
|
|
svg = ch.render()
|
|
h = float(re.search(r'viewBox="0 0 [\d.]+ ([\d.]+)"', svg).group(1))
|
|
return svg[svg.index(">") + 1:svg.rindex("</svg>")].strip(), h
|
|
|
|
|
|
FOOTER = ["Nivelurile 94.00 / 94.55 / 93.20 / 94.40 / 93.10 / 95.70 sunt construite ca exemplu —",
|
|
"materialul dă regulile, nu prețurile."]
|
|
|
|
body_a, h_a = panou(ch_a)
|
|
body_n, h_n = panou(ch_n, h_a + VGAP)
|
|
H = h_n + 12 + 17 * len(FOOTER)
|
|
svg = "\n ".join([
|
|
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">',
|
|
body_a,
|
|
f'<line x1="24" y1="{h_a + VGAP / 2:.0f}" x2="{W - 14}" y2="{h_a + VGAP / 2:.0f}" stroke="#ddd"/>',
|
|
body_n,
|
|
*(f'<text x="24" y="{H - 8 - 17 * (len(FOOTER) - 1 - k):.0f}" fill="#555">{t}</text>'
|
|
for k, t in enumerate(FOOTER)),
|
|
]) + "\n</svg>"
|
|
|
|
# ============================================================ verificare pe SVG-ul final
|
|
assert "\n\n" not in svg, "linie goală în SVG"
|
|
CW = 6.2
|
|
|
|
|
|
def texts(frag: str):
|
|
"""(x, y, ancoră, text) din fiecare <text> care nu e doar un wrapper."""
|
|
out = []
|
|
for m in re.finditer(r'<text x="([-\d.]+)" y="([-\d.]+)"([^>]*)>(.*?)</text>', frag):
|
|
if m.group(4).startswith("<"):
|
|
continue
|
|
am = re.search(r'text-anchor="(\w+)"', m.group(3))
|
|
out.append((float(m.group(1)), float(m.group(2)),
|
|
am.group(1) if am else "start", m.group(4)))
|
|
return out
|
|
|
|
|
|
def span(x: float, anchor: str, t: str) -> tuple[float, float]:
|
|
w = len(t) * CW
|
|
left = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
|
|
return left, left + w
|
|
|
|
|
|
all_txt = texts(svg)
|
|
assert len(all_txt) >= 12, f"prea puține etichete: {len(all_txt)}"
|
|
for x, y, anchor, t in all_txt:
|
|
l, r = span(x, anchor, t)
|
|
assert -1 <= l and r <= W + 1, f"text iese din pânză: {t!r} ({l:.0f}..{r:.0f})"
|
|
assert 0 <= y <= H, f"text ieșit pe verticală: {t!r} ({y:.0f})"
|
|
|
|
# două etichete nu stau pe același rând, una peste alta
|
|
for i, (xi, yi, ai, si) in enumerate(all_txt):
|
|
li, ri = span(xi, ai, si)
|
|
for xj, yj, aj, sj in all_txt[i + 1:]:
|
|
lj, rj = span(xj, aj, sj)
|
|
if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
|
|
raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
|
|
|
|
# etichetele cerute de spec există în desen
|
|
for cheie in ("pivot monthly 94.00", "intrare 94.40", "SL 93.10", "TP1 95.70 (1:1)",
|
|
"picior 1: 94.55", "picior 2: 93.20", "primul low 94.60",
|
|
"primul low", "close 92.00", "spre 90.80",
|
|
"A+ — al doilea picior la suport, divergență, trigger",
|
|
"NU — close sub pivot și sub primul low = breakdown"):
|
|
assert cheie in svg, f"lipsește din desen: {cheie!r}"
|
|
|
|
# nicio etichetă de preț peste o linie desenată sau peste un corp de candelă:
|
|
# exact defectul din 02.10.2026 („picior 1: 94.55" sub linia de 94.00,
|
|
# „close 92.00" lipită de linia lui 90.80).
|
|
eticheta_libera(svg, "aplus", {"primul low"}, rects=True)
|
|
# etichetele de săgeată stau lipite de coada propriei săgeți prin construcție,
|
|
# deci la ele se verifică doar corpurile de candelă...
|
|
eticheta_libera(svg, "aplus-sageti", {"picior 1: 94.55", "picior 2: 93.20"},
|
|
rects=True, segmente=False)
|
|
# ...plus înălțimea, care e defectul raportat pe 02.10.2026: „picior 1: 94.55"
|
|
# era tipărită sub linia pivotului de 94.00, deci se citea ca ~93.5.
|
|
y_pivot = float(re.search(rf'<line x1="[\d.]+" y1="([\d.]+)"[^/]*stroke="{ORANGE}"',
|
|
body_a).group(1))
|
|
for eticheta, pret in (("picior 1: 94.55", LOW1), ("picior 2: 93.20", LOW2)):
|
|
y = float(re.search(rf'<text x="[-\d.]+" y="([\d.]+)"[^>]*>{eticheta}<', body_a).group(1))
|
|
asteptat = y_pivot - (pret - SUPORT) * SCALE
|
|
assert abs(y - asteptat) <= 8, (f"{eticheta!r} e la y={y:.0f}, dar prețul "
|
|
f"{pret:.2f} cade la y={asteptat:.0f}")
|
|
eticheta_libera(svg, "aplus-niveluri",
|
|
{"pivot monthly 94.00", "intrare 94.40", "SL 93.10",
|
|
"TP1 95.70 (1:1)", "primul low 94.60", "close 92.00",
|
|
"spre 90.80"},
|
|
min_px=2.0, rects=True) # min_px=2: eticheta stă la 6 px peste
|
|
# linia ei, deci doar liniile străine
|
|
|
|
OUT.write_text(svg, encoding="utf-8")
|
|
print(f"scris {OUT.name}: {W}x{H:.0f}, {len(all_txt)} etichete, verificare OK")
|