Cea mai grava, si cea care a cerut refacerea seriei: exemplul central de MBT-10 era luat de pe 60 de minute si bagat in procedura de daily, cu barele numite "zile", desi sursa spune explicit de ce nu merge pe daily. Pe langa asta, suportul fusese schimbat din 88.00 (testat de trei ori, cu compresie, cum e in sursa) in 87.50 cu un double bottom si o divergenta de volum imprumutate din alt exemplu. Seria e refacuta pe exemplul materialului, cu assert pe cele trei atingeri ale lui 88.00 si pe compresia varfurilor. Adaosurile au fost scoase, nu doar marcate: cu ele in desen, suportul din sursa n-ar fi avut sens. Aritmetica a supravietuit neatinsa. Restul: stopul de 60.40 sta acum sub o structura reala; pivotul daily nu mai e o linie fixa peste tot graficul, ci un segment peste ziua intrarii; SL-ul de pe LABU respecta regula proprie de 5 centi; citatul de la A2 nu mai e dat ca al autorului; pragul de 1-2% isi spune originea (cursul da 0,5%, pe 60 min); o singura conventie de distanta, impartirea la nivel; swing low definit la fel in pas si in glosar. eticheta_libera a prins 7 suprapuneri reale. La bh_aplus a trebuit rulat pe fiecare panou separat: wrapper-ul le muta cu <g transform>, pe care unealta nu-l citeste - de notat pentru data viitoare. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
190 lines
9.1 KiB
Python
190 lines
9.1 KiB
Python
#!/usr/bin/env python3
|
||
"""LABU — buy hammer la suportul monthly, +20 % în jumătate de lună (Nivel 2.5).
|
||
|
||
Citește `gen/strategii/hammer_mbt10_bh_labu.spec.txt` și scrie
|
||
`gen/strategii/hammer_mbt10_bh_labu.svg`: 38 de candele daily, downtrend +
|
||
consolidare până pe suportul monthly 80.00, hammer chiar pe nivel
|
||
(o 80.80 / h 80.90 / l 79.60 / c 80.90), confirmarea de la 81.30 (close peste
|
||
high-ul hammer-ului) și urcarea în ~11 bare până la 97.56 = +20,0 % față de
|
||
intrare.
|
||
|
||
REAL din curs (`18_2025-09-23.md:721-726`): că e buy hammer la suportul
|
||
monthly și că a dat +20 % în jumătate de lună. CONSTRUITE de sumarizator:
|
||
suportul 80.00, cifrele hammer-ului (80.80/80.90/79.60), intrarea 81.30, SL
|
||
79.55 (low − 5 cenți, regula proprie a capitolului) și seria de prețuri —
|
||
materialul nu dă entry, SL sau TP pentru cazul
|
||
ăsta. Legenda din desen spune exact asta.
|
||
|
||
Rulare: python3 gen/strategii/hammer_mbt10_bh_labu.py
|
||
"""
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, "/workspace/atm-curs-dl")
|
||
sys.path.insert(0, "/workspace/atm-curs-dl/gen")
|
||
from eticheta_libera import eticheta_libera # noqa: E402
|
||
from svg_candles import Chart, Level, MA, Mark, from_closes # noqa: E402
|
||
|
||
OUT = Path(__file__).with_suffix(".svg")
|
||
|
||
# --- nivelurile din specificație (toate CONSTRUITE) ------------------------
|
||
SUPPORT = 80.00 # suport monthly (construit)
|
||
HAMMER_H = 80.90 # high-ul hammer-ului = close (H=C)
|
||
HAMMER_L = 79.60 # low-ul hammer-ului
|
||
ENTRY = 81.30 # intrare = close-ul confirmării, peste 80.90 (construit)
|
||
SL = 79.55 # low 79.60 − 5 cenți: marja folosită în tot capitolul
|
||
PEAK = 97.56 # max close de după intrare: 97.56 / 81.30 = +20,0 % (real)
|
||
|
||
# --- seria de close-uri din specificație (38 bare, o = close precedent) ----
|
||
closes = [
|
||
# downtrend + consolidare 0-22
|
||
92.00, 90.50, 91.20, 89.40, 88.10, 88.60, 87.00, 85.80, 86.30, 84.90,
|
||
83.60, 82.40, 83.00, 81.80, 80.70, 81.20, 80.20, 80.80, 81.50, 80.40,
|
||
80.10, 80.60, 80.80,
|
||
80.90, # 23 = hammer: o 80.80, c 80.90, h 80.90, l 79.60
|
||
81.30, # 24 = confirmare, close > high[23] = intrare
|
||
# 25..37 = 13 bare de urcare; max close 97.56 / 81.30 - 1 = +20,0 %
|
||
81.90, 82.60, 83.40, 84.20, 85.10, 86.00, 87.00, 88.10, 89.30, 90.60,
|
||
92.00, 93.50, 97.56,
|
||
]
|
||
assert len(closes) == 38, f"seria are {len(closes)} bare, spec cere 38"
|
||
|
||
# wick-uri: (up, down). Restul (0.30, 0.40); hammer-ul are fitil jos 1.20 și
|
||
# zero fitil sus ca low-ul/high-urile cerute de asserts să iasă exact.
|
||
wicks = [(0.30, 0.40)] * len(closes)
|
||
wicks[23] = (0.00, 1.20) # hammer: h 80.90, l 79.60
|
||
|
||
# volume construite (nu sunt cifre de curs, doar panoul de volum cerut de
|
||
# stil): vânzare în creștere spre suport, explozie la hammer + confirmare.
|
||
vols = [
|
||
52, 50, 47, 49, 45, 46, 43, 45, 41, 43,
|
||
44, 47, 42, 45, 48, 46, 50, 44, 43, 46,
|
||
51, 49, 55, 88, 84,
|
||
70, 62, 58, 54, 52, 50, 48, 46, 45, 47,
|
||
55, 62, 72,
|
||
]
|
||
assert len(vols) == len(closes), f"volume {len(vols)} != bare {len(closes)}"
|
||
|
||
cds = from_closes(closes, wicks=wicks, vols=vols, start=94.00)
|
||
assert abs(cds[0].o - 94.00) < 1e-9 and cds[0].c < cds[0].o, "prima bară nu e roșie"
|
||
for a, b in zip(cds, cds[1:]):
|
||
assert abs(a.c - b.o) < 1e-9, "open != close-ul barei anterioare"
|
||
|
||
# --- assert-urile din specificație (toate, ca assert reale) ----------------
|
||
# 1. hammer-ul stă pe suportul monthly 80.00
|
||
assert cds[23].l == 79.60 <= SUPPORT <= cds[23].h == HAMMER_H, \
|
||
f"hammer 79.60 <= 80.00 <= 80.90 nu iese: {cds[23].l}/{cds[23].h}"
|
||
|
||
# 2. forma hammer-ului: o = close[22] = 80.80, corp 0.10, fitil jos 1.20,
|
||
# 1.20 >= 2 * 0.10, close == high (fără fitil sus)
|
||
assert cds[23].o == cds[22].c == 80.80, "open[23] nu e close[22] = 80.80"
|
||
body = abs(cds[23].c - cds[23].o)
|
||
lower_wick = min(cds[23].o, cds[23].c) - cds[23].l
|
||
upper_wick = cds[23].h - max(cds[23].o, cds[23].c)
|
||
assert abs(body - 0.10) < 1e-9, f"corpul hammer-ului e {body}, nu 0.10"
|
||
assert abs(lower_wick - 1.20) < 1e-9, f"fitilul jos e {lower_wick}, nu 1.20"
|
||
assert abs(upper_wick) < 1e-9, f"hammer-ul are fitil sus {upper_wick}"
|
||
assert lower_wick >= 2 * body, f"1.20 < 2 * {body} (nu e hammer valid)"
|
||
assert cds[23].c == cds[23].h == HAMMER_H, "close[23] != high[23] == 80.90"
|
||
|
||
# 3. confirmarea: close[24] == 81.30 == intrare și > high[23] == 80.90
|
||
assert abs(cds[24].c - ENTRY) < 1e-9, f"close[24]={cds[24].c} != intrare 81.30"
|
||
assert cds[24].c > cds[23].h == HAMMER_H, "confirmarea nu închide peste 80.90"
|
||
|
||
# 4. SL = low-ul hammer-ului (cel mai jos din 22:25) minus marja de 5 cenți.
|
||
# Prețurile LABU sunt construite, deci se aplică regula proprie din capitol
|
||
# (summaries/17_STRATEGII/06_hammer_mbt10.md, pasul A4): stopul nu stă
|
||
# exact pe low, ci cu 5 cenți sub el.
|
||
min_22_25 = min(c.l for c in cds[22:25])
|
||
assert abs(min_22_25 - cds[23].l) < 1e-9, f"min low[22:25]={min_22_25} != low[23]"
|
||
assert abs(SL - (cds[23].l - 0.05)) < 1e-9, f"SL {SL} nu e low 79.60 − 5 cenți"
|
||
assert SL < min_22_25, "linia stopului trebuie să stea sub toate fitilele"
|
||
|
||
# 5. procentul REAL al cursului: +20,0 % de la intrare la vârf
|
||
peak = max(c.c for c in cds[25:38])
|
||
pct = round((peak / ENTRY - 1) * 100, 1)
|
||
assert abs(peak - PEAK) < 1e-9, f"max close[25:38]={peak} != 97.56"
|
||
assert pct == 20.0, f"procentul iese {pct}, nu +20,0 %"
|
||
assert abs(peak / ENTRY - 1.20) < 1e-9, "97.56 / 81.30 nu e exact 1.20"
|
||
|
||
# 6. downtrend/consolidare înainte de hammer
|
||
assert cds[23].c < cds[0].c - 8, f"close[23]={cds[23].c} nu e cu 8 sub close[0]"
|
||
|
||
# 7. ordinea nivelurilor
|
||
assert SL < ENTRY < peak, f"SL/entry/vârf: {SL}/{ENTRY}/{peak}"
|
||
|
||
# --- verificarea serii (stil + geometrie) ----------------------------------
|
||
entry = cds[24].c
|
||
w = 760
|
||
ch = Chart(
|
||
candles=cds,
|
||
levels=[
|
||
Level(SUPPORT, "suport monthly 80.00", "#777",
|
||
dash="4 3", label_at="left"),
|
||
Level(SL, "SL 79.55 = low 79.60 − 5 cenți (construit)", "#c00",
|
||
label_at="right"),
|
||
Level(ENTRY, "intrare 81.30 (construit)", "#188a3e", label_at="right"),
|
||
],
|
||
marks=[
|
||
Mark(23, "hammer 80.90 / 79.60 (construit)", "#b06000", "below"),
|
||
Mark(37, f"+20 % (cifra cursului): {peak:.2f} / {ENTRY:.2f}",
|
||
"#188a3e", "above"),
|
||
],
|
||
mas=[MA(20, "#b06000", dash="4 3", text="SMA-20")],
|
||
scale=8.0, gap=15, body_w=9, left=44, width=w, vol_h=70,
|
||
vol_label="volum", title="LABU — daily",
|
||
caption="nivelurile de preț sunt construite de sumarizator.",
|
||
)
|
||
# note pe bare: explicațiile hammer-ului/confirmării și legenda obligatorie
|
||
# (textul lung de nivel ar intra peste candele, deci stă în blocul de jos).
|
||
cds[0].note = ("bara 23 = hammer: o 80.80, h 80.90, l 79.60, c 80.90 — "
|
||
"corp 0.10, fitil jos 1.20 (H=C), pe suportul 80.00")
|
||
cds[1].note = ("bara 24 = confirmare: close 81.30 — close peste high-ul "
|
||
"hammer-ului 80.90 = intrare 81.30 (construit)")
|
||
cds[2].note = ("Procentul (+20 % în jumătate de lună) și contextul (suport "
|
||
"monthly) sunt ale cursului;")
|
||
|
||
svg = ch.render()
|
||
assert "\n\n" not in svg and "<svg" in svg, "SVG invalid (linie goală?)"
|
||
# eticheta de stop stă lipită de linia ei prin construcție: verificăm doar
|
||
# candelele și media mobilă de sub ea
|
||
eticheta_libera(svg, "bh_labu",
|
||
{"SL 79.55 = low 79.60 − 5 cenți (construit)",
|
||
"hammer 80.90 / 79.60 (construit)"}, rects=True, segmente=False)
|
||
|
||
# --- citirea SVG-ului: etichete în panză, fără suprapuneri -----------------
|
||
TEXT = re.compile(r'<text x="([-\d.]+)" y="([-\d.]+)"([^>]*)>(.*?)</text>', re.S)
|
||
H = float(re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', svg).group(2))
|
||
labels = []
|
||
for m in TEXT.finditer(svg):
|
||
x, y, attrs, s = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
|
||
anchor = "end" if 'text-anchor="end"' in attrs else \
|
||
("middle" if 'text-anchor="middle"' in attrs else "start")
|
||
bold = "bold" in attrs
|
||
wpx = len(s) * 12 * (0.62 if bold else 0.52)
|
||
left = x - wpx if anchor == "end" else (x - wpx / 2 if anchor == "middle" else x)
|
||
labels.append((left, left + wpx, y, s))
|
||
|
||
for left, right, y, s in labels:
|
||
assert left >= -2 and right <= w + 2, f"text iese din panză: {s!r} [{left:.0f},{right:.0f}]"
|
||
assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y:.0f})"
|
||
|
||
for i in range(len(labels)):
|
||
li, ri, yi, si = labels[i]
|
||
for j in range(i + 1, len(labels)):
|
||
lj, rj, yj, sj = labels[j]
|
||
if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
|
||
raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}\n {labels[i]}\n {labels[j]}")
|
||
|
||
# candelele sunt o serie continuă, lipită
|
||
xs = [ch._cx(i) for i in range(len(cds))]
|
||
assert xs[-1] + ch.body_w / 2 < w, f"ultima candelă iese din panză (x={xs[-1]})"
|
||
assert all(xs[i + 1] - xs[i] == ch.gap for i in range(len(xs) - 1)), "serie cu goluri"
|
||
|
||
print(f"LABU: {len(cds)} bare, hammer {cds[23].l:.2f}/{cds[23].h:.2f} pe suport "
|
||
f"{SUPPORT:.2f}, intrare {entry:.2f}, SL {SL:.2f}, vârf {peak:.2f}")
|
||
print(f"procent: {peak:.2f} / {entry:.2f} - 1 = +{pct:.1f} % (cifra cursului)")
|
||
print(f"scris {OUT} ({w}x{H:.0f}); {len(labels)} etichete verificate, fără suprapuneri")
|
||
OUT.write_text(svg, encoding="utf-8")
|