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
280 lines
13 KiB
Python
280 lines
13 KiB
Python
"""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")
|