"""Buy hammer — A+ vs. mediocru (două panouri, exemple construite). Citește `gen/strategii/hammer_mbt10_bh_aplus.spec.txt` (marcajul ``, Partea A §2.2). Același pattern ca formă (downtrend → hammer → confirmare), context opus: - stânga (A+): downtrend de 12 candele de la 50.00 spre 42.80, hammer-ul cu low 41.90 chiar pe suportul monthly 42.00 (0,24 % sub nivel; corp 0.10, fitil jos 0.90), confirmarea închide 42.95 peste high-ul 42.90, pe volum peste media ultimelor 10 bare — „acumulare"; - dreapta (NU): fără downtrend real (range lateral 44–46), hammer-ul în mijlocul intervalului (o=45.00, c=h=45.10, l=44.10), departe de orice nivel (suportul construit 42.50 e la 3,6 % sub low), confirmarea închide tot 45.15 peste 45.10, dar pe volum slab — context invalid. Nivelurile 42.00 / 42.50 și cifrele de preț sunt construite (materialul nu dă prețuri pentru acest contrast); regulile (locația la nivel, downtrend, volum) sunt din curs. Două `Chart`-uri randate separat, lipite într-un SVG wrapper: coordonatele fiecărui panou rămân corecte prin construcție. Notă de serie: specificația listează `close[11]=42.40` (A+) și `44.60` (NU), dar descrie hammer-ul cu `o=42.80 / c=42.90`, respectiv `o=45.00 / c=45.10`, iar assert 9 cere `close == high`. Cum open-ul fiecărei bare = close-ul precedentei (STYLE.md), am ridicat close[11] la 42.80 / 45.00 și close[12] la 42.90 / 45.10, ca seria să susțină exact OHLC-ul din etichetă. Restul close-urilor sunt cele din specificație. Rulează: python3 gen/strategii/hammer_mbt10_bh_aplus.py Scrie: gen/strategii/hammer_mbt10_bh_aplus.svg """ import re import sys from pathlib import Path sys.path.insert(0, "/workspace/atm-curs-dl") from svg_candles import Chart, Level, Mark, from_closes # noqa: E402 OUT = Path(__file__).resolve().parent / "hammer_mbt10_bh_aplus.svg" GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" HAM_A, CONF_A = 12, 13 # indicii în panoul A+ HAM_N, CONF_N = 12, 13 # indicii în panoul NU SUPORT_A = 42.00 # suport monthly (A+) SUPORT_N = 42.50 # cel mai apropiat nivel construit (NU) # ============================================================ PANOUL A+ (20 bare) # 12 bare de downtrend 50.00 -> 42.80, hammer la 42.00, confirmare, apoi urcare. CLOSES_A = [ 50.00, 49.10, 49.30, 48.20, 47.30, 47.60, 46.40, 45.50, 44.60, 43.70, 43.00, 42.80, 42.90, # 12 = hammer: o=42.80, c=h=42.90, l=41.90 (H=C, fitil jos 0.90) 42.95, # 13 = confirmare, close > 42.90 => intrare 43.40, 43.80, 44.20, 44.00, 44.50, 44.80, ] assert len(CLOSES_A) == 20, len(CLOSES_A) WICKS_A = [(0.25, 0.30)] * 20 WICKS_A[HAM_A] = (0.00, 0.90) # high 42.90, low 41.90 (pe suportul 42.00) WICKS_A[CONF_A] = (0.20, 0.20) # confirmarea: o=42.90, c=42.95 # Volum ales de generator (materialul nu dă volum): scade pe downtrend, climax # pe hammer, spike pe confirmare („acumulare"), moderat după. VOLS_A = [54, 52, 51, 49, 48, 47, 46, 45, 44, 43, 42, 45, 64, # 12 = hammer: exces de vânzare 90, # 13 = confirmare: volum peste medie 74, 70, 66, 62, 60, 58] assert len(VOLS_A) == 20 cds_a = from_closes(CLOSES_A, wicks=WICKS_A, vols=VOLS_A, start=50.60) ham_a, conf_a = cds_a[HAM_A], cds_a[CONF_A] # ============================================================ PANOUL NU (20 bare) # range lateral 44-46, fără downtrend; hammer în mijloc, confirmare pe volum slab. CLOSES_N = [ 46.00, 45.30, 45.80, 45.10, 45.60, 44.90, 45.40, 44.80, 45.20, 44.70, 45.10, 45.00, 45.10, # 12 = hammer: o=45.00, c=h=45.10, l=44.10 (H=C, fitil jos 0.90) 45.15, # 13 = confirmare, close > 45.10 44.90, 44.70, 44.40, 44.20, 44.00, 43.80, ] assert len(CLOSES_N) == 20, len(CLOSES_N) WICKS_N = [(0.25, 0.30)] * 20 WICKS_N[HAM_N] = (0.00, 0.90) # high 45.10, low 44.10 WICKS_N[CONF_N] = (0.20, 0.20) # Volum ales de generator: lateral, constant, confirmarea pe volum slab. VOLS_N = [52, 50, 53, 49, 51, 48, 52, 50, 49, 51, 50, 49, 56, # 12 31, # 13 = confirmare: volum slab 52, 50, 48, 51, 49, 47] assert len(VOLS_N) == 20 cds_n = from_closes(CLOSES_N, wicks=WICKS_N, vols=VOLS_N, start=45.60) ham_n, conf_n = cds_n[HAM_N], cds_n[CONF_N] # ============================================================ assert-urile din spec # 1. A+: low[12] la <= 0,5 % de 42.00 (41.90 e la 0,24 %) dist_a = abs(ham_a.l - SUPORT_A) / SUPORT_A assert dist_a <= 0.005, f"low hammer A+ {ham_a.l} la {dist_a * 100:.2f} % de 42.00" # low-ul hammer-ului chiar trece sub nivel (fitilul sparge suportul) assert ham_a.l < SUPORT_A <= ham_a.h, "hammer-ul A+ nu are fitilul pe/la nivel" # 2. A+: close[13] > high[12] (tehnic valid) assert conf_a.c > ham_a.h, f"A+ close {conf_a.c} <= high hammer {ham_a.h}" assert abs(conf_a.c - 42.95) < 1e-9 # 3. A+: volumul pe bara 13 > media volumelor pe 10 bare media_a = sum(VOLS_A[CONF_A - 10:CONF_A]) / 10 assert VOLS_A[CONF_A] > media_a, f"vol confirmare A+ {VOLS_A[CONF_A]} <= media {media_a}" # 4. A+: close[12] < close[0] - 5 (downtrend vizibil) assert cds_a[HAM_A].c < cds_a[0].c - 5, "downtrendul A+ nu e vizibil" # 5. NU: (low[12] - 42.50) / 42.50 > 0.02 (hammer-ul NU e departe de nivel) dist_n = (ham_n.l - SUPORT_N) / SUPORT_N assert dist_n > 0.02, f"low hammer NU {ham_n.l} prea aproape de 42.50 ({dist_n:.1%})" # eticheta „la -3,6 %" din spec: (44.10 - 42.50) / 44.10 pct_n = (ham_n.l - SUPORT_N) / ham_n.l * 100 assert round(pct_n, 1) == 3.6, f"procentul NU e {pct_n:.2f} %, nu 3,6 %" # 6. NU: close[13] > high[12] (tehnic valid) assert conf_n.c > ham_n.h, f"NU close {conf_n.c} <= high hammer {ham_n.h}" assert abs(conf_n.c - 45.15) < 1e-9 # 7. NU: volumul pe bara 13 < media volumelor pe 10 bare media_n = sum(VOLS_N[CONF_N - 10:CONF_N]) / 10 assert VOLS_N[CONF_N] < media_n, f"vol confirmare NU {VOLS_N[CONF_N]} >= media {media_n}" # 8. NU: |close[12] - close[0]| <= 1.5 (fără downtrend real, range lateral) assert abs(cds_n[HAM_N].c - cds_n[0].c) <= 1.5, "NU nu e range lateral" # 9. ambele hammer: fitil jos >= 2x corp si close == high (fitil de sus inexistent) for tag, o, l, c, h in ( ("A+", ham_a.o, ham_a.l, ham_a.c, ham_a.h), ("NU", ham_n.o, ham_n.l, ham_n.c, ham_n.h)): assert (o - l) >= 2 * (c - o), f"fitilul hammer-ului {tag} < 2x corpul" assert abs(c - h) < 1e-9, f"hammer-ul {tag} are fitil de sus" # OHLC-ul descris în spec (eticheta e adevărul) assert abs(ham_a.o - 42.80) < 1e-9 and abs(ham_a.c - 42.90) < 1e-9 and abs(ham_a.l - 41.90) < 1e-9 assert abs(ham_n.o - 45.00) < 1e-9 and abs(ham_n.c - 45.10) < 1e-9 and abs(ham_n.l - 44.10) < 1e-9 # context: hammer-ul A+ e la baza downtrendului, NU nu are downtrend assert min(c.l for c in cds_a[:HAM_A]) > ham_a.l, "low-ul hammer-ului A+ nu e minimul" assert abs(cds_a[-1].c - 44.80) < 1e-9 and cds_a[-1].c > conf_a.c, "A+ nu urcă după intrare" print(f"A+ hammer {ham_a.o:.2f}/{ham_a.c:.2f}/{ham_a.h:.2f}/{ham_a.l:.2f} " f"conf {conf_a.c:.2f} > high {ham_a.h:.2f} vol {VOLS_A[CONF_A]} > medie {media_a:.1f}") print(f"NU hammer {ham_n.o:.2f}/{ham_n.c:.2f}/{ham_n.h:.2f}/{ham_n.l:.2f} " f"conf {conf_n.c:.2f} > high {ham_n.h:.2f} vol {VOLS_N[CONF_N]} < medie {media_n:.1f}") print(f"NU low {ham_n.l:.2f} vs suport {SUPORT_N:.2f} = -{pct_n:.1f} %") # ============================================================ desenele celor două panouri PW = 430 # lățimea fiecărui panou SCALE = 26.0 # aceeași scară în ambele panouri (comparație cinstită) ch_a = Chart( candles=cds_a, levels=[Level(SUPORT_A, "suport monthly 42.00", GREEN, label_at="left")], marks=[Mark(HAM_A, "hammer la nivel (low 41.90)", ORANGE, "below", dy=0), Mark(CONF_A, "acumulare", GREEN, "above", dy=-26)], mas=[], scale=SCALE, gap=15, body_w=9, left=44, width=PW, vol_h=60, vol_label="volum", title="A+ — hammer la suport monthly, volum în acumulare", caption="bara 13: 42.95 > high 42.90, pe volum peste medie.", ) ch_n = Chart( candles=cds_n, levels=[Level(SUPORT_N, "fără nivel apropiat (suport 42.50 la −3,6 %)", GRAY, label_at="left")], marks=[Mark(HAM_N, "hammer în mijloc de range", ORANGE, "below", dy=0), Mark(CONF_N, "volum slab", RED, "above", dy=0)], mas=[], scale=SCALE, gap=15, body_w=9, left=44, width=PW, vol_h=60, vol_label="volum", title="Mediocru — hammer în mijloc de range, fără nivel", caption="bara 13: 45.15 > 45.10, dar pe volum slab.", ) # ============================================================ 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 = 24, 8 TITLE_H, FOOTER_H = 30, 38 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) TOP_TITLE = "Buy hammer A+ vs. mediocru (două panouri, exemple construite)" FOOTER1 = ("Reguli din curs: 26_2025-06-11.md:701-703 (locația la nivel), " "05_patternuri_candele.md:77-80, 05_volum.md:2169.") FOOTER2 = ("Nivelurile 42.00 / 42.50 și cifrele de preț sunt construite " "(materialul nu dă prețuri pentru acest contrast).") FOOTER = FOOTER1 + " " + FOOTER2 wrapper = ( f'' f'{TOP_TITLE}' f'{inner(svg_a)}' f'{inner(svg_n)}' f'{FOOTER1}' f'{FOOTER2}' 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)) # textele din wrapper (titlu + footer), în coordonate globale for x, y, anchor, t in ((TOTAL_W / 2, 20.0, "middle", TOP_TITLE), (TOTAL_W / 2, TOTAL_H - PAD - 14, "middle", FOOTER1), (TOTAL_W / 2, TOTAL_H - PAD, "middle", FOOTER2)): gl, gr = span(x, anchor, t) assert -1 <= gl and gr <= TOTAL_W + 1, f"text din wrapper iese din pânză: {t!r}" assert 0 <= y <= TOTAL_H, f"text din wrapper în afara înălțimii: {t!r}" all_txt.append((x, y, anchor, t)) assert len(all_txt) >= 10, "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 ("suport monthly 42.00", "hammer la nivel", "acumulare", "fără nivel apropiat (suport 42.50 la −3,6 %)", "volum slab", "A+ — hammer la suport monthly, volum în acumulare", "Mediocru — hammer în mijloc de range, fără nivel"): 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")