diff --git a/gen/az_context.py b/gen/az_context.py index 43b1b09..b4a214d 100644 --- a/gen/az_context.py +++ b/gen/az_context.py @@ -23,95 +23,7 @@ STYLE = dict(gap=15, body_w=9, left=44, width=620) svgs = {} -@dataclass -class Seg: - """nivel valabil doar pe barele i0..i1""" - i0: int - i1: int - price: float - text: str - color: str = ORANGE - label_at: str = "out" - dy: float = -5.0 - dash: str = "" - - -def spread(items, gap=14): - """[(y, ...)] -> aceleași, cu y-urile împinse ca două etichete să nu se atingă""" - items = sorted(items, key=lambda t: t[0]) - out = [] - for it in items: - y = it[0] if not out else max(it[0], out[-1][0] + gap) - out.append((y,) + tuple(it[1:])) - return out - - -@dataclass -class PChart(Chart): - """copiat din gen/az_pivoti.py (vezi acolo explicațiile)""" - segs: list = field(default_factory=list) - seps: list = field(default_factory=list) - pad_px: float = 24.0 - lm: int = 104 - pad_bot_px: float = 0.0 - - 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) - 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]) - 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] - 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 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: - 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) - 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 +from pchart import PChart, Seg, spread # noqa: E402 def px_ratio(ch, a, b, c): diff --git a/gen/az_earnings.py b/gen/az_earnings.py index e8e95c2..a4af165 100644 --- a/gen/az_earnings.py +++ b/gen/az_earnings.py @@ -29,97 +29,9 @@ FACTS = {} # ---------------------------------------------------------------- layout (din az_pivoti.py) -@dataclass -class Seg: - """nivel valabil doar pe barele i0..i1""" - i0: int - i1: int - price: float - text: str - color: str = ORANGE - label_at: str = "out" - dy: float = -5.0 - dash: str = "" +from pchart import PChart, Seg, spread # noqa: E402 -def spread(items, gap=14): - """[(y, ...)] -> aceleași, cu y-urile împinse ca două etichete să nu se atingă""" - items = sorted(items, key=lambda t: t[0]) - out = [] - for it in items: - y = it[0] if not out else max(it[0], out[-1][0] + gap) - out.append((y,) + tuple(it[1:])) - return out - - -@dataclass -class PChart(Chart): - segs: list = field(default_factory=list) - seps: list = field(default_factory=list) - pad_px: float = 24.0 - lm: int = 104 - pad_bot_px: float = 0.0 - - 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) - 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]) - 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] - 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 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: - 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) - 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 - - -# ---------------------------------------------------------------- utilitare def series(closes, start, wicks=None, opens=None, vols=None): """ca from_closes, dar cu open explicit pe barele de gap (opens={i: open})""" wicks, opens = wicks or {}, opens or {} diff --git a/gen/az_executie.py b/gen/az_executie.py index 0abe64f..8c2cb0e 100644 --- a/gen/az_executie.py +++ b/gen/az_executie.py @@ -26,27 +26,11 @@ RISC = 25.0 svgs = {} -@dataclass -class Seg: - """nivel valabil doar pe barele i0..i1""" - 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 - dash: str = "" - - -def spread(items, gap=14): - """[(y, ...)] -> aceleași, cu y-urile împinse ca două etichete să nu se atingă""" - items = sorted(items, key=lambda t: t[0]) - out = [] - for it in items: - y = it[0] if not out else max(it[0], out[-1][0] + gap) - out.append((y,) + tuple(it[1:])) - return out +# `Seg` si `spread` vin din pchart.py. `PChart` NU: varianta de aici calculeaza +# mediile dintr-un istoric de close-uri dinainte de bara 0 (`hist`), ca SMA-20 sa +# fie deja incalzita la prima bara desenata, si si le deseneaza singura. Clasa +# comuna nu are nevoie de asta, iar unirea lor ar risca cele 10 diagrame de aici. +from pchart import Seg, spread # noqa: E402 @dataclass diff --git a/gen/az_pivoti.py b/gen/az_pivoti.py index be2d5b9..6333e46 100644 --- a/gen/az_pivoti.py +++ b/gen/az_pivoti.py @@ -24,107 +24,7 @@ STYLE = dict(gap=15, body_w=9, left=44, width=830) svgs = {} -@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 +from pchart import PChart, Seg, spread # noqa: E402 def pivots(bars): diff --git a/gen/az_sinteza.py b/gen/az_sinteza.py index d90d137..653cc25 100644 --- a/gen/az_sinteza.py +++ b/gen/az_sinteza.py @@ -24,88 +24,7 @@ svgs = {} # ------------------------------------------------ PChart, copiat din gen/az_pivoti.py -@dataclass -class Seg: - """nivel valabil doar pe barele i0..i1 (o lună)""" - i0: int - i1: int - price: float - text: str - color: str = ORANGE - label_at: str = "out" - dy: float = -5.0 - dash: str = "" - - -def spread(items, gap=14): - items = sorted(items, key=lambda t: t[0]) - out = [] - for it in items: - y = it[0] if not out else max(it[0], out[-1][0] + gap) - out.append((y,) + tuple(it[1:])) - return out - - -@dataclass -class PChart(Chart): - segs: list = field(default_factory=list) - seps: list = field(default_factory=list) - pad_px: float = 24.0 - lm: int = 104 - pad_bot_px: float = 0.0 - - 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) - 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]) - 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] - 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 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}') - for y, s in out_lab: - if s.i1 == 0 and s.price == 0: - 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) - 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 +from pchart import PChart, Seg, spread # noqa: E402 def pivots(bars): diff --git a/gen/pchart.py b/gen/pchart.py new file mode 100644 index 0000000..7369723 --- /dev/null +++ b/gen/pchart.py @@ -0,0 +1,121 @@ +"""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 diff --git a/summaries/08_ANALIZA_ZILNICA/07_context_de_piata.html b/summaries/08_ANALIZA_ZILNICA/07_context_de_piata.html index 1d3915b..d0ee7f7 100644 --- a/summaries/08_ANALIZA_ZILNICA/07_context_de_piata.html +++ b/summaries/08_ANALIZA_ZILNICA/07_context_de_piata.html @@ -366,9 +366,9 @@ de la RSP, care este S&P-ul equal weight" (17.08)

SL 57.50 - vârf 1 56.30 + vârf 1 56.30 - intrare short 55.90 + intrare short 55.90 TP 52.70 = 2R @@ -915,17 +915,19 @@ spike-ul e o bară de peste 2x media.

Exemplu construit (fără ticker) — daily, indicele + + TP2 60.80 = 2R - TP1 58.40 = 1/2 până la vârf (0.9R) + TP1 58.40 = 1/2 până la vârf (0.9R) intrare 56.60 - locul crimei 54.60 + locul crimei 54.60 - SL 54.50 - SMA-20 + SL 54.50 + SMA-20 @@ -1080,9 +1082,11 @@ oversold + locul crimei / gap închis / SMA 20.

Exemplu construit (fără ticker) — daily, indicele + + - locul crimei 54.80 - SMA-20 + locul crimei 54.80 + SMA-20 @@ -1656,16 +1660,18 @@ mare, atenție la tranzacții în jurul orei (04.09).

Exemplu construit (fără ticker) — daily, indicele + + TP 60.30 = 2R intrare 56.90 - range high 55.40 + range high 55.40 - SL 55.20 + SL 55.20 - range low 54.40 + range low 54.40 diff --git a/summaries/08_ANALIZA_ZILNICA/07_context_de_piata.md b/summaries/08_ANALIZA_ZILNICA/07_context_de_piata.md index d8d9a70..468ab70 100644 --- a/summaries/08_ANALIZA_ZILNICA/07_context_de_piata.md +++ b/summaries/08_ANALIZA_ZILNICA/07_context_de_piata.md @@ -170,9 +170,9 @@ RSP nu face vârf nou, **compania medie nu urcă**. SL 57.50 - vârf 1 56.30 + vârf 1 56.30 - intrare short 55.90 + intrare short 55.90 TP 52.70 = 2R @@ -695,17 +695,19 @@ spike-ul e o bară de peste 2x media. Exemplu construit (fără ticker) — daily, indicele + + TP2 60.80 = 2R - TP1 58.40 = 1/2 până la vârf (0.9R) + TP1 58.40 = 1/2 până la vârf (0.9R) intrare 56.60 - locul crimei 54.60 + locul crimei 54.60 - SL 54.50 - SMA-20 + SL 54.50 + SMA-20 @@ -861,9 +863,11 @@ oversold + locul crimei / gap închis / SMA 20**. Exemplu construit (fără ticker) — daily, indicele + + - locul crimei 54.80 - SMA-20 + locul crimei 54.80 + SMA-20 @@ -1305,16 +1309,18 @@ mare, atenție la tranzacții în jurul orei (04.09). Exemplu construit (fără ticker) — daily, indicele + + TP 60.30 = 2R intrare 56.90 - range high 55.40 + range high 55.40 - SL 55.20 + SL 55.20 - range low 54.40 + range low 54.40 diff --git a/summaries/08_ANALIZA_ZILNICA/08_executie_si_risc.html b/summaries/08_ANALIZA_ZILNICA/08_executie_si_risc.html index ba80e23..7a246b0 100644 --- a/summaries/08_ANALIZA_ZILNICA/08_executie_si_risc.html +++ b/summaries/08_ANALIZA_ZILNICA/08_executie_si_risc.html @@ -1367,12 +1367,12 @@ liquiditatea de pe high-ul ăsta des și aș intra în bucăți."

vârful după HL 434 (aici NU cumperi tot) - tranșele 1 și 3: câte 2 acț. la 418 / 419 + tranșele 1 și 3: câte 2 acț. la 418 / 419 tranșa 2: 2 acț. la 424 (1/3) - tranșa 4: 2 acț. la 414 (2/3) + tranșa 4: 2 acț. la 414 (2/3) SL comun 399 (sub HL 404) diff --git a/summaries/08_ANALIZA_ZILNICA/08_executie_si_risc.md b/summaries/08_ANALIZA_ZILNICA/08_executie_si_risc.md index fc4ac40..d136e5c 100644 --- a/summaries/08_ANALIZA_ZILNICA/08_executie_si_risc.md +++ b/summaries/08_ANALIZA_ZILNICA/08_executie_si_risc.md @@ -1105,12 +1105,12 @@ poziția: **4 tranșe de câte 1/4**. După higher low prețul poate retesta 1/3 vârful după HL 434 (aici NU cumperi tot) - tranșele 1 și 3: câte 2 acț. la 418 / 419 + tranșele 1 și 3: câte 2 acț. la 418 / 419 tranșa 2: 2 acț. la 424 (1/3) - tranșa 4: 2 acț. la 414 (2/3) + tranșa 4: 2 acț. la 414 (2/3) SL comun 399 (sub HL 404) diff --git a/summaries/08_ANALIZA_ZILNICA/09_earnings_si_optiuni.html b/summaries/08_ANALIZA_ZILNICA/09_earnings_si_optiuni.html index eafae13..6bc859c 100644 --- a/summaries/08_ANALIZA_ZILNICA/09_earnings_si_optiuni.html +++ b/summaries/08_ANALIZA_ZILNICA/09_earnings_si_optiuni.html @@ -399,16 +399,20 @@ ai un nivel concret pentru stop: sub zona apărată.

Exemplu construit (fără ticker) — daily + + + + TP 72.50 = 2R - intrare 66.90 + intrare 66.90 - sus gap 66.70 + sus gap 66.70 - 1/2 gap 64.50 + 1/2 gap 64.50 - SL 64.10 + SL 64.10 jos gap 62.30 @@ -840,12 +844,14 @@ se execută ca ordine de vânzare și împing prețul și mai jos.

Exemplu construit (fără ticker) — daily + + TP 57.90 = 2R - intrare 53.40 + intrare 53.40 - neckline 53.15 + neckline 53.15 SL 51.15 @@ -1378,16 +1384,20 @@ fără să dea un exemplu. Varianta de mai jos e construcția mea.

Exemplu construit (fără ticker) — daily + + + + call cumpărat 56.00 - call vândut 54.00 + call vândut 54.00 - +7% implicit 53.50 + +7% implicit 53.50 - −7% implicit 46.50 + −7% implicit 46.50 - put vândut 46.00 + put vândut 46.00 put cumpărat 44.00 diff --git a/summaries/08_ANALIZA_ZILNICA/09_earnings_si_optiuni.md b/summaries/08_ANALIZA_ZILNICA/09_earnings_si_optiuni.md index 84c1423..34ba528 100644 --- a/summaries/08_ANALIZA_ZILNICA/09_earnings_si_optiuni.md +++ b/summaries/08_ANALIZA_ZILNICA/09_earnings_si_optiuni.md @@ -331,16 +331,20 @@ ai un nivel concret pentru stop: sub zona apărată. Exemplu construit (fără ticker) — daily + + + + TP 72.50 = 2R - intrare 66.90 + intrare 66.90 - sus gap 66.70 + sus gap 66.70 - 1/2 gap 64.50 + 1/2 gap 64.50 - SL 64.10 + SL 64.10 jos gap 62.30 @@ -760,12 +764,14 @@ se execută ca ordine de vânzare și împing prețul și mai jos. Exemplu construit (fără ticker) — daily + + TP 57.90 = 2R - intrare 53.40 + intrare 53.40 - neckline 53.15 + neckline 53.15 SL 51.15 @@ -1265,16 +1271,20 @@ fără să dea un exemplu. Varianta de mai jos e construcția mea. Exemplu construit (fără ticker) — daily + + + + call cumpărat 56.00 - call vândut 54.00 + call vândut 54.00 - +7% implicit 53.50 + +7% implicit 53.50 - −7% implicit 46.50 + −7% implicit 46.50 - put vândut 46.00 + put vândut 46.00 put cumpărat 44.00 diff --git a/summaries/08_ANALIZA_ZILNICA/10_sinteza.html b/summaries/08_ANALIZA_ZILNICA/10_sinteza.html index ea39bbc..aa272de 100644 --- a/summaries/08_ANALIZA_ZILNICA/10_sinteza.html +++ b/summaries/08_ANALIZA_ZILNICA/10_sinteza.html @@ -311,7 +311,7 @@ tabelul de mai sus.

Exemplu construit (fără ticker) — daily - pivot M6 73.10 + pivot M6 73.10 pivot M7 77.77 @@ -321,7 +321,7 @@ tabelul de mai sus.

intrare 75.60 - SL 73.60 (sub HL) + SL 73.60 (sub HL) diff --git a/summaries/08_ANALIZA_ZILNICA/10_sinteza.md b/summaries/08_ANALIZA_ZILNICA/10_sinteza.md index 81f6103..938b46e 100644 --- a/summaries/08_ANALIZA_ZILNICA/10_sinteza.md +++ b/summaries/08_ANALIZA_ZILNICA/10_sinteza.md @@ -229,7 +229,7 @@ tabelul de mai sus. Exemplu construit (fără ticker) — daily - pivot M6 73.10 + pivot M6 73.10 pivot M7 77.77 @@ -239,7 +239,7 @@ tabelul de mai sus. intrare 75.60 - SL 73.60 (sub HL) + SL 73.60 (sub HL) diff --git a/svg_candles.py b/svg_candles.py index d88d3b7..081c381 100644 --- a/svg_candles.py +++ b/svg_candles.py @@ -22,9 +22,25 @@ Două stiluri de desen: candelă se suprapun. """ +import re from dataclasses import dataclass, field +def _check_dash(dash: str, cls: str) -> None: + """`dash` e un stroke-dasharray SVG ("4 3", "" etc.), nu o pozitie. + + Garda exista pentru ca `Level(p, text, color, "left")` pune tacut "left" + in `dash` in loc de `label_at`, iar SVG-ul iese fara linie punctata si + fara nicio eroare. Trei agenti au pierdut timp pe asta pe 13.09.2026. + """ + if dash and not re.fullmatch(r"[\d.,\s]+", dash): + raise ValueError( + f"{cls}.dash={dash!r} nu e un stroke-dasharray. " + f"Daca voiai pozitia etichetei, argumentul se cheama label_at " + f"si se da pe nume: {cls}(..., label_at={dash!r})." + ) + + @dataclass class Candle: o: float @@ -45,6 +61,11 @@ class Level: dash: str = "4 3" label_at: str = "right" # "right" | "left" — unde stă eticheta + def __post_init__(self) -> None: + _check_dash(self.dash, "Level") + if self.label_at not in ("right", "left"): + raise ValueError(f"Level.label_at={self.label_at!r}, astept 'right' sau 'left'") + @dataclass class Mark: @@ -64,6 +85,9 @@ class MA: dash: str = "" text: str = "" + def __post_init__(self) -> None: + _check_dash(self.dash, "MA") + @dataclass class Chart: