diff --git a/gen/strategii/mdsrt_aplus.py b/gen/strategii/mdsrt_aplus.py new file mode 100644 index 0000000..34d195a --- /dev/null +++ b/gen/strategii/mdsrt_aplus.py @@ -0,0 +1,262 @@ +"""MDSRT — setup A+ vs. setup ratat (două panouri, exemple construite). + +Citește `gen/strategii/mdsrt_aplus.spec.txt` (marcajul ``). +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 + (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. + +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. + +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") +from svg_candles import Candle, Chart, Level, Mark, from_closes # 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 +PW = 520 +SCALE = 20.0 +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", + title="A+ — al doilea picior la suport, divergență, trigger", + caption="Divergență bull: low mai jos pe preț, volum mai mare la piciorul 2.", +) +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(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", + title="NU — close sub pivot și sub primul low = breakdown", + caption="Fără trigger: close sub pivot și sub primul low.", +) + +# ============================================================ wrapper: două panouri alăturate +def inner(svg: str) -> str: + """Conținutul dintre tag-urile ale unui panou, fără elementul rădăcină.""" + return svg[svg.index(">") + 1:svg.rindex("")] + + +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)) + + +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'' + f'{inner(svg_a)}' + f'{inner(svg_n)}' + f'{FOOTER}' + f'' +) + +# ============================================================ verificare pe SVG-ul final +def texts(svg: str): + """Toate (x, y, text-anchor, text) din elementele ale unui fragment.""" + out = [] + for m in re.finditer(r']*)>(.*?)', svg): + x, y, attrs, t = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4) + if t.startswith("<"): + continue + am = re.search(r'text-anchor="(\w+)"', attrs) + out.append((x, y, am.group(1) if am else "start", t)) + 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 + 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" + +# 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] + 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", + "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}" + +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") diff --git a/gen/strategii/mdsrt_aplus.svg b/gen/strategii/mdsrt_aplus.svg new file mode 100644 index 0000000..028651d --- /dev/null +++ b/gen/strategii/mdsrt_aplus.svg @@ -0,0 +1,203 @@ + + A+ — al doilea picior la suport, divergență, trigger + + pivot monthly 94.00 + + intrare 94.40 + + SL 93.10 + + TP1 95.70 (1:1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + picior 1: 94.55 + picior 2: 93.20 + Divergență bull: low mai jos pe preț, volum mai mare la piciorul 2. + + NU — close sub pivot și sub primul low = breakdown + + pivot monthly 94.00 + + primul low 94.60 + + spre 90.80 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + primul low + close 92.00 = breakdown + Fără trigger: close sub pivot și sub primul low. +Nivelurile 94.00 / 94.55 / 93.20 / 94.40 / 93.10 / 95.70 sunt construite ca exemplu — materialul dă regulile, nu prețurile. \ No newline at end of file diff --git a/strategii/07_mdsrt.md b/strategii/07_mdsrt.md index b9999ab..9f5acd0 100644 --- a/strategii/07_mdsrt.md +++ b/strategii/07_mdsrt.md @@ -461,7 +461,209 @@ confirmat" numără **surse distincte** din fișă, nu loturi de triaj. Același schelet — downtrend, al doilea picior mai jos, trigger de candelă — dar context opus. Diagrama le pune alături. - + + A+ — al doilea picior la suport, divergență, trigger + + pivot monthly 94.00 + + intrare 94.40 + + SL 93.10 + + TP1 95.70 (1:1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + picior 1: 94.55 + picior 2: 93.20 + Divergență bull: low mai jos pe preț, volum mai mare la piciorul 2. + + NU — close sub pivot și sub primul low = breakdown + + pivot monthly 94.00 + + primul low 94.60 + + spre 90.80 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + primul low + close 92.00 = breakdown + Fără trigger: close sub pivot și sub primul low. +Nivelurile 94.00 / 94.55 / 93.20 / 94.40 / 93.10 / 95.70 sunt construite ca exemplu — materialul dă regulile, nu prețurile. | | **A+** | **Mediocru / NU** | |---|---|---|