Files
atm-curs-dl/gen/strategii/buy_doji_aplus.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

281 lines
14 KiB
Python

"""Buy Doji — A+ vs. mediocru (două panouri, exemple construite).
Citește `gen/strategii/buy_doji_aplus.spec.txt` (marcajul `<!-- DIAGRAMA: aplus -->`).
Același pattern (trend → doji → confirmare), context opus:
- stânga (A+): downtrend 52 → 42.60, doji cu low 41.95 chiar pe suportul
42.00 (0.12 % sub nivel), confirmare close 43.60 pe volum peste media
ultimelor 10 bare — acumulare;
- dreapta (NU): uptrend 70 → 76.80, gap-up la open 78.20, doji lipit de
rezistența 78.50, confirmare close 79.30 pe volum sub medie („varză"), apoi
prețul cade înapoi sub 78.00.
Nivelurile 42.00 / 78.50 și cifrele de preț sunt construite de mine ca să
susțină etichetele și assert-urile din spec (materialul nu dă prețuri pentru
acest contrast). Două `Chart`-uri randate separat, lipite într-un SVG wrapper:
coordonatele fiecărui panou rămân corecte prin construcție.
Rulează: python3 gen/strategii/buy_doji_aplus.py
Scrie: gen/strategii/buy_doji_aplus.svg
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Arrow, Candle, Chart, Level, Zone, from_closes # noqa: E402
OUT = Path(__file__).resolve().parent / "buy_doji_aplus.svg"
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
DOJI_A, CONF_A = 10, 11 # indicii în panoul A+
DOJI_N, CONF_N = 14, 15 # indicii în panoul NU
# ============================================================ PANOUL A+ (24 bare)
# 10 bare de downtrend de la 52.00 spre 42.60, apoi doji, apoi confirmare.
CLOSES_A = [
51.60, 50.80, 50.00, 49.20, 48.40, 47.60, 46.80, 45.50, 44.20, 42.60, # 0-9 downtrend
42.60, # 10 doji (open = close)
43.60, # 11 confirmare
44.10, 44.50, 44.20, 44.80, 45.20, 45.00, 45.50, 45.90, 46.20, 46.50, 46.80, 47.10, # 12-23 acumulare
]
assert len(CLOSES_A) == 24, len(CLOSES_A)
WICKS_A = [(0.30, 0.30)] * 24
WICKS_A[DOJI_A] = (0.30, 0.65) # high 42.90, low 41.95 (pe suportul 42.00)
WICKS_A[CONF_A] = (0.25, 0.30) # high 43.85
VOLS_A = [56, 53, 51, 49, 47, 45, 44, 42, 41, 39, # 0-9 vânzare în scădere
60, # 10 doji: exces de vânzare
86, # 11 confirmare: volum mare
72, 75, 70, 78, 82, 80, 85, 88, 92, 95, 98, 100] # 12-23 acumulare
cds_a = from_closes(CLOSES_A, wicks=WICKS_A, vols=VOLS_A, start=52.00)
doji_a, conf_a = cds_a[DOJI_A], cds_a[CONF_A]
# ============================================================ PANOUL NU (24 bare)
# uptrend 70 → 76.80 (14 bare), gap-up la open 78.20 + doji, confirmare 79.30
# pe volum slab, apoi cădere înapoi sub 78.00. Lista e manuală pentru că gap-ul
# (open > high-ul barei precedente) nu iese din `from_closes`.
BARE_NU = [
# o, h, l, c, vol
(70.00, 70.70, 69.80, 70.50, 48), (70.50, 71.20, 70.30, 71.00, 50),
(71.00, 71.75, 70.85, 71.55, 49), (71.55, 72.30, 71.40, 72.10, 52),
(72.10, 72.80, 71.95, 72.60, 51), (72.60, 73.30, 72.45, 73.10, 54),
(73.10, 73.80, 72.95, 73.60, 53), (73.60, 74.25, 73.45, 74.05, 56),
(74.05, 74.70, 73.90, 74.50, 55), (74.50, 75.15, 74.35, 74.95, 58),
(74.95, 75.60, 74.80, 75.40, 57), (75.40, 76.00, 75.25, 75.80, 60),
(75.80, 76.40, 75.65, 76.20, 59), (76.20, 76.80, 76.05, 76.80, 62), # 13: high 76.80
(78.20, 78.50, 78.10, 78.20, 58), # 14: GAP-UP + doji (o = c = 78.20)
(78.20, 79.50, 78.10, 79.30, 30), # 15: confirmare, volum slab
(79.30, 79.40, 78.40, 78.60, 55), (78.60, 78.70, 77.80, 78.00, 60),
(78.00, 78.10, 77.20, 77.40, 64), (77.40, 77.55, 76.80, 77.00, 62),
(77.00, 77.15, 76.40, 76.60, 66), (76.60, 76.75, 76.10, 76.30, 63),
(76.30, 76.40, 75.80, 76.00, 65), (76.00, 76.15, 75.50, 75.70, 61),
]
assert len(BARE_NU) == 24, len(BARE_NU)
cds_n = [Candle(o, h, l, c, vol=v) for (o, h, l, c, v) in BARE_NU]
doji_n, conf_n = cds_n[DOJI_N], cds_n[CONF_N]
# ============================================================ assert-urile din spec
# 1. A+: low-ul dojiului e la ≤ 0,5 % de 42.00 (41.95 e la 0.12 %)
dist_a = abs(doji_a.l - 42.00) / 42.00
assert dist_a <= 0.005, f"low doji A+ {doji_a.l} la {dist_a * 100:.2f} % de 42.00"
assert abs(doji_a.l - 41.95) < 1e-9, f"low doji A+ = {doji_a.l}, aștept 41.95"
# 2. A+: volumul pe bara de confirmare > 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 {VOLS_A[CONF_A]} <= media {media_a}"
# 3. NU: doji-ul apare după un gap-up (open doji > high bara precedentă)
assert cds_n[DOJI_N].o > cds_n[DOJI_N - 1].h, "doji NU nu e după gap-up"
assert abs(cds_n[DOJI_N].o - 78.20) < 1e-9 and abs(cds_n[DOJI_N - 1].h - 76.80) < 1e-9
# 4. NU: |high doji - 78.50| / 78.50 <= 0.005
dist_n = abs(doji_n.h - 78.50) / 78.50
assert dist_n <= 0.005, f"high doji NU {doji_n.h} la {dist_n * 100:.2f} % de 78.50"
# 5. NU: volumul pe bara de confirmare < media pe 10 bare
media_n = sum(b[4] for b in BARE_NU[CONF_N - 10:CONF_N]) / 10
assert conf_n.vol < media_n, f"vol confirmare NU {conf_n.vol} >= media {media_n}"
# 6. ambele: close-ul de confirmare > high-ul dojiului (tehnic valid)
assert conf_a.c > doji_a.h, f"A+ close {conf_a.c} <= high doji {doji_a.h}"
assert conf_n.c > doji_n.h, f"NU close {conf_n.c} <= high doji {doji_n.h}"
assert abs(conf_a.c - 43.60) < 1e-9 and abs(conf_n.c - 79.30) < 1e-9
# 7. ambele doji: abs(o-c) <= 0.10*(h-l)
for tag, d in (("A+", doji_a), ("NU", doji_n)):
assert abs(d.o - d.c) <= 0.10 * (d.h - d.l), f"bara doji {tag} nu e doji"
# suplimentare: structura promisă de spec
assert min(c.l for c in cds_a[:10]) > doji_a.l, "low-ul dojiului A+ nu e minimul"
assert cds_a[9].c < cds_a[0].c - 8, "downtrend A+ prea mic"
assert cds_n[CONF_N].c > 78.50 and cds_n[-1].c < 78.00, "NU nu cade sub 78.00 la final"
assert cds_n[13].c < 78.00 < cds_n[DOJI_N].o, "NU: uptrendul nu se oprește sub 78.00"
print(f"A+ doji {doji_a.h:.2f}/{doji_a.l:.2f} conf {conf_a.c:.2f} "
f"vol {VOLS_A[CONF_A]} > medie {media_a:.1f}")
print(f"NU doji {doji_n.h:.2f}/{doji_n.l:.2f} conf {conf_n.c:.2f} "
f"vol {conf_n.vol} < medie {media_n:.1f}")
# ============================================================ desenele celor două panouri
PW = 560 # lățimea fiecărui panou
ch_a = Chart(
candles=cds_a,
levels=[Level(42.00, "suport 42.00", GREEN, label_at="left")],
zones=[Zone(DOJI_A, DOJI_A, "", GREEN, p_top=43.2, p_bot=41.7)],
arrows=[Arrow(DOJI_A, "doji chiar pe nivel:\nvânzarea s-a oprit pe suport",
where="below", color=GREEN, dx=-40),
Arrow(CONF_A, "confirmarea, pe volum mare:\ncumpărătorii mari intră",
where="above", color=GREEN, dx=60)],
mas=[], scale=22.0, gap=15, body_w=9, left=44, width=PW, vol_h=60,
vol_label="volum", title="A+ — doji la suport, volum în acumulare",
)
ch_n = Chart(
candles=cds_n,
levels=[Level(78.50, "rezistență 78.50", RED, label_at="right")],
# eticheta zonei ar urca peste titlul panoului (zona e chiar în vârf):
# textul stă pe săgeata din stânga
zones=[Zone(DOJI_N - 1, CONF_N, "", RED, p_top=79.62)],
arrows=[Arrow(DOJI_N, "gap-up în rezistență:\naici cumpără cei întârziați",
where="left", color=RED, dx=-30, dy=30),
Arrow(18, "cade înapoi sub 78.00:\nsemnalul a mințit", where="below", color=RED, dx=40)],
mas=[], scale=22.0, gap=15, body_w=9, left=44, width=PW, vol_h=60,
vol_label="volum", title="Mediocru — gap-up în rezistență, volum varză",
)
# ============================================================ 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))
def vol_note(svg: str, i0: int, i1: int, text: str, color: str, box: bool) -> str:
"""Evidențiere în panoul de volum (Zone/Arrow din svg_candles lucrează doar pe preț).
Coordonatele se citesc din SVG-ul randat (barele de volum sunt rect-urile cu
opacity 0.55, în ordinea candelelor), ca să nu depindă de deplasarea internă
a lui Chart. Eticheta stă sub linia de bază, unde nu acoperă nimic;
`box` = dreptunghi peste bare, altfel săgeată de jos în sus, sub bară.
"""
bars = [tuple(map(float, m)) for m in re.findall(
r'<rect x="([\d.]+)" y="([\d.]+)" width="([\d.]+)" height="([\d.]+)" '
r'fill="#[0-9a-f]+" opacity="0.55"/>', svg)]
base = bars[0][1] + bars[0][3]
x0, x1 = bars[i0][0] - 4, bars[i1][0] + bars[i1][2] + 4
top = min(b[1] for b in bars[i0:i1 + 1]) - 4
if box:
mark = (f'<rect x="{x0:.1f}" y="{top:.1f}" width="{x1 - x0:.1f}" height="{base - top:.1f}" '
f'fill="{color}" fill-opacity="0.12" stroke="{color}" stroke-width="1"/>')
ty = base + 15
else:
cx = (x0 + x1) / 2
mark = (f'<line x1="{cx:.1f}" y1="{base + 22:.1f}" x2="{cx:.1f}" y2="{base + 9:.1f}" '
f'stroke="{color}" stroke-width="1.8"/>'
f'<polygon points="{cx:.1f},{base + 2:.1f} {cx - 4:.1f},{base + 10:.1f} '
f'{cx + 4:.1f},{base + 10:.1f}" fill="{color}"/>')
ty = base + 34
label = (f'<text x="{x0:.1f}" y="{ty:.1f}" fill="{color}" '
f'style="font-weight:bold">{text}</text>')
# dreptunghiul/săgeata în spatele barelor, eticheta la final
i = svg.index('<rect', svg.index(f'y1="{base:.1f}"'))
svg = svg[:i] + mark + svg[i:]
return svg.replace("</svg>", label + "</svg>")
def grow(svg: str, dh: float) -> str:
"""Mărește viewBox-ul unui panou cu dh px jos, pentru eticheta de sub volum."""
h = vb_height(svg)
return svg.replace(f'0 0 {PW} {h:.0f}"', f'0 0 {PW} {h + dh:.0f}"', 1)
PANEL_GAP, PAD = 24, 8
TITLE_H, FOOTER_H = 30, 0
svg_a = grow(vol_note(ch_a.render(), CONF_A, CONF_A + 4,
"volum în creștere = acumulare", GREEN, box=True), 10)
svg_n = grow(vol_note(ch_n.render(), CONF_N, CONF_N,
"volum slab: nimeni mare nu cumpără", RED, box=False), 30)
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)
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">Setup A+ vs. setup mediocru (două panouri, exemple construite)</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'</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",
"Setup A+ vs. setup mediocru (două panouri, exemple construite)"),):
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) >= 8, "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
assert "Surse" not in wrapper and ".md:" not in wrapper, "trimiteri la surse în SVG"
for cheie in ("suport 42.00", "doji chiar pe nivel", "vânzarea s-a oprit", "volum în creștere = acumulare",
"rezistență 78.50", "gap-up în rezistență", "cei întârziați",
"semnalul a mințit", "volum slab: nimeni mare nu cumpără",
"A+ — doji la suport, volum în acumulare",
"Mediocru — gap-up în rezistență, volum varză"):
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")