"""Nucleul comun al generatoarelor de Analiza Zilnica: `PChart` si `spread`. Era copiat in cinci generatoare, iar copiile divergisera: reparatia din `spread()` (grupurile care se ating se centreaza pe media pozitiilor naturale, nu doar se impig in jos - altfel o eticheta care cade fix pe linia de deasupra decaleaza toate etichetele de sub ea) traia doar in `az_pivoti.py`. Aici e versiunea buna, singura. Un generator nou importa de aici: from pchart import PChart, Seg, spread """ import re from dataclasses import dataclass, field from svg_candles import Chart, Level, Mark, MA GRAY, RED, GREEN, BLUE, ORANGE, PURPLE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000", "#6a3d9a" @dataclass class Seg: """nivel valabil doar pe barele i0..i1 (o lună, un trimestru)""" i0: int i1: int price: float text: str color: str = ORANGE label_at: str = "out" # "out" marginea dreaptă | "lm" marginea stângă | "left"/"right" pe linie dy: float = -5.0 # pentru "left"/"right": eticheta deasupra liniei (negativ) sau dedesubt dash: str = "" def spread(items, gap=14): """[(y, ...)] -> aceleași, cu y-urile împinse ca două etichete să nu se atingă""" # grupurile care se ating se centrează pe media pozițiilor naturale (nu doar împinse în jos), # altfel o etichetă care coincide cu linia de deasupra decalează toate etichetele de sub ea groups = [] # [start_y, [items]] for it in sorted(items, key=lambda t: t[0]): groups.append([it[0], [it]]) while len(groups) > 1 and groups[-2][0] + gap * len(groups[-2][1]) > groups[-1][0]: last = groups.pop() its = groups[-1][1] + last[1] groups[-1] = [sum(t[0] - gap * k for k, t in enumerate(its)) / len(its), its] return [(g[0] + gap * k,) + tuple(t[1:]) for g in groups for k, t in enumerate(g[1])] @dataclass class PChart(Chart): """Chart cu niveluri pe perioade. Nivelurile (`levels`) devin segmente pe toată seria; etichetele stau în margini (dreapta / stânga), niciodată peste candele.""" segs: list = field(default_factory=list) seps: list = field(default_factory=list) # indecși unde începe o perioadă nouă pad_px: float = 24.0 # spațiu între cel mai sus preț și titlu lm: int = 104 # marginea stângă pentru etichetele "lm" pad_bot_px: float = 0.0 # spațiu sub cel mai jos preț, pentru etichetele de sub candele def render(self) -> str: n = len(self.candles) self.left = self.lm if any(s.label_at == "lm" for s in self.segs) else self.left segs = list(self.segs) + [Seg(0, n - 1, lv.price, lv.text, lv.color, "out", dash="4 3") for lv in self.levels] saved_lv, saved_ma = list(self.levels), list(self.mas) # lățimea: cât să încapă cea mai lungă etichetă din marginea dreaptă longest = max([len(s.text) for s in segs if s.label_at == "out"] + [len(m.text) for m in saved_ma] + [0]) self.width = max(self.width, int(self._cx(n - 1) + self.gap / 2 + 6 + longest * 6.2 + 8)) top_price = max([c.h for c in self.candles] + [s.price for s in segs]) # prețurile segmentelor intră în scară ca niveluri invizibile self.levels = [Level(s.price, "", "none") for s in segs] self.levels.append(Level(top_price + self.pad_px / self.scale, "", "none")) if self.pad_bot_px: low_price = min([c.l for c in self.candles] + [s.price for s in segs]) self.levels.append(Level(low_price - self.pad_bot_px / self.scale, "", "none")) self.mas = [MA(m.period, m.color, m.dash) for m in saved_ma] # eticheta MA o punem noi, în margine svg = self.render_base() self.levels, self.mas = saved_lv, saved_ma parts = svg.split("\n ") extra = [] for k in self.seps: x = self._cx(k) - self.gap / 2 extra.append(f'') closes = [c.c for c in self.candles] xr = self._cx(n - 1) + self.gap / 2 + 6 out_lab = [(self._y(s.price) + 4, s) for s in segs if s.label_at == "out"] out_lab += [(self._y(sum(closes[-m.period:]) / m.period) + 4, Seg(0, 0, 0, m.text, m.color)) for m in saved_ma if m.text] placed = {id(s): y for y, s in spread(out_lab)} placed.update({id(s): y for y, s in spread([(self._y(s.price) + 4, s) for s in segs if s.label_at == "lm"])}) for y, s in out_lab: # etichetă mutată de pe linia ei: legătură scurtă până la ea if abs(placed[id(s)] - y) > 3: extra.append(f'') for s in segs: x0, x1, y = self._cx(s.i0) - self.gap / 2, self._cx(s.i1) + self.gap / 2, self._y(s.price) dash = f' stroke-dasharray="{s.dash}"' if s.dash else "" extra.append(f'') if s.label_at == "out": extra.append(f'{s.text}') elif s.label_at == "lm": extra.append(f'{s.text}') elif s.label_at == "left": extra.append(f'{s.text}') else: extra.append(f'{s.text}') for y, s in out_lab: if s.i1 == 0 and s.price == 0: # eticheta de MA extra.append(f'{s.text}') k = 2 if self.title else 1 return "\n ".join(parts[:k] + extra + parts[k:]) def render_base(self): svg = Chart.render(self) # nivelurile invizibile lasă linii/etichete goale: le scoatem svg = re.sub(r'\n ]*stroke="none"[^>]*/>\n ]*fill="none"[^>]*>', "", svg) prices = [p for c in self.candles for p in (c.o, c.h, c.l, c.c)] + [lv.price for lv in self.levels] self._body_bottom = self.top + (max(prices) - min(prices)) * self.scale return svg