""" 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''] # nivelurile: linie pe toată lățimea, eticheta deasupra liniei la dreapta for lv in self.levels: y = self._y(lv.price) out.append(f'') out.append(f'{lv.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'') # 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'' f'{"downtrend anterior" if self.trend == "down" else "uptrend anterior"}') 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'') h = max(self._y(bot_body) - self._y(top_body), 2.0) out.append(f'') if cd.tag: out.append(f'{cd.tag}') # 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'{cd.note}') ny += 17 if self.caption: out.append(f'{self.caption}') out.append("") return "\n ".join(out).replace("\n ", "\n") 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 "