Files
atm-curs-dl/gen/strategii/hammer_mbt10_bh_aplus.py
Claude Agent a7785740cc 06_hammer_mbt10: capitol reconstruit dupa sablon
Doua proceduri complet separate, nu una amestecata, pentru ca se fac
altfel: buy hammer e pattern public, desenabil si exersabil retroactiv;
MBT-10 e cutie neagra din care se poate reface o singura conditie.
Nivelul 1 le pune fata in fata, ca cititorul sa stie din prima de ce nu
le poate trata la fel. Cate 5 pasi fiecare, oglinda pe short la hammer,
checklist dublu. Sursele in fise/06_hammer_mbt10_surse.md.

Zece diagrame noi: hammer-ul nu era definit vizual nicaieri, iar hanging
man era doar text — acum e aceeasi bara in doua contexte, cu assert ca e
identica. Cele 7 vechi si-au pierdut trimiterile fisier:linie din
legende; toate le aveau.

Doua serii partajate (bh_ansamblu si mbt_confluente expun serie()), deci
un trade nu poate avea doua variante. Oglinda pe short e trade-ul long
oglindit in jurul lui K=162.30, deci riscul ramane 1.30 in ambele.

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

280 lines
13 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",
caption="bara 13: 42.95 peste 42.90, volum bun.",
)
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 fără nivel",
caption="bara 13: 45.15 peste 45.10, 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: locația la un nivel major, scăderea dinainte și volumul; "
"trimiterile stau în fișa de surse.")
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",
"Mediocru — hammer 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")