From f647874a52daf941aefd523dd699c07f1f5b82e5 Mon Sep 17 00:00:00 2001
From: Claude Agent
Date: Fri, 18 Sep 2026 11:57:57 +0000
Subject: [PATCH] datoria tehnica: garda pe dash, si PChart intr-un singur loc
Garda: `Level(p, text, color, "left")` punea tacut "left" in `dash` in loc
de `label_at`, iar SVG-ul iesea fara linie punctata si fara nicio eroare -
trei agenti au pierdut timp pe asta pe 13.09. Acum `__post_init__` crapa
cu mesaj care spune ce argument voiai. Verificat pe cazurile valide si pe
cele doua gresite.
PChart: era copiat in cinci generatoare, iar copiile divergisera - toate
cinci aveau hash-uri diferite si patru pastrau `spread()` cel vechi.
Nucleul comun trece in gen/pchart.py; 385 de linii sterse, 11 adaugate.
az_executie.py isi pastreaza PChart-ul: varianta lui calculeaza mediile
dintr-un istoric dinainte de bara 0 si si le deseneaza singura. Unirea
celor doua comportamente ca sa scap de un fisier ar fi riscat cele 10
diagrame de acolo, pentru zero castig. Ia doar `spread` reparat.
Reparatia schimba 4 documente, deci PROGRESS gresea cand spunea ca nicio
diagrama comisa nu sufera de bug: 08 si 10 se misca cu 0,2 px, dar 07 si
09 aveau etichete la 6 px de linia lor. Verificat cu ochii pe 3 diagrame
randate - etichetele stau acum pe linia lor, cu liniuta de legatura unde
a trebuit mutata. audit_diagrams: 0 probleme.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
---
gen/az_context.py | 90 +------------
gen/az_earnings.py | 90 +------------
gen/az_executie.py | 26 +---
gen/az_pivoti.py | 102 +--------------
gen/az_sinteza.py | 83 +-----------
gen/pchart.py | 121 ++++++++++++++++++
.../07_context_de_piata.html | 28 ++--
.../08_ANALIZA_ZILNICA/07_context_de_piata.md | 28 ++--
.../08_executie_si_risc.html | 4 +-
.../08_ANALIZA_ZILNICA/08_executie_si_risc.md | 4 +-
.../09_earnings_si_optiuni.html | 30 +++--
.../09_earnings_si_optiuni.md | 30 +++--
summaries/08_ANALIZA_ZILNICA/10_sinteza.html | 4 +-
summaries/08_ANALIZA_ZILNICA/10_sinteza.md | 4 +-
svg_candles.py | 24 ++++
15 files changed, 236 insertions(+), 432 deletions(-)
create mode 100644 gen/pchart.py
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.90TP 52.70 = 2R
@@ -915,17 +915,19 @@ spike-ul e o bară de peste 2x media.
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 / 419tranș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 / 419tranș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.10jos 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.15SL 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.00put 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.10jos 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.15SL 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.00put 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.10pivot 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.10pivot 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: