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
This commit is contained in:
Claude Agent
2026-09-19 21:09:31 +00:00
parent aff03870e8
commit 8b7b5f6b44
50 changed files with 7011 additions and 6921 deletions

View File

@@ -1,132 +1,134 @@
"""Buy Doji — intrarea în trei tranșe (exemplu construit, doji 53.00 / 51.60).
"""Buy Doji — intrarea în trei tranșe, pe același trade ca restul capitolului.
Citește specificația `gen/strategii/buy_doji_transe.spec.txt`. Barele 0-14
sunt aceleași ca în buy_doji_stop.py. Nivelurile de 1/3 și 1/2 se calculează
din corpul candelei de confirmare, nu se scriu de mână.
Citește `gen/strategii/buy_doji_transe.spec.txt`. Barele vin din
`buy_doji_ansamblu.serie()`: doji 50.10/49.25, confirmare cu close 50.40,
stop 49.20. Nivelurile de 1/3 și 1/2 se calculează din corpul candelei de
confirmare (open 49.90 → close 50.40), nu se scriu de mână.
Lecția: împărțind intrarea în trei, prețul mediu scade de la 50.40 la
50.26, deci riscul pe acțiune de la 1.20 la 1.06 — dacă prețul revine.
Bara de după confirmare coboară la 50.10 și umple ambele ordine limită.
"""
import re
import sys; sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Candle, Chart, Level, Mark
import sys
from pathlib import Path
OUT = "/workspace/atm-curs-dl/gen/strategii/buy_doji_transe.svg"
sys.path.insert(0, "/workspace/atm-curs-dl")
sys.path.insert(0, str(Path(__file__).parent))
from svg_candles import Chart, Level, Zone, Arrow # noqa: E402
from buy_doji_ansamblu import serie, DOJI, CONF, DOJI_L, ENTRY, SL, TP1, R # noqa: E402
# --- seria: barele 0-14 identice cu buy_doji_stop.py --------------------------
closes = [60.40, 59.60, 59.90, 58.80, 58.10, 58.40, 57.20, 56.30, 56.60,
55.30, 54.20, 52.40]
bars, o = [], 61.00
for c in closes: # fitile implicite 0.15 peste/sub corp
bars.append((o, max(o, c) + 0.15, min(o, c) - 0.15, c))
o = c
bars += [(52.40, 53.00, 51.60, 52.30), # 12 doji
(52.30, 52.85, 52.10, 52.60), # 13 +1
(52.60, 53.85, 52.45, 53.70), # 14 confirmare
(53.70, 53.80, 53.10, 53.45), # 15 umple 1/3 și 1/2
(53.45, 54.20, 53.35, 54.10),
(54.10, 54.85, 54.00, 54.70),
(54.70, 56.00, 54.60, 55.90)] # 18 TP1
o_, h_, l_, c_ = (list(x) for x in zip(*bars))
OUT = Path(__file__).with_suffix(".svg")
S = 12 # prima bară arătată (zoom pe fereastra de intrare)
GREEN, RED, ORANGE, BLUE, GREY = "#188a3e", "#c00", "#b06000", "#1a5fb4", "#555"
W, SCALE = 930, 100.0
# --- nivelurile, calculate din corpul barei 14 ---------------------------------
op, T1 = o_[14], c_[14]
corp = T1 - op
cds = serie()[S:20] # până la bara care atinge ținta
d, cf = DOJI - S, CONF - S # doji, confirmare (indici locali)
op, T1 = cds[cf].o, cds[cf].c # corpul candelei de confirmare
corp = round(T1 - op, 2)
T2, T3 = round(T1 - corp / 3, 2), round(T1 - corp / 2, 2)
MED = round((T1 + T2 + T3) / 3, 2)
SL = round(min(l_[11:15]) - 0.05, 2)
TP1 = round(T1 + (T1 - SL), 2)
RISC_MED, RISC_CLOSE = round(MED - SL, 2), round(T1 - SL, 2)
TP_MED = round(MED + RISC_MED, 2) # ținta de 1R socotită de la prețul mediu
retest = cds[cf + 1]
# --- aserțiuni ------------------------------------------------------------------
assert (op, T1) == (49.90, ENTRY) and corp == 0.50, (op, T1, corp)
assert (T2, T3) == (50.23, 50.15), (T2, T3)
assert MED == 50.26 and RISC_MED == 1.06 and RISC_CLOSE == R == 1.20
assert retest.l <= T3 < T2, "bara de după confirmare nu umple ambele ordine"
assert round(retest.l, 2) == 50.10, retest.l
assert min(c.l for c in cds[cf - 3:cf + 1]) == DOJI_L and round(DOJI_L - 0.05, 2) == SL
assert round(T1 + RISC_CLOSE, 2) == TP1
# ținta de 1R vine mai devreme dacă prețul mediu de intrare e mai bun
assert TP_MED == 51.32 == round(MED + RISC_MED, 2) and TP_MED < TP1
assert next(i for i in range(cf + 1, len(cds)) if cds[i].h >= TP1) == len(cds) - 1
# măsurat pe toată candela (nu doar pe corp) 1/2 iese tot T3; 1/3 iese 50.28
hi, lo = cds[cf].h, cds[cf].l
assert round(hi - (hi - lo) / 3, 2) == 50.28 and round((hi + lo) / 2, 2) == T3
print(f"transe: T1 {T1:.2f} T2 {T2:.2f} T3 {T3:.2f} medie {MED:.2f} "
f"risc {RISC_MED:.2f} (față de {RISC_CLOSE:.2f}) retest low {retest.l:.2f}")
def dy_sub(price, rand):
"""Săgeata pleacă din nivel și coboară sub linia stopului, pe rândul `rand`."""
return (price - SL) * SCALE + rand
# --- assert-urile din spec -----------------------------------------------------
for i in range(1, len(bars)): # 1
assert abs(o_[i] - c_[i - 1]) < 1e-9, f"open[{i}] != close[{i-1}]"
for i in range(len(bars)):
assert l_[i] <= min(o_[i], c_[i]) and h_[i] >= max(o_[i], c_[i]), f"bara {i}"
assert abs(o_[12] - c_[12]) <= 0.10 * (h_[12] - l_[12]) + 1e-9 # 2
assert c_[13] <= h_[12] < c_[14] and h_[12] == 53.00
assert min(l_[11:15]) == l_[12] == 51.60 and SL == 51.55 # 3
assert (op, T1) == (52.60, 53.70) and (T2, T3) == (53.33, 53.15) # 4
assert l_[15] <= T3 < T2 # 5
assert MED == 53.39 and round(MED - SL, 2) == 1.84 # 6
assert round(T1 - SL, 2) == 2.15
assert TP1 == 55.85 # 7
assert next(i for i in range(15, len(bars)) if h_[i] >= TP1) == 18
hi, lo = h_[14], l_[14] # 8
assert round(hi - (hi - lo) / 3, 2) == 53.38 and round((hi + lo) / 2, 2) == T3
GREY, RED, ORANGE, GREEN, BLUE = "#555", "#c00", "#b06000", "#188a3e", "#1a5fb4"
W = 780
ch = Chart(
candles=[Candle(*b) for b in bars],
levels=[Level(TP1, f"TP1 {TP1:.2f} = 1R de la close", GREEN, "2 2"),
Level(SL, f"SL {SL:.2f} (low 4 candele 51.60, minus 5 cenți)", RED, "4 3")],
marks=[Mark(14, f"confirmare {T1:.2f}", GREEN, "above", dy=-38)],
title="Intrarea în trei tranșe — daily, exemplu construit (doji 53.00 / 51.60)",
scale=58.0, gap=18, body_w=10, left=44, width=W,
candles=cds,
levels=[Level(TP1, "ținta 1: 51.60 = 1R față de close", GREEN, "2 2"),
Level(TP_MED, f"ținta 1: {TP_MED:.2f} = 1R față de prețul mediu {MED:.2f}", BLUE, "2 2"),
Level(SL, "stopul: 49.20", RED, "4 3", "left")],
zones=[Zone(cf, cf, "", GREEN, p_top=T1, p_bot=op)],
arrows=[
Arrow(cf, "1/3 la close 50.40\nca să nu ratezi trade-ul dacă pleacă fără tine",
where="right", price=T1, color=GREEN, dx=150, dy=dy_sub(T1, 40)),
Arrow(cf, "1/3 cu ordin limită la 50.23 (o treime din corp)\nmai ieftin, dacă prețul revine",
where="right", price=T2, color=ORANGE, dx=150, dy=dy_sub(T2, 74)),
Arrow(cf, "1/3 cu ordin limită la 50.15 (jumătatea corpului)\nși mai ieftin",
where="right", price=T3, color=ORANGE, dx=150, dy=dy_sub(T3, 108)),
Arrow(cf - 1, "media mai bună coboară ținta:\nieși la 51.32, nu la 51.60",
where="above", price=TP_MED, color=BLUE, dx=150),
Arrow(cf + 1, "retestul: prețul revine la 50.10\nși umple amândouă ordinele limită",
where="below", price=retest.l, color=BLUE, dx=-150, dy=20),
],
scale=SCALE, gap=30, body_w=10, left=56, width=W,
title="Intrarea în trei tranșe — același trade (doji 50.10 / 49.25)",
caption="Paranteza de pe candela de confirmare e corpul ei (49.90 → 50.40): pe el se măsoară 1/3 și 1/2. Stopul e unul singur. Exemplu construit.",
)
svg = ch.render()
y, x = ch._y, ch._cx
OFF = (float(re.search(r'<line x1="24" y1="([\d.]+)"', svg).group(1))
- ch._y(ch.levels[0].price)) # render() mută `top` și îl restaurează
# același lucru pe orizontală: render() lărgește pânza și mută `left` când un
# text de săgeată ar ieși în stânga. Offsetul se citește din prima candelă.
OFF_X = (float(re.search(r'<line x1="([\d.]+)"[^>]*stroke="#1a1a1a"', svg).group(1))
- ch._cx(0))
def Y(p):
return ch._y(p) + OFF
def x(i):
return ch._cx(i) + OFF_X
ex = []
# linia de ghidaj de la confirmare la eticheta ei (eticheta stă sus, peste bara 17)
ex.append(f'<line x1="{x(14):.1f}" y1="{y(h_[14]) - 3:.1f}" x2="{x(14):.1f}" '
f'y2="{y(h_[14]) - 44:.1f}" stroke="{GREEN}" stroke-width="1"/>')
# cele trei tranșe: linii de la bara 13 până la paranteza corpului
XB = 404 # paranteza corpului barei 14
for p, col, dash in ((T1, GREEN, ""), (T2, ORANGE, "3 3"), (T3, ORANGE, "3 3")):
d = f' stroke-dasharray="{dash}"' if dash else ""
ex.append(f'<line x1="{x(13) - 6:.1f}" y1="{y(p):.1f}" x2="{XB}" y2="{y(p):.1f}" '
f'stroke="{col}" stroke-width="1.5"{d}/>')
# paranteza: open 52.60 → close 53.70, cu diviziuni la 1/3 și 1/2
ex.append(f'<line x1="{XB}" y1="{y(T1):.1f}" x2="{XB}" y2="{y(op):.1f}" '
f'stroke="{GREY}" stroke-width="1.5"/>')
ex.append(f'<line x1="{XB - 6}" y1="{y(op):.1f}" x2="{XB}" y2="{y(op):.1f}" '
f'stroke="{GREY}" stroke-width="1.5"/>')
ex.append(f'<rect x="{XB - 3}" y="{y(MED) - 3:.1f}" width="6" height="6" fill="#888" '
f'transform="rotate(45 {XB} {y(MED):.1f})"/>')
# etichetele, răsfirate la dreapta, cu linii de ghidaj
yc, XL = y((T1 + T3) / 2), 440
labels = [(T1, f"1/3 la close {T1:.2f}", GREEN, ""),
(MED, f"medie {MED:.2f} → risc {MED - SL:.2f} pe acțiune (tot la close: {T1 - SL:.2f})", "#777", "2 2"),
(T2, f"1/3 la 1/3 din corp {T2:.2f}", ORANGE, "3 3"),
(T3, f"1/3 la 1/2 din corp {T3:.2f}", ORANGE, "3 3"),
(op, f"corp: open {op:.2f} → close {T1:.2f} = {corp:.2f}", GREY, "")]
for k, (p, txt, col, dash) in enumerate(labels):
ly = yc + (k - 1.5) * 22
d = f' stroke-dasharray="{dash}"' if dash else ""
ex.append(f'<polyline points="{XB},{y(p):.1f} {XL - 6},{ly - 4:.1f}" fill="none" '
f'stroke="{col}" stroke-width="1.2"{d}/>')
ex.append(f'<text x="{XL}" y="{ly:.1f}" fill="{col}" style="font-weight:bold">{txt}</text>')
# umpleri: T1 la close-ul barei 14, T2 și T3 pe low-ul barei 15; TP1 pe bara 18
for i, p, col in ((14, T1, GREEN), (15, T2, ORANGE), (15, T3, ORANGE), (18, TP1, GREEN)):
ex.append(f'<circle cx="{x(i):.1f}" cy="{y(p):.1f}" r="3.5" fill="#fff" '
# paranteza corpului, în stânga candelei de confirmare, cu diviziunile 1/3 și 1/2
xb = x(cf) - 17
ex.append(f'<polyline points="{xb + 5:.1f},{Y(T1):.1f} {xb:.1f},{Y(T1):.1f} {xb:.1f},{Y(op):.1f} '
f'{xb + 5:.1f},{Y(op):.1f}" fill="none" stroke="{GREY}" stroke-width="2"/>')
# fără etichetă lângă paranteză: oriunde lângă candele s-ar suprapune peste ele
# sau peste liniile săgeților. O numește legenda de sub grafic.
# nivelurile de tranșă, ca segmente scurte peste candela de confirmare
for p, col in ((T1, GREEN), (T2, ORANGE), (T3, ORANGE)):
dash = "" if p == T1 else ' stroke-dasharray="3 3"'
ex.append(f'<line x1="{xb:.1f}" y1="{Y(p):.1f}" x2="{x(cf) + 8:.1f}" y2="{Y(p):.1f}" '
f'stroke="{col}" stroke-width="1.5"{dash}/>')
# unde se umple fiecare tranșă: T1 la close, T2 și T3 pe low-ul barei următoare
for i, p, col in ((cf, T1, GREEN), (cf + 1, T2, ORANGE), (cf + 1, T3, ORANGE)):
ex.append(f'<circle cx="{x(i):.1f}" cy="{Y(p):.1f}" r="4" fill="#fff" '
f'stroke="{col}" stroke-width="2"/>')
# etichetele de sub candele, pe rânduri (Mark "below" le-ar pune pe același rând)
yb = y(SL) + 20
rows = [(15, f"low {l_[15]:.2f}: umple T2 și T3", ORANGE),
(13, f"+1: {c_[13]:.2f}, sub high", GREY),
(12, f"doji {h_[12]:.2f} / {l_[12]:.2f}", BLUE)]
for r, (i, txt, col) in enumerate(rows):
ry = yb + 16 * r
ex.append(f'<line x1="{x(i):.1f}" y1="{y(l_[i]) + 3:.1f}" x2="{x(i):.1f}" '
f'y2="{ry - 11:.1f}" stroke="{col}" stroke-width="1" stroke-dasharray="1 2"/>')
ex.append(f'<text x="{x(i) - 4:.1f}" y="{ry:.1f}" fill="{col}" '
# cele două paranteze de risc, la dreapta: tot la close vs cu tranșe
for xp, hi_p, txt, col in ((x(len(cds) - 1) + 40, T1,
f"risc {RISC_CLOSE:.2f} pe acțiune (tot la close)", RED),
(x(len(cds) - 1) + 290, MED,
f"risc {RISC_MED:.2f} pe acțiune (cu tranșe, preț mediu {MED:.2f})", BLUE)):
y1, y2 = Y(hi_p), Y(SL)
ex.append(f'<polyline points="{xp - 5:.1f},{y1:.1f} {xp:.1f},{y1:.1f} {xp:.1f},{y2:.1f} '
f'{xp - 5:.1f},{y2:.1f}" fill="none" stroke="{col}" stroke-width="1.5"/>')
ex.append(f'<text x="{xp + 6:.1f}" y="{(y1 + y2) / 2 + 4:.1f}" fill="{col}" '
f'style="font-weight:bold">{txt}</text>')
# legenda, pe două rânduri
ly = yb + 16 * len(rows) + 14
ex.append(f'<text x="24" y="{ly:.0f}" fill="#555">Măsurat pe corp (open {op:.2f} → close {T1:.2f}). '
f'Măsurat pe toată candela ({lo:.2f} → {hi:.2f}), 1/3 ar fi '
f'{hi - (hi - lo) / 3:.2f}; 1/2 iese tot {(hi + lo) / 2:.2f}.</text>')
ex.append(f'<text x="24" y="{ly + 17:.0f}" fill="#555">Stopul e unul singur, pentru toată '
f'poziția. Barele 0-11 și 15-18 sunt construite.</text>')
H = ly + 17 + 12
body = svg.rsplit("</svg>", 1)[0].rstrip()
svg = "\n ".join([body, *ex]) + "\n</svg>"
svg = re.sub(r'viewBox="0 0 (\d+) [\d.]+"', rf'viewBox="0 0 \1 {H:.0f}"', svg, count=1)
assert "<svg" in svg and "\n\n" not in svg, "linie goală în SVG"
with open(OUT, "w") as f:
f.write(svg)
print(f"scris {OUT} ({W}x{H:.0f})")
vw, vh = (float(v) for v in re.search(r'viewBox="0 0 ([\d.]+) ([\d.]+)"', svg).groups())
for tx, attrs, txt in re.findall(r'<text x="([-\d.]+)"([^>]*)>(.*?)</text>', svg):
ty = float(re.search(r'y="([-\d.]+)"', attrs).group(1))
w = len(txt) * 6.2
x0 = float(tx) - (w if 'text-anchor="end"' in attrs else w / 2 if 'middle' in attrs else 0)
assert 0 <= x0 and x0 + w <= vw and 0 <= ty <= vh, f"text în afara pânzei: {txt!r}"
assert "\n\n" not in svg, "linie goală în SVG"
OUT.write_text(svg, encoding="utf-8")
print(f"scris {OUT} ({vw:.0f}x{vh:.0f})")