Files
atm-curs-dl/gen/strategii/hammer_mbt10_bh_aplus.py
Claude Agent 01599d2e9d strategii: primele 30 de diagrame, cu generatoarele lor
Cate un agent grafician per diagrama, fiecare cu generatorul si SVG-ul lui.
Graficele de pret trec prin svg_candles, cu assert-urile din specificatie
scrise in generator — verifica structura, nu doar ca ruleaza: ca al doilea
picior e mai jos, ca intrarea e close-ul barei de confirmare, ca 1:1 e exact.

Verificare prin sondaj pe buy_doji_ansamblu, cu screenshot citit: serie
continua de 32 de candele, panou de volum, MA(20), doji-ul si ieșirea pe
trail etichetate, cifrele identice cu specificatia.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-18 19:22:13 +00:00

280 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Buy hammer — A+ vs. mediocru (două panouri, exemple construite).
Citește `gen/strategii/hammer_mbt10_bh_aplus.spec.txt`
(marcajul `<!-- DIAGRAMA: bh_aplus -->`, 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 <svg> ale unui panou, fără elementul rădăcină."""
return svg[svg.index(">") + 1:svg.rindex("</svg>")]
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'<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'<text x="{TOTAL_W / 2:.0f}" y="20" fill="#333" text-anchor="middle" '
f'style="font-weight:bold">{TOP_TITLE}</text>'
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 - 14:.0f}" fill="#555" '
f'text-anchor="middle" style="font-size:10px">{FOOTER1}</text>'
f'<text x="{TOTAL_W / 2:.0f}" y="{TOTAL_H - PAD:.0f}" fill="#555" '
f'text-anchor="middle" style="font-size:10px">{FOOTER2}</text>'
f'</svg>'
)
# ============================================================ verificare pe SVG-ul final
def texts(svg: str):
"""Toate (x, y, text-anchor, text) din elementele <text> ale unui fragment."""
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("<"):
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")