Files
atm-curs-dl/gen/strategii/buy_doji_labu.py
Claude Agent 8b7b5f6b44 buy_doji: refacut ca tutorial pe 3 niveluri, cu diagrame adnotate
Userul a respins mostra de format tutorial: prea lunga, prea multe
trimiteri in text, termeni tehnici nedefiniti, prea multe cifre fara
desen. Cerintele lui sunt acum contract in STYLE.md (FORMAT TUTORIAL v2).

Textul: 8430 -> 4190 de cuvinte. Nivel 1 (din avion, cu mini-glosar) ->
nivel 2 (6 pasi mecanici, o diagrama fiecare) -> oglinda short -> nivel 3
(rafinari). Trimiterile fisier:linie ies din corp si se aduna la final,
cu legenda de prescurtari. Doua tabele in tot documentul.

Toate cele 14 diagrame refacute: zone dreptunghiulare de evidentiere si
sageti care spun si de ce, nu doar ce. svg_candles primeste Zone si
Arrow; render() nu mai muta obiectul la iesire.

Trei corecturi de continut:
- un singur trade prin toata procedura (doji 50.10/49.25, intrare 50.40,
  stop 49.20, 1R 1.20, TP1 51.60); seria e acum o functie comuna,
  importata de stop/transe/iesire
- short: stopul urca la 100.15, ca sa fie deasupra maximului cum cere
  regula oglindita; cifra cursului ramane in text ca observatie
- LABU: trei minime dar doua atingeri ale pivotului 86.00, ceea ce
  impaca "triple bottom" cu "al doilea test", ambele din transcriere

Reparat si bug-ul vechi din buy_doji_sell.svg: titlul peste eticheta
dojiului.

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

288 lines
15 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.

#!/usr/bin/env python3
"""LABU — Buy Doji la pivotul monthly (Nivel 2.5).
Citește `gen/strategii/buy_doji_labu.spec.txt` și scrie
`gen/strategii/buy_doji_labu.svg`: 57 de candele daily, downtrend → triple
bottom (trei minime aproape egale, dar pivotul lunar atins doar de două ori)
→ doji la pivotul monthly 86.00 → confirmare (close 89.50 peste high-ul
dojiului) → bull reversal → higher high, cu SL 81.45 (−9 %) și
TP 116.35 (+30 %).
Toate cifrele din desen vin din specificație: pivot 86.00, intrare 89.50,
SL 81.45, TP 116.35, high/low-ul dojiului (86.30 / 84.20) și low-urile
triple bottom-ului (82.40 / 81.45 / 81.45). Procentele 9 % și 30 % sunt ale
cursului (reale); nivelurile de preț sunt construite de sumarizator —
legenda o spune explicit. Capitolul spune și „triple bottom", și „doji la al
doilea test al pivotului": desenul le satisface pe amândouă — trei minime în
zona 81-83, dintre care doar primele două vin după o urcare peste 86.00.
Rulare: python3 gen/strategii/buy_doji_labu.py
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Arrow, Chart, Level, MA, Mark, Zone, from_closes # noqa: E402
OUT = Path(__file__).with_suffix(".svg")
# --- nivelurile din specificație -------------------------------------------
PIVOT = 86.00 # pivot monthly (construit)
ENTRY = 89.50 # intrare = close-ul confirmării (construit)
SL = 81.45 # SL = −9 % față de intrare (construit; procentul e real)
TP = 116.35 # TP = +30 % față de intrare (construit; procentul e real)
DOJI_H, DOJI_L = 86.80, 84.90
I_DOJI, I_CONF = 33, 34
B1, B2, B3 = 16, 25, 31 # cele trei picioare ale triplului fund
# (B1, B2 = testele pivotului; B3 rămâne sub el)
I_RETEST = 37 # revenirea care testează intrarea și ține
FLAG = (42, 45) # consolidarea scurtă (bull flag)
I_TP = 55 # bara pe care prețul atinge 116.35
# --- seria de close-uri -----------------------------------------------------
# Textul capitolului e sursa: „triple bottom" (trei minime aproape egale) și
# „doji la al doilea test al pivotului". Nu se bat cap în cap: sunt trei
# minime de preț, dar doar două dintre ele vin după o urcare peste 86.00 —
# al treilea e o alunecare care nu mai atinge pivotul de sus în jos.
# Restul: „+30 % într-o lună" (21 de bare daily de la confirmare) și etapele
# „retest → bull flag → breakout". Prețurile sunt construite în jurul
# nivelurilor deja folosite în capitol (86.00 / 89.50 / 81.45 / 97.55 / 116.35).
closes = [
# downtrend 0-14
108.0, 106.5, 105.0, 103.0, 101.5, 100.0, 98.0, 96.5, 95.0, 93.5,
92.5, 91.0, 89.5, 87.5, 85.5,
# 15-16: primul test al pivotului (bottom 1, low 82.40)
83.5, 82.80,
# 17-20: revenire peste pivot (88.90 high) — de aici vine al doilea test
84.5, 86.5, 88.0, 88.5,
# 21-24: coboară înapoi prin pivot: al doilea test al lui 86.00
87.0, 85.5, 84.0, 82.8,
# 25: bottom 2 (low 82.00), la capătul celui de-al doilea test
82.30,
# 26-29: un pull-up scurt care NU mai ajunge la pivot (high max 85.80)
83.5, 84.8, 85.4, 84.2,
# 30: alunecă iar, fără să fi atins pivotul între timp
83.0,
# 31: bottom 3 (low 81.45 = stopul) — al treilea minim, nu un test nou
81.90,
# 32: bara de întoarcere, închide peste pivot
86.10,
# 33 = doji: o 86.10, c 86.00, h 86.80, l 84.90 — corpul chiar pe pivot
86.00,
# 34 = confirmare: close 89.50 peste high-ul dojiului => intrare
89.50,
# 35-36: pornirea
92.0, 94.0,
# 37: retest — cade la 89.60, pe intrare, și ține
91.0,
# 38-41: reia urcarea și trece de ținta 1:1 (97.55)
93.5, 95.5, 98.5, 100.0,
# 42-45: bull flag — patru candele care se odihnesc
99.2, 99.8, 98.9, 99.5,
# 46-54: breakout și continuare
102.5, 104.0, 103.0, 106.0, 108.5, 107.5, 110.5, 112.5, 114.0,
# 55: +30 % față de intrare; 56: încă un higher high
116.35, 117.5,
]
assert len(closes) == 57, f"seria are {len(closes)} bare, aștept 57"
# wick-uri: (sus, jos) față de corp. Restul (0.4, 0.5); ajustate ca low-urile
# cerute de assert-uri să iasă exact.
wicks = [(0.4, 0.5)] * len(closes)
wicks[B1] = (0.4, 0.40) # bottom 1: low 82.40
wicks[17] = (0.4, 0.00) # low 82.80 > 82.40 (piciorul 1 rămâne minim local)
wicks[B2] = (0.4, 0.30) # bottom 2: low 82.00
wicks[26] = (0.4, 0.00) # low 82.30 > 82.00 (piciorul 2 rămâne minim local)
wicks[B3] = (0.4, 0.45) # bottom 3: low 81.45 = stopul
wicks[32] = (0.4, 0.00) # low 81.90 (nu coboară sub 81.45)
wicks[I_DOJI] = (0.70, 1.10) # doji: high 86.80, low 84.90
wicks[35] = (0.4, 0.00) # prima bară după intrare nu se mai duce sub 89.50
wicks[I_RETEST] = (0.4, 1.40) # retest: low 89.60, peste intrarea de 89.50
# volume construite: vânzare peste medie la cele două picioare, volum mare pe
# confirmare, volum mic pe flag. Nu sunt numere din curs.
vols = [
55, 52, 48, 50, 45, 47, 44, 46, 42, 44,
40, 42, 38, 41, 58,
62, 68, 50, 46, 44,
43, 45, 44, 47, 46,
45, 47, 52, 45, 44,
48, 70, 62, 58, 88,
76, 70, 58, 62, 66,
72, 70, 44, 40, 38,
42, 74, 70, 64, 68,
72, 66, 74, 78, 82,
90, 96,
]
assert len(vols) == len(closes), f"volume {len(vols)} != bare {len(closes)}"
cds = from_closes(closes, wicks=wicks, vols=vols, start=110.0)
assert abs(cds[0].o - 110.0) < 1e-9 and cds[0].c < cds[0].o, "prima bară nu e roșie"
# nota explicativă a dojiului (bloc stânga-jos, nu se suprapune cu candelele)
cds[I_DOJI].note = ("doji = corp mic (o 86.10, c 86.00): cumpărătorii și vânzătorii sunt la "
"egalitate, exact pe pivotul lunar 86.00")
TP1 = round(ENTRY + (ENTRY - SL), 2) # ținta 1:1: cât ai riscat
assert abs(TP1 - 97.55) < 1e-9, f"ținta 1:1 = {TP1}, capitolul spune 97.55"
R = (TP - ENTRY) / (ENTRY - SL)
assert 3.2 <= R <= 3.4, f"{R:.2f}R, capitolul spune „cam 3,3R”"
I_TP1 = next(i for i in range(35, len(cds)) if cds[i].h >= TP1) # prima bară peste 1:1
cds[I_TP1].note = (f"ținta 1:1 ({TP1:.2f}) e atinsă pe bara {I_TP1}: acolo vinzi o treime, "
"restul merge mai departe — de asta nu vinzi tot la 1:1")
# --- assert-urile din specificație (toate, ca assert reale) ----------------
# 1. pivot 86.00 e straddled de doji
assert cds[I_DOJI].l <= PIVOT <= cds[I_DOJI].h, "doji-ul nu straddlează 86.00"
# corpul dojiului stă pe pivot, nu la un dolar sub el (eticheta spune „exact pe")
assert min(cds[I_DOJI].o, cds[I_DOJI].c) <= PIVOT <= max(cds[I_DOJI].o, cds[I_DOJI].c) + 0.15, \
f"corpul dojiului ({cds[I_DOJI].o}/{cds[I_DOJI].c}) nu e pe pivotul {PIVOT}"
# 2. confirmarea: close[34] == 89.50 == intrare și > high-ul dojiului
assert abs(cds[I_CONF].c - ENTRY) < 1e-9, f"close[{I_CONF}]={cds[I_CONF].c} != intrare 89.50"
assert cds[I_CONF].c > cds[I_DOJI].h, "confirmarea nu închide peste high-ul dojiului"
# 3. SL = extrema ultimelor 4 candele de la confirmare
min_win = min(c.l for c in cds[I_CONF - 4:I_CONF + 1])
assert abs(min_win - SL) < 1e-9, f"min low[{I_CONF - 4}:{I_CONF + 1}]={min_win} != SL {SL}"
# 4./5. procentele REALE ale cursului
assert round((ENTRY - SL) / ENTRY * 100, 1) == 9.0, "stopul nu iese −9 %"
assert round((TP - ENTRY) / ENTRY * 100, 1) == 30.0, "TP nu iese +30 %"
# 6. triple bottom, dar pivotul atins de două ori — aserțiunea care ține
# laolaltă cele două formulări din capitol („trei minime aproape egale" și
# „doji la al doilea test al pivotului 86.00").
lows = [c.l for c in cds]
bottom_idx = [i for i in range(1, len(cds) - 1)
if lows[i] < lows[i - 1] and lows[i] < lows[i + 1]
and lows[i] < PIVOT and 81.0 <= lows[i] <= 83.0]
assert bottom_idx == [B1, B2, B3], f"minimele fundului: {bottom_idx}, aștept [{B1}, {B2}, {B3}]"
assert len(bottom_idx) == 3, f"{len(bottom_idx)} minime locale în zona fundului, aștept 3"
b1, b2, b3 = (lows[i] for i in bottom_idx)
# „aproape egale": nici unul nu e cu mai mult de 2 % sub primul
assert max(b1, b2, b3) - min(b1, b2, b3) <= 0.02 * b1, \
f"minimele nu sunt aproape egale: {b1}/{b2}/{b3}"
assert abs(b1 - 82.40) < 1e-9 and abs(b2 - 82.00) < 1e-9 and abs(b3 - SL) < 1e-9, \
f"low-urile triplului fund: {b1}/{b2}/{b3}"
# un minim e „atingere a pivotului" doar dacă prețul a fost peste 86.00 după
# minimul dinainte: coboară prin pivot ca să ajungă acolo. Al treilea nu e.
tests = []
for k, i in enumerate(bottom_idx):
prev = bottom_idx[k - 1] + 1 if k else 0
if max(c.h for c in cds[prev:i]) >= PIVOT:
tests.append(i)
assert tests == [B1, B2], f"atingeri ale pivotului la {tests}, aștept [{B1}, {B2}]"
assert len(tests) == 2, f"{len(tests)} atingeri ale pivotului 86.00, aștept 2"
# între primele două picioare prețul urcă clar peste pivot (al doilea test)
assert max(c.h for c in cds[B1 + 1:B2]) > PIVOT + 1.5, "nu e revenire peste pivot înainte de B2"
# între al doilea și al treilea NU mai ajunge la pivot: al treilea nu e test
assert max(c.h for c in cds[B2 + 1:B3]) < PIVOT, \
f"pull-up-ul dintre B2 și B3 atinge pivotul ({max(c.h for c in cds[B2 + 1:B3]):.2f})"
# 7. TP atins la ~o lună de bursă de la confirmare (capitolul: „într-o lună")
assert abs(cds[I_TP].c - TP) < 1e-9, f"close[{I_TP}]={cds[I_TP].c} != {TP}"
assert max(c.h for c in cds[I_CONF + 1:I_TP]) < TP, "TP atins mai devreme de bara 55"
bars_to_tp = I_TP - I_CONF
assert 20 <= bars_to_tp <= 22, f"{bars_to_tp} bare până la +30 %, aștept ~21 (o lună)"
# 8. doji: corp <= 10 % din amplitudine
assert abs(cds[I_DOJI].o - cds[I_DOJI].c) <= 0.10 * (cds[I_DOJI].h - cds[I_DOJI].l), \
"bara doji nu e doji"
assert abs(cds[I_DOJI].h - DOJI_H) < 1e-9 and abs(cds[I_DOJI].l - DOJI_L) < 1e-9, \
"doji h/l nu ies 86.80/84.90"
# 9. retestul: revine până pe intrare și ține (nu o rupe)
assert ENTRY <= cds[I_RETEST].l <= ENTRY + 0.5, \
f"retestul nu atinge intrarea: low {cds[I_RETEST].l}"
assert min(c.l for c in cds[I_CONF + 1:]) >= ENTRY, "prețul cade sub intrare după confirmare"
# 10. bull flag: 4 candele strânse, sub high-ul barei dinainte, urmate de breakout
f0, f1 = FLAG
flag_h = max(c.h for c in cds[f0:f1 + 1])
flag_l = min(c.l for c in cds[f0:f1 + 1])
assert flag_h - flag_l <= 2.5, f"flag prea larg: {flag_h - flag_l:.2f}"
assert flag_h <= cds[f0 - 1].h, "flagul face high nou — nu mai e consolidare"
assert cds[f1 + 1].c > flag_h, "nu există breakout după flag"
# în plus: confirmarea vine cu volum peste medie, flagul cu volum sub medie
mean_vol = sum(vols) / len(vols)
assert vols[I_CONF] > mean_vol, f"volumul confirmării {vols[I_CONF]} <= media {mean_vol:.0f}"
assert max(vols[f0:f1 + 1]) < mean_vol, "flagul nu vine pe volum scăzut"
entry = cds[34].c
w = 990 # 57 bare × gap 15 + loc pentru etichetele din dreapta
ch = Chart(
candles=cds,
levels=[Level(PIVOT, "pivot lunar 86.00", "#777", dash="4 3", label_at="left"),
Level(SL, "stop 81.45 = 9 % sub intrare (procent real)", "#c00", label_at="left"),
Level(ENTRY, "intrare 89.50", "#188a3e"),
Level(TP1, f"ținta 1:1 {TP1:.2f}", "#777", dash="2 3", label_at="left"),
Level(TP, "116.35 = +30 % față de intrare (procent real)", "#1a5fb4", label_at="left")],
zones=[Zone(0, B3, "1. setup: scădere lungă, apoi trei minime aproape egale — pivotul 86.00 atins de două ori", "#777"),
Zone(I_DOJI, I_CONF, "2. doji + confirmare", "#b06000"),
# p_top sub 116.35: altfel eticheta zonei 3 se așază peste linia țintei
Zone(I_CONF + 1, I_TP - 1, "3. trendul pornește", "#188a3e", p_top=113.0),
Zone(I_TP, len(cds) - 1, "4. ținta", "#1a5fb4")],
arrows=[Arrow(I_CONF, "aici intri, la close 89.50:\nabia acum știi că au câștigat cumpărătorii",
ENTRY, where="above", color="#188a3e", dx=-150),
Arrow(B3, "al treilea minim: 81.45,\nfără o nouă atingere a pivotului\naici pui stopul, sub minime",
SL, where="below", color="#c00", dx=-40),
Arrow(I_RETEST, "retest: cade înapoi pe intrare\nși ține — trendul e sănătos",
cds[I_RETEST].l, where="below", color="#188a3e", dx=110, dy=56),
Arrow(FLAG[0] + 1, "aici se odihnește, nu se strică:\nbull flag, pe volum mic",
cds[FLAG[0] + 1].h, where="above", color="#188a3e", dx=-20),
Arrow(I_TP, f"a ajuns la 116.35: +30 % în 21 de zile de bursă,\ncam {R:.1f}R din cât ai riscat".replace("3.3R", "3,3R"),
TP, where="left", color="#1a5fb4", dx=-30, dy=-40)],
marks=[Mark(B1, f"test 1 · {lows[B1]:.2f}", "#c00", "below"),
Mark(B2, f"test 2 · {lows[B2]:.2f}", "#c00", "below")],
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=("Procentele (stop 9 %, +30 %) sunt ale cursului; "
"nivelurile de preț sunt puse de sumarizator."),
)
svg = ch.render()
assert "\n\n" not in svg and "<svg" in svg, "SVG invalid (linie goală?)"
# --- citirea SVG-ului: etichete în panză, fără suprapuneri -----------------
TEXT = re.compile(r'<text x="([-\d.]+)" y="([-\d.]+)"([^>]*)>(.*?)</text>', re.S)
W_SVG, H = (float(x) for x in re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', svg).groups())
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_SVG + 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 ".md:" not in svg and "Surse" not in svg, "trimiteri la surse în SVG"
assert xs[-1] + ch.body_w / 2 < W_SVG, 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"
for a, b in zip(cds, cds[1:]):
assert abs(a.c - b.o) < 1e-9, "open != close-ul barei anterioare"
print(f"LABU: {len(cds)} bare, doji {cds[33].l:.2f}-{cds[33].h:.2f} peste pivot "
f"{PIVOT:.2f}, intrare {entry:.2f}, SL {SL:.2f} (−9 %), TP {TP:.2f} (+30 %)")
print(f"triple bottom: {b1:.2f} / {b2:.2f} / {b3:.2f}; pivotul 86.00 atins de "
f"{len(tests)} ori (barele {tests}); +30 % după {bars_to_tp} bare "
f"volum confirmare {vols[34]} > medie {mean_vol:.0f}")
print(f"scris {OUT} ({W_SVG:.0f}x{H:.0f}); {len(labels)} etichete verificate, fără suprapuneri")
OUT.write_text(svg, encoding="utf-8")