La mdsrt_aplus eticheta spunea "primul low 94.60" pentru o bara al carei low era 94.50. Reparatia a mutat low-ul, nu eticheta: regula capitolului numara piciorul dupa low, iar specul spune tot 94.60 - doar umbra de 0.10 o contrazicea. Nicio cifra tiparita nu se schimba, iar nivelul isi ia acum eticheta din date, cu assert, ca sa nu mai poata minti. Cele trei de la mp2 aveau toate aceeasi cauza fizica: text colorat peste un corp de candela de aceeasi culoare. Acolo caracterele nu se incaleca, dispar - "intrare 577.00" se citea "intrare 577. 0", "crossing" se citea "crossi g". VERDICT pe restul de ~94: nu merita un lot. Din 20 de suprapuneri vazute efectiv pe ecran, doar 3 schimbau ce citesti, si toate trei din cauza aia. Celelalte 17 - text peste fitil, peste linie de nivel, peste chenar, peste coada de sageata - raman lizibile, fiindca fundalul e o linie subtire, nu o suprafata plina. Filtrul util nu e tiparul geometric, ci: eticheta cu cifra, peste un <rect> de aceeasi culoare. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
283 lines
13 KiB
Python
283 lines
13 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
|
|
# bara 18 e „primul low 94.60" al panoului: fără umbră de jos, ca low-ul ei
|
|
# să fie chiar 94.60. Cu umbra implicită de 0.10 low-ul era 94.50, iar
|
|
# nivelul desenat la 94.60 numea de fapt close-ul (raportat pe 02.10.2026).
|
|
WICKS_N[18] = (0.10, 0.00)
|
|
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
|
|
# nivelul „primul low 94.60" numește low-ul barei 18, nu close-ul ei
|
|
assert abs(cds_n[18].l - 94.60) < 1e-9, cds_n[18].l
|
|
# (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(cds_n[18].l, f"primul low {cds_n[18].l:.2f}", 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")
|