17_STRATEGII/07_mdsrt: patru restante aplicate, doua erau deja facute

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
This commit is contained in:
Claude Agent
2026-10-02 05:18:02 +00:00
parent f103d9830f
commit 70f5c19574
10 changed files with 841 additions and 777 deletions

View File

@@ -3,20 +3,20 @@
Citește `gen/strategii/mdsrt_aplus.spec.txt` (marcajul `<!-- DIAGRAMA: aplus -->`).
Același schelet (downtrend -> al doilea picior -> trigger de candelă), context opus:
- stânga (A+): downtrend ~15 candele până la pivotul monthly 94.00; piciorul 1
- 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.
- dreapta (NU): coborâre fără oprire la un nivel; bara 18 = primul low 94.60;
bara 25 închide 92.00, sub pivotul 94.00 ȘI sub primul low -> breakdown,
nu setup; prețul continuă spre 90.80.
- 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 randate separat și lipite într-un wrapper SVG, ca la
`buy_doji_aplus.py` — coordonatele fiecărui panou rămân corecte.
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
@@ -26,7 +26,9 @@ import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Candle, Chart, Level, Mark, from_closes # noqa: E402
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"
@@ -138,125 +140,137 @@ 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
PW = 520
# 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(ENTRY, "intrare 94.40", GREEN),
Level(SL, "SL 93.10", RED),
Level(TP1, "TP1 95.70 (1:1)", BLUE, label_at="left")],
marks=[Mark(18, "picior 1: 94.55", ORANGE, "below"),
Mark(24, "picior 2: 93.20", RED, "below")],
scale=SCALE, gap=15, body_w=8, left=44, width=PW, vol_h=60,
vol_label="volum",
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),
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),
Mark(25, "close 92.00 = breakdown", RED, "below", dy=12)],
scale=SCALE, gap=15, body_w=8, left=44, width=PW, vol_h=60,
vol_label="volum",
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,
)
# ============================================================ wrapper: două panouri alăturate
def inner(svg: str) -> str:
"""Conținutul dintre tag-urile <svg> ale unui panou, fără elementul rădăcină."""
return svg[svg.index(">") + 1:svg.rindex("</svg>")]
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
def vb_height(svg: str) -> float:
m = re.search(r'viewBox="0 0 [\d.]+ ([\d.]+)"', svg)
assert m, "nu găsesc viewBox în panoul randat"
return float(m.group(1))
FOOTER = ["Nivelurile 94.00 / 94.55 / 93.20 / 94.40 / 93.10 / 95.70 sunt construite ca exemplu —",
"materialul dă regulile, nu prețurile."]
PANEL_GAP, PAD, TITLE_H, FOOTER_H = 24, 8, 8, 26
svg_a, svg_n = ch_a.render(), ch_n.render()
HA, HN = vb_height(svg_a), vb_height(svg_n)
assert svg_a.count("\n\n") == 0 and svg_n.count("\n\n") == 0, "linie goală în panou"
TOTAL_W = PAD + PW + PANEL_GAP + PW + PAD
TOTAL_H = PAD + TITLE_H + max(HA, HN) + FOOTER_H + PAD
OFF_A = (PAD, PAD + TITLE_H)
OFF_N = (PAD + PW + PANEL_GAP, PAD + TITLE_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.")
wrapper = (
f'<svg viewBox="0 0 {TOTAL_W} {TOTAL_H:.0f}" width="{TOTAL_W}" height="{TOTAL_H:.0f}" '
f'xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;font:12px sans-serif">'
f'<g transform="translate({OFF_A[0]},{OFF_A[1]})">{inner(svg_a)}</g>'
f'<g transform="translate({OFF_N[0]},{OFF_N[1]})">{inner(svg_n)}</g>'
f'<text x="{TOTAL_W / 2:.0f}" y="{TOTAL_H - PAD:.0f}" fill="#555" '
f'text-anchor="middle">{FOOTER}</text>'
f'</svg>'
)
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
def texts(svg: str):
"""Toate (x, y, text-anchor, text) din elementele <text> ale unui fragment."""
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>', svg):
x, y, attrs, t = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
if t.startswith("<"):
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+)"', attrs)
out.append((x, y, am.group(1) if am else "start", t))
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]:
"""Intervalul orizontal aproximativ al unui text, față de ancora lui."""
w = len(t) * 12 * 0.56
w = len(t) * CW
left = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
return left, left + w
assert wrapper.count("\n\n") == 0, "linie goală în SVG"
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})"
# fiecare text (după translatarea panoului) e în întregime în interiorul pânzei
all_txt = []
for frag, (ox, oy) in ((inner(svg_a), OFF_A), (inner(svg_n), OFF_N)):
for x, y, anchor, t in texts(frag):
gx, gy = x + ox, y + oy
gl, gr = gx + span(0, anchor, t)[0], gx + span(0, anchor, t)[1]
assert ox <= gl and gr <= ox + PW, f"text iese din panou: {t!r} ({gl:.0f}..{gr:.0f})"
assert -1 <= gl and gr <= TOTAL_W + 1, f"text ieșit pe orizontală: {t!r} ({gl:.0f}..{gr:.0f})"
assert 0 <= gy <= TOTAL_H, f"text ieșit pe verticală: {t!r} ({gy:.0f})"
all_txt.append((gx, gy, anchor, t))
# footer-ul, în coordonate globale
fl, fr = span(TOTAL_W / 2, "middle", FOOTER)
assert -1 <= fl and fr <= TOTAL_W + 1, "footer iese din pânză"
all_txt.append((TOTAL_W / 2, TOTAL_H - PAD, "middle", FOOTER))
assert len(all_txt) >= 8, "prea puține etichete"
# nu se suprapun două texte pe aceeași poziție sau pe rândul apropiat
for i in range(len(all_txt)):
xi, yi, ai, si = all_txt[i]
li, ri = xi + span(0, ai, si)[0], xi + span(0, ai, si)[1]
for j in range(i + 1, len(all_txt)):
xj, yj, aj, sj = all_txt[j]
lj, rj = xj + span(0, aj, sj)[0], xj + span(0, aj, sj)[1]
# 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",
"close 92.00 = breakdown", "spre 90.80",
"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 wrapper, f"lipsește din desen: {cheie!r}"
assert cheie in svg, f"lipsește din desen: {cheie!r}"
OUT.write_text(wrapper, encoding="utf-8")
print(f"scris {OUT.name}: {TOTAL_W}x{TOTAL_H:.0f}, "
f"{len(all_txt)} etichete, verificare OK")
# 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")