Files
atm-curs-dl/svg_candles.py
Claude Agent 9415fea0e7 Rescrie patternurile de candele: diagrame generate din preturi + explicatii complete
Semnalat de user: diagramele nu erau proportionale cu textul si patternurile
erau doar prezentate, fara sa spuna ce fac si cand se tranzactioneaza.

Verificat numeric, erau reale:
- hammer: entry->SL 75 px/unitate, entry->TP(2R) 41 px/unitate
- engulfing: 110 vs 17.5 px/unitate
- morning star: 82 vs 10.6 px/unitate
- marubozu bull: eticheta de open pusa deasupra celei de close (axa inversata)
- etichete taiate de viewBox si suprapuse

Fix:
- svg_candles.py - generator care calculeaza coordonatele din preturi, cu o
  singura scara pe desen. Are selftest care verifica exact cazul de mai sus.
- gen/patternuri_candele.py - regenereaza fisierul intreg; diagramele nu se
  mai editeaza de mana
- fiecare pattern are acum: ce arata, ce s-a intamplat mecanic, DE CE il
  urmaresti, CE FACI cand il vezi, trigger DA/NU, si trade complet unde e cazul
- adaugate explicatii practice unde lipseau complet: inverted hammer, hanging
  man, shooting star, spinning top, harami, marubozu (comparatia de risc la
  intrare directa vs retest midpoint), abandoned baby, piercing line,
  three white soldiers
- STYLE.md: regula de generare + verificarea automata de scara

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtSTyTmt6AbCL9j5ajEDm1
2026-09-12 20:59:01 +00:00

133 lines
5.9 KiB
Python

"""
Generator de diagrame de candele pentru summaries/.
Motivul existenței: diagramele desenate de mână ies la scară greșită. În
`01_MODUL_1/05_patternuri_candele.md` ținta de 2R era desenată la distanța
de 1R față de intrare (75 px/unitate în jos, 41 px/unitate în sus), iar la
Marubozu eticheta de open era pusă deasupra celei de close — axa de preț
inversată. Aici coordonatele se calculează din prețuri, deci scara e
corectă prin construcție.
Convenție: un singur `scale` (px per unitate de preț) pentru tot desenul.
"""
from dataclasses import dataclass, field
@dataclass
class Candle:
o: float
h: float
l: float
c: float
tag: str = "" # eticheta de sub candelă
note: str = "" # a doua linie de etichetă
color: str | None = None # implicit: verde dacă c >= o
@dataclass
class Level:
price: float
text: str
color: str = "#1a5fb4"
dash: str = "4 3"
@dataclass
class Chart:
candles: list[Candle]
levels: list[Level] = field(default_factory=list)
scale: float = 60.0 # px per unitate de preț
width: int = 620
body_w: int = 26
gap: int = 92 # distanță între candele
left: int = 64 # x-ul primei candele
top: int = 34 # spațiu deasupra celui mai mare preț
label_gap: int = 34 # spațiu sub cea mai joasă candelă pentru etichete
caption: str = ""
trend: str = "" # "down" / "up" / "" — segment de trend înainte
def _y(self, price: float) -> float:
return self.top + (self._pmax - price) * self.scale
def render(self) -> str:
prices = [p for c in self.candles for p in (c.o, c.h, c.l, c.c)]
prices += [lv.price for lv in self.levels]
self._pmax, pmin = max(prices), min(prices)
# segmentul de trend iese din zona candelelor: rezervă loc pentru el,
# altfel se suprapune peste etichete sau peste marginea desenului
if self.trend == "down":
self.top += 56
body_bottom = self.top + (self._pmax - pmin) * self.scale
extra = 62 if self.trend == "up" else 0
n_notes = sum(1 for c in self.candles if c.note)
height = body_bottom + extra + self.label_gap + 17 * n_notes + (24 if self.caption else 8)
out = [f'<svg viewBox="0 0 {self.width} {height:.0f}" xmlns="http://www.w3.org/2000/svg" '
f'style="width:100%;height:auto;font:12px sans-serif">']
# nivelurile: linie pe toată lățimea, eticheta deasupra liniei la dreapta
for lv in self.levels:
y = self._y(lv.price)
out.append(f'<line x1="24" y1="{y:.1f}" x2="{self.width - 14}" y2="{y:.1f}" '
f'stroke="{lv.color}" stroke-width="1.5" stroke-dasharray="{lv.dash}"/>')
out.append(f'<text x="{self.width - 28}" y="{y - 6:.1f}" fill="{lv.color}" '
f'text-anchor="end">{lv.text}</text>')
x0 = self.left
if self.trend:
y_start = self._y(self.candles[0].h if self.trend == "down" else self.candles[0].l)
dy = -46 if self.trend == "down" else 46
out.append(f'<polyline points="24,{y_start + dy:.1f} {x0 - 8:.0f},{y_start:.1f}" '
f'fill="none" stroke="#999" stroke-width="2"/>')
# la downtrend linia coboară spre dreapta (etichetă deasupra),
# la uptrend urcă (etichetă dedesubt) — altfel textul stă pe linie
ty = y_start + dy - 12 if self.trend == "down" else y_start + dy + 20
out.append(f'<text x="24" y="{ty:.1f}" fill="#999">'
f'{"downtrend anterior" if self.trend == "down" else "uptrend anterior"}</text>')
for i, cd in enumerate(self.candles):
cx = x0 + i * self.gap
top_body, bot_body = max(cd.o, cd.c), min(cd.o, cd.c)
color = cd.color or ("#188a3e" if cd.c >= cd.o else "#c00")
out.append(f'<line x1="{cx}" y1="{self._y(cd.h):.1f}" x2="{cx}" y2="{self._y(cd.l):.1f}" '
f'stroke="#1a1a1a" stroke-width="1.5"/>')
h = max(self._y(bot_body) - self._y(top_body), 2.0)
out.append(f'<rect x="{cx - self.body_w // 2}" y="{self._y(top_body):.1f}" '
f'width="{self.body_w}" height="{h:.1f}" fill="{color}"/>')
if cd.tag:
out.append(f'<text x="{cx}" y="{body_bottom + extra + 18:.1f}" fill="{color}" '
f'text-anchor="middle" style="font-weight:bold">{cd.tag}</text>')
# detaliile OHLC ca bloc stânga-jos, câte o linie per candelă: centrate sub
# candelă se suprapun între ele de îndată ce textul depășește `gap`.
ny = body_bottom + extra + self.label_gap + 4
for cd in self.candles:
if cd.note:
color = cd.color or ("#188a3e" if cd.c >= cd.o else "#c00")
out.append(f'<text x="24" y="{ny:.1f}" fill="{color}">{cd.note}</text>')
ny += 17
if self.caption:
out.append(f'<text x="24" y="{ny + 8:.0f}" fill="#555">{self.caption}</text>')
out.append("</svg>")
return "\n ".join(out).replace("\n </svg>", "\n</svg>")
def selftest():
"""O unitate de preț trebuie să dea aceeași distanță oriunde pe desen."""
ch = Chart(candles=[Candle(o=63.05, h=63.30, l=62.10, c=63.30)],
levels=[Level(62.05, "SL"), Level(63.35, "entry"), Level(65.95, "TP 2R")],
scale=60)
svg = ch.render()
ys = {lv.price: ch._y(lv.price) for lv in ch.levels}
risc = ys[62.05] - ys[63.35] # px, în jos
castig = ys[63.35] - ys[65.95] # px, în sus
assert abs(castig / risc - 2.0) < 0.01, f"2R desenat la {castig/risc:.2f}R"
assert "<svg" in svg and svg.count("\n\n") == 0, "linie goală în svg rupe markdown-ul"
print("selftest ok: 2R desenat la exact 2R, fără linii goale")
if __name__ == "__main__":
selftest()