From 6b2d9eb1e5d338f6dd915ba3df3d6ecb4f1c4f76 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sun, 13 Sep 2026 07:46:00 +0000 Subject: [PATCH] svg_candles: serie continua, panou de volum, medii mobile, etichete ancorate Userul a cerut ca diagramele de pret sa arate ca un grafic real (TradingView): linie continua de pret si volum, ca sa se vada trendul. Candelele izolate cu spatii mari nu spun nimic despre trend. Adaugat in svg_candles.py: - from_closes() / walk(): serii continue, open = close-ul barei anterioare, cu structura controlata exact din close-uri - panou de volum (vol_h), bare colorate pe directia candelei - MA: medii mobile calculate din close-uri - Mark: etichete ancorate pe candele anume (la 30 de bare, tag-urile per candela se suprapun) - title, Level.label_at - selftest extins: volum proportional, serie fara goluri, structura pastrata STYLE.md: regula noua, cu cele doua stiluri (serie de 25-45 de bare pentru orice grafic de pret; stilul vechi de 3-6 candele doar pentru definitii de pattern-uri de o candela) si obligatia de assert in generator. gen/exemplu_grafic_real.py: referinta de copiat. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MtSTyTmt6AbCL9j5ajEDm1 --- STYLE.md | 32 +++ gen/exemplu_grafic_real.py | 42 +++ summaries/01_MODUL_1/05_patternuri_candele.md | 258 +++++++++--------- svg_candles.py | 202 ++++++++++++-- 4 files changed, 389 insertions(+), 145 deletions(-) create mode 100644 gen/exemplu_grafic_real.py diff --git a/STYLE.md b/STYLE.md index 4aaa1f2..f80bd34 100644 --- a/STYLE.md +++ b/STYLE.md @@ -110,6 +110,38 @@ Verificare automată de pus în orice pagină cu trade-uri: pentru fiecare diagramă cu trei niveluri (SL / intrare / TP), raportul dintre distanțele în preț trebuie să fie identic cu raportul dintre distanțele în pixeli. +## Cum trebuie să arate un grafic de preț (cerut de user, 2026-09-13) + +Userul citește diagramele ca să vadă **trendul** și să facă analogie cu un +grafic real din TradingView. Candelele izolate, cu spații mari între ele, nu +arată ca un grafic și nu-i spun nimic despre trend. Deci: + +**Orice diagramă care arată preț sau volum se desenează ca serie continuă:** +- **25-45 de candele**, lipite (`gap=15-18`, `body_w=9-10`), nu 3-6 candele + izolate. Pattern-ul trebuie să aibă context în stânga și în dreapta lui. +- **Open-ul fiecărei bare = close-ul barei anterioare.** Folosește + `from_closes(...)` (definești close-urile, deci controlezi exact structura) + sau `walk(...)`. Fără goluri artificiale între bare. +- **Panou de volum dedesubt** (`vol_h=70`), cu bare colorate pe direcția + candelei, de fiecare dată când textul vorbește despre volum. +- **Medie mobilă** (`mas=[MA(20, ...)]`) când textul vorbește despre trend + sau despre SMA — se calculează singură din close-uri. +- **Etichetele se ancorează pe candele** cu `marks=[Mark(i, ...)]`, nu cu + `tag` per candelă: la 30 de bare, etichetele per candelă se suprapun. +- `title="SIMBOL — timeframe"` sus, ca pe un chart real. + +Stilul vechi (3-6 candele, `gap=92`) rămâne valabil **doar** pentru +definiții de pattern-uri de o candelă (ce e un marubozu, ce e un doji), +unde contează forma barei, nu trendul. + +**Verificare obligatorie în generator:** după ce construiești seria, extrage +din ea nivelurile pe care le afirmi în text (`min(c.l for c in cds[a:b])`) și +pune `assert` că se potrivesc — ex. că al doilea picior chiar e mai jos decât +primul, că intrarea e egală cu close-ul ultimei bare, că R-ul din etichetă e +cel calculat. Ochiul nu prinde o structură care contrazice eticheta; calculul +da. (Găsit ca bug real de agenții de verificare, 13.09.2026: o intrare short +descrisă „sub low-ul barei de retest", desenată deasupra lui.) + ## Diagrame (obligatorii, nu opționale — userul e vizual learner) - **Orice paragraf "Exemplu real" cu cifre concrete are nevoie de propria diagramă chiar acolo, lipită de text** — nu doar undeva mai jos în fișier, diff --git a/gen/exemplu_grafic_real.py b/gen/exemplu_grafic_real.py new file mode 100644 index 0000000..edeedbe --- /dev/null +++ b/gen/exemplu_grafic_real.py @@ -0,0 +1,42 @@ +"""Exemplu de referință pentru stilul „grafic real" cerut de user pe 13.09.2026. + +Rulează-l ca să vezi cum se construiește o serie continuă cu volum, medie +mobilă și etichete ancorate. Copiază structura în generatoarele tale. +vezi STYLE.md, secțiunea „Cum trebuie să arate un grafic de preț". +""" +import sys; sys.path.insert(0,"/workspace/atm-curs-dl") +from svg_candles import Chart, Level, Mark, MA, from_closes + +# close-urile definesc exact structura; wick-urile fixeaza minimele/maximele +closes = [113.0,111.4,109.9,110.6,108.8,107.4,106.0,106.9,105.2,104.0, # downtrend + 104.6,103.2,102.4,103.8,105.0, # picior 1 (low 101.50) + 104.2,103.0,101.9,101.1,102.6, # picior 2 (low 100.20) + 104.1,105.2,106.8, # breakout peste 105.50 + 105.9,106.0, # retest + 106.6,107.0] # intrare 107.00 +wicks = [(0.4,0.4)]*10 + [(0.5,0.5),(0.4,0.6),(0.5,0.90),(0.6,0.4),(0.5,0.4)] \ + + [(0.4,0.5),(0.4,0.6),(0.5,0.5),(0.4,0.90),(0.5,0.4)] \ + + [(0.5,0.3),(0.4,0.3),(0.2,0.3)] + [(0.3,0.6),(0.4,0.4)] + [(0.3,0.3),(0.2,0.3)] +vols = [46,44,41,38,52,47,44,39,48,43, 41,44,39,42,58, 47,44,40,43,70, 86,70,63, 47,44, 58,55] +cds = from_closes(closes, wicks=wicks, vols=vols, start=114.4) + +lo1 = min(c.l for c in cds[10:15]); lo2 = min(c.l for c in cds[15:20]) +peak = max(c.h for c in cds[13:15]); entry = cds[-1].c +assert lo2 < lo1, (lo1, lo2) +print(f"picior1 {lo1:.2f} picior2 {lo2:.2f} varf {peak:.2f} intrare {entry:.2f}") +risc = entry - 104.60 +print(f"risc {risc:.2f} TP1 {(117-entry)/risc:.2f}R TP2 {(124-entry)/risc:.2f}R") + +ch = Chart(candles=cds, + levels=[Level(104.60,"SL 104.60","#c00",label_at="left"), + Level(entry,f"intrare {entry:.2f}","#188a3e"), + Level(117.00,"TP1 117.00 = 4.2R","#1a5fb4"), + Level(124.00,"TP2 124.00 = 7.1R","#1a5fb4")], + marks=[Mark(13,f"picior 1: {lo1:.2f}","#c00","below"), Mark(19,f"picior 2: {lo2:.2f} (mai jos)","#c00","below"), + Mark(22,"breakout","#188a3e"), Mark(24,"retest","#b06000","below"), + Mark(26,"intrare","#188a3e")], + mas=[MA(20,"#b06000",dash="4 3",text="SMA-20")], + scale=8.0, gap=15, body_w=9, left=44, width=620, vol_h=70, + vol_label="volum", title="Disney — daily", + caption="Ale lui: pretul de 107 si tintele 117 / 124. Structura si volumul sunt construite de mine, la scara.") +print(ch.render()) diff --git a/summaries/01_MODUL_1/05_patternuri_candele.md b/summaries/01_MODUL_1/05_patternuri_candele.md index 4e44753..028bf8c 100644 --- a/summaries/01_MODUL_1/05_patternuri_candele.md +++ b/summaries/01_MODUL_1/05_patternuri_candele.md @@ -20,12 +20,12 @@ aceeași înălțime oriunde pe desen, inclusiv între intrare, stop-loss și Corpul arată distanța dintre open și close. Fitilele arată cât de departe a ajuns prețul în fiecare direcție **înainte să fie împins înapoi**. - - - bull - - - bear + + + bull + + + bear bull — O 50.00, H 50.65, L 49.80, C 50.60 (close peste open) bear — O 50.60, H 50.65, L 49.80, C 50.00 (close sub open) Aceleași patru prețuri, ordine inversă a lui open și close. Scară: 0.10 = 15 px. @@ -81,9 +81,9 @@ fitil de sus aproape inexistent. downtrend anterior - - - hammer + + + hammer O 63.05, H = C 63.30, L 62.10 — fitil de 0.95, corp de 0.25 Fitilul e de aproape 4 ori corpul. Scară: 0.10 = 10 px. @@ -103,23 +103,23 @@ aici, și unde pui stopul ca să afli repede că te-ai înșelat. prag de confirmare 63.30 - - - hammer - - - DA + + + hammer + + + DA DA — candela următoare închide la 64.10, peste high-ul hammer-ului (63.30) nivel de invalidare 62.10 - - - hammer - - - NU + + + hammer + + + NU NU — închide la 62.00, sub low-ul hammer-ului: downtrend-ul continuă @@ -133,12 +133,12 @@ aici, și unde pui stopul ca să afli repede că te-ai înșelat. SL 62.05 — sub low-ul hammer-ului downtrend anterior - - - hammer - - - confirmare + + + hammer + + + confirmare hammer — low-ul 62.10 e nivelul care ține ipoteza confirmare — închide 64.10, peste 63.30 Scară: 1.00 = 52 px. Distanța intrare→TP e exact dublul distanței intrare→SL. @@ -162,9 +162,9 @@ Nu „cât simți". downtrend anterior - - - inverted hammer + + + inverted hammer O 62.15, H 63.30, L 62.10, C 62.40 Fitilul de sus e de aproape 4 ori corpul. Scară: 0.10 = 10 px. @@ -195,9 +195,9 @@ final încă în favoarea taberei vechi. uptrend anterior - - - hanging man + + + hanging man O 63.05, H = C 63.30, L 62.10 — exact aceleași cifre ca la hammer Formă identică cu hammer. Contextul de trend e singura diferență. @@ -223,9 +223,9 @@ creștere. Geometric identic cu inverted hammer. uptrend anterior - - - shooting star + + + shooting star O 64.10, H 65.10, L 63.90, C 63.95 Scară: 0.10 = 9 px. @@ -253,12 +253,12 @@ devine rezistență până e depășit la închidere. TP 61.25 — 2R uptrend anterior - - - shooting star - - - confirmare + + + shooting star + + + confirmare shooting star — high-ul 65.10 e oferta confirmare — închide 63.70, sub low-ul 63.90 Scară: 1.00 = 44 px. Risc 1.30 în sus, țintă 2.60 în jos — exact dublul. @@ -268,9 +268,9 @@ devine rezistență până e depășit la închidere. **Ce arată:** open și close practic egale, cu fitile în ambele direcții. - - - doji + + + doji O 62.75, C 62.73 — corp de 0.02, interval total de 1.20 Prețul s-a mișcat 1.20 și a închis exact de unde a plecat. Scară: 0.10 = 10 px. @@ -300,12 +300,12 @@ Fără pasul 3, un doji e zgomot. Apar des. SL 62.05 — sub low-ul doji downtrend anterior - - - doji - - - confirmare + + + doji + + + confirmare doji — extremele 63.30 și 62.10 sunt pragurile confirmare — închide 64.00, peste 63.30 Scară: 1.00 = 52 px. Risc 1.30, țintă 2.60 — exact dublul. @@ -323,9 +323,9 @@ doji-ului, nu dincolo de o extremă. Atunci nu s-a rezolvat nimic — aștepți **Ce arată:** fitile lungi în ambele direcții, corp mic **dar nu neglijabil**. - - - spinning top + + + spinning top O 62.60, C 62.80 — corp de 0.20 într-un interval de 1.10 Corpul e sub 20% din interval. Scară: 0.10 = 10 px. @@ -357,12 +357,12 @@ doar o ajustare de risc. **Ce arată:** corp maxim, **fără fitile**. La cel bull: open = low și close = high. La cel bear: invers. - - - marubozu bull - - - marubozu bear + + + marubozu bull + + + marubozu bear bull — O = L = 62.00 (jos), C = H = 64.00 (sus) bear — O = H = 64.00 (sus), C = L = 62.00 (jos) La cel bull, open-ul e la minim; la cel bear, open-ul e la maxim. Scară: 1.00 = 70 px. @@ -387,15 +387,15 @@ jumătatea corpului, 63.00 în exemplul de sus. midpoint 63.00 - - - marubozu - - - pullback - - - reluare + + + marubozu + + + pullback + + + reluare marubozu bull — corp de 2.00, fără fitile pullback — coboară până la 63.05, aproape de midpoint reluare — cumpărătorii revin din midpoint @@ -427,12 +427,12 @@ opusă. În varianta strictă folosită în curs, îi depășește și fitilele. downtrend anterior - - - candela 1 - - - candela 2 + + + candela 1 + + + candela 2 candela 1 (mică, bear) — O 62.80, H 62.85, L 62.25, C 62.30 candela 2 (mare, bull) — O 62.20, H 63.05, L 62.15, C 63.00 Corpul candelei 2 (62.20→63.00) cuprinde complet corpul candelei 1 (62.80→62.30). Scară: 0.10 = 20 px. @@ -468,12 +468,12 @@ Fără condiția 1, aștepți confirmare ca la restul. SL 62.10 — sub low-ul c2 downtrend anterior - - - c1 - - - c2 — engulfing + + + c1 + + + c2 — engulfing candela 1 — cei care au vândut aici sunt acum pe pierdere candela 2 — intri la închiderea ei Scară: 1.00 = 62 px. Risc 1.00, țintă 2.00 — exact dublul. @@ -498,12 +498,12 @@ complet **în interiorul corpului** candelei 1. Numele vine din japoneză, downtrend anterior - - - candela 1 - - - candela 2 + + + candela 1 + + + candela 2 candela 1 (mare, bear) — O 64.00, H 64.10, L 62.10, C 62.20 candela 2 (mică, bull) — O 62.60, C 62.90, interval total 0.40 Candela 2 se tranzacționează într-un interval de 0.40, în interiorul corpului de 1.80 al candelei 1. @@ -544,12 +544,12 @@ corpul candelei 1 — doar trece de jumătatea lui (midpoint). midpoint 63.15 downtrend anterior - - - candela 1 - - - candela 2 + + + candela 1 + + + candela 2 candela 1 (bear) — O 63.80, C 62.50, midpoint 63.15 candela 2 (bull) — deschide sub 62.50 și închide la 63.40, peste midpoint Candela 2 recuperează peste jumătate din corpul candelei 1, dar nu tot. Scară: 0.10 = 12 px. @@ -592,15 +592,15 @@ care închide peste midpoint-ul primei. midpoint 63.10 — pragul de validare downtrend anterior - - - c1 — bear - - - c2 — indecizie - - - c3 — bull + + + c1 — bear + + + c2 — indecizie + + + c3 — bull candela 1 — O 64.00, C 62.20, midpoint 63.10 candela 2 — corp mic, izolată jos: nici vânzătorii nu mai coboară candela 3 — închide 63.40, peste midpoint-ul 63.10 @@ -638,15 +638,15 @@ tratezi ca atare. SL 61.85 — sub low-ul c2 downtrend anterior - - - c1 - - - c2 - - - c3 + + + c1 + + + c2 + + + c3 low-ul candelei 2 (61.90) e nivelul de invalidare intri la închiderea candelei 3, la 63.45 Scară: 1.00 = 42 px. Risc 1.60, țintă 3.20 — exact dublul. @@ -663,15 +663,15 @@ prin gap-uri — high-ul ei e sub low-ul candelei 1 **și** sub low-ul candelei downtrend anterior - - - c1 — bear - - - c2 — abandonată - - - c3 — bull + + + c1 — bear + + + c2 — abandonată + + + c3 — bull candela 1 — low-ul ei e 62.60 candela 2 — high-ul ei (62.25) e SUB low-ul candelei 1: gap candela 3 — low-ul ei (62.45) e PESTE high-ul candelei 2: al doilea gap @@ -701,15 +701,15 @@ nu apare. fiecare deschizând în interiorul corpului precedentei și închizând peste high-ul ei (sau sub low, la variantă bear). - - - 1 - - - 2 - - - 3 + + + 1 + + + 2 + + + 3 fiecare deschide în corpul precedentei și închide peste high-ul ei Three White Soldiers. Varianta bear (Three Black Crows) e oglinda exactă. diff --git a/svg_candles.py b/svg_candles.py index 861c29f..d88d3b7 100644 --- a/svg_candles.py +++ b/svg_candles.py @@ -9,6 +9,17 @@ 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. + +Două stiluri de desen: + +1. **Ilustrativ** (implicit, `gap=92`, 3-6 candele): pentru definiții de + pattern-uri, unde contează forma unei candele, nu trendul. +2. **Grafic real** (`gap=14`, `body_w=9`, 25-45 de candele, `vol_h=70`): + serie continuă de preț cu panou de volum dedesubt, ca pe TradingView. + Userul a cerut explicit stilul ăsta pentru orice diagramă unde trebuie + să se vadă un trend sau relația preț-volum. Etichetează candelele care + contează cu `marks`, nu cu `tag` — la 40 de candele etichetele per + candelă se suprapun. """ from dataclasses import dataclass, field @@ -20,9 +31,10 @@ class Candle: h: float l: float c: float - tag: str = "" # eticheta de sub candelă - note: str = "" # a doua linie de etichetă + tag: str = "" # eticheta de sub candelă (stil ilustrativ) + note: str = "" # linie de text în blocul de sub grafic color: str | None = None # implicit: verde dacă c >= o + vol: float | None = None # volumul barei, pentru panoul de volum @dataclass @@ -31,12 +43,34 @@ class Level: text: str color: str = "#1a5fb4" dash: str = "4 3" + label_at: str = "right" # "right" | "left" — unde stă eticheta + + +@dataclass +class Mark: + """Etichetă ancorată pe o candelă anume, pentru serii lungi.""" + i: int # indexul candelei + text: str + color: str = "#1a5fb4" + where: str = "above" # "above" | "below" — față de candelă + dy: float = 0.0 # ajustare fină, dacă se suprapune cu altceva + + +@dataclass +class MA: + """Medie mobilă simplă, calculată din close-uri.""" + period: int + color: str = "#b06000" + dash: str = "" + text: str = "" @dataclass class Chart: candles: list[Candle] levels: list[Level] = field(default_factory=list) + marks: list[Mark] = field(default_factory=list) + mas: list[MA] = field(default_factory=list) scale: float = 60.0 # px per unitate de preț width: int = 620 body_w: int = 26 @@ -46,33 +80,60 @@ class Chart: label_gap: int = 34 # spațiu sub cea mai joasă candelă pentru etichete caption: str = "" trend: str = "" # "down" / "up" / "" — segment de trend înainte + vol_h: float = 0.0 # înălțimea panoului de volum (0 = fără panou) + vol_gap: float = 26.0 # spațiu între panoul de preț și cel de volum + vol_label: str = "volum" + title: str = "" # ex. "SPY — daily" def _y(self, price: float) -> float: return self.top + (self._pmax - price) * self.scale + def _cx(self, i: int) -> float: + return self.left + i * self.gap + 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) + if self.title: + self.top += 22 # 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 + panel_bottom = body_bottom + extra + + vols = [c.vol for c in self.candles if c.vol is not None] + vol_top = vol_base = None + if self.vol_h and vols: + vol_top = panel_bottom + self.vol_gap + vol_base = vol_top + self.vol_h + panel_bottom = vol_base + 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) + tags = any(c.tag for c in self.candles) + height = (panel_bottom + (self.label_gap if tags else 18) + + 17 * n_notes + (24 if self.caption else 8)) out = [f''] - # nivelurile: linie pe toată lățimea, eticheta deasupra liniei la dreapta + if self.title: + out.append(f'{self.title}') + + # nivelurile: linie pe toată lățimea, eticheta deasupra liniei for lv in self.levels: y = self._y(lv.price) out.append(f'') - out.append(f'{lv.text}') + if lv.label_at == "left": + out.append(f'{lv.text}') + else: + out.append(f'{lv.text}') x0 = self.left if self.trend: @@ -86,22 +147,72 @@ class Chart: out.append(f'' f'{"downtrend anterior" if self.trend == "down" else "uptrend anterior"}') + # mediile mobile, sub candele ca ordine de desen + closes = [c.c for c in self.candles] + for ma in self.mas: + pts = [] + for i in range(len(closes)): + if i + 1 >= ma.period: + avg = sum(closes[i + 1 - ma.period:i + 1]) / ma.period + pts.append(f"{self._cx(i):.1f},{self._y(avg):.1f}") + if len(pts) >= 2: + dash = f' stroke-dasharray="{ma.dash}"' if ma.dash else "" + out.append(f'') + if ma.text: + last = pts[-1].split(",") + out.append(f'{ma.text}') + for i, cd in enumerate(self.candles): - cx = x0 + i * self.gap + cx = self._cx(i) 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'') + h = max(self._y(bot_body) - self._y(top_body), 1.6) + 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 + # panoul de volum: bare colorate pe direcția candelei + if vol_top is not None: + assert vol_base is not None + vmax = max(vols) + out.append(f'') + for i, cd in enumerate(self.candles): + if cd.vol is None: + continue + bh = cd.vol / vmax * self.vol_h + color = cd.color or ("#188a3e" if cd.c >= cd.o else "#c00") + out.append(f'') + out.append(f'{self.vol_label}') + + # etichete ancorate pe candele anume + for mk in self.marks: + cd = self.candles[mk.i] + cx = self._cx(mk.i) + if mk.where == "above": + y = self._y(cd.h) - 10 + mk.dy + else: + # sub candelă, dar niciodată peste panoul de volum + y = min(self._y(cd.l) + 20 + mk.dy, body_bottom + 14) + anchor = "middle" + if cx < 90: + anchor, cx = "start", cx - 12 + elif cx > self.width - 90: + anchor, cx = "end", cx + 12 + out.append(f'{mk.text}') + + # detaliile 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 + ny = panel_bottom + (self.label_gap if tags else 18) + 4 for cd in self.candles: if cd.note: color = cd.color or ("#188a3e" if cd.c >= cd.o else "#c00") @@ -114,8 +225,46 @@ class Chart: return "\n ".join(out).replace("\n ", "\n") +def walk(start: float, moves: list[tuple[float, float, float]], + vols: list[float] | None = None) -> list[Candle]: + """Construiește o serie continuă de candele dintr-un preț de start. + + Fiecare `move` e (delta_close, wick_sus, wick_jos) față de open-ul barei, + iar open-ul fiecărei bare e close-ul barei anterioare — deci seria nu are + goluri artificiale, exact ca pe un grafic real. + """ + out, o = [], start + for i, (dc, wu, wd) in enumerate(moves): + c = o + dc + h = max(o, c) + wu + l = min(o, c) - wd + out.append(Candle(o, h, l, c, vol=(vols[i] if vols else None))) + o = c + return out + + +def from_closes(closes: list[float], wick: float = 0.0, + wicks: list[tuple[float, float]] | None = None, + vols: list[float] | None = None, + start: float | None = None) -> list[Candle]: + """Serie continuă de candele dintr-o listă de close-uri. + + Open-ul fiecărei bare = close-ul precedentei, deci structura (swing + high-uri, swing low-uri) e exact cea din lista de close-uri — util când + diagrama trebuie să susțină o afirmație despre niveluri. + """ + out = [] + o = start if start is not None else closes[0] + for i, c in enumerate(closes): + wu, wd = wicks[i] if wicks else (wick, wick) + out.append(Candle(o, max(o, c) + wu, min(o, c) - wd, c, + vol=(vols[i] if vols else None))) + o = c + return out + + def selftest(): - """O unitate de preț trebuie să dea aceeași distanță oriunde pe desen.""" + """Scara trebuie să fie aceeași oriunde, iar volumul proporțional.""" 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) @@ -125,7 +274,28 @@ def selftest(): 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 " min(c.l for c in ser[3:5]), \ + "al doilea picior ar trebui să fie mai jos" + + print("selftest ok: 2R la exact 2R, serie fără goluri, volum proporțional") if __name__ == "__main__":