Semnalat de user: diagramele nu erau proportionale cu textul si patternurile erau doar prezentate, fara sa spuna ce fac si cand se tranzactioneaza. Verificat numeric, erau reale: - hammer: entry->SL 75 px/unitate, entry->TP(2R) 41 px/unitate - engulfing: 110 vs 17.5 px/unitate - morning star: 82 vs 10.6 px/unitate - marubozu bull: eticheta de open pusa deasupra celei de close (axa inversata) - etichete taiate de viewBox si suprapuse Fix: - svg_candles.py - generator care calculeaza coordonatele din preturi, cu o singura scara pe desen. Are selftest care verifica exact cazul de mai sus. - gen/patternuri_candele.py - regenereaza fisierul intreg; diagramele nu se mai editeaza de mana - fiecare pattern are acum: ce arata, ce s-a intamplat mecanic, DE CE il urmaresti, CE FACI cand il vezi, trigger DA/NU, si trade complet unde e cazul - adaugate explicatii practice unde lipseau complet: inverted hammer, hanging man, shooting star, spinning top, harami, marubozu (comparatia de risc la intrare directa vs retest midpoint), abandoned baby, piercing line, three white soldiers - STYLE.md: regula de generare + verificarea automata de scara Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtSTyTmt6AbCL9j5ajEDm1
133 lines
5.9 KiB
Python
133 lines
5.9 KiB
Python
"""
|
|
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'<svg viewBox="0 0 {self.width} {height:.0f}" xmlns="http://www.w3.org/2000/svg" '
|
|
f'style="width:100%;height:auto;font:12px sans-serif">']
|
|
|
|
# nivelurile: linie pe toată lățimea, eticheta deasupra liniei la dreapta
|
|
for lv in self.levels:
|
|
y = self._y(lv.price)
|
|
out.append(f'<line x1="24" y1="{y:.1f}" x2="{self.width - 14}" y2="{y:.1f}" '
|
|
f'stroke="{lv.color}" stroke-width="1.5" stroke-dasharray="{lv.dash}"/>')
|
|
out.append(f'<text x="{self.width - 28}" y="{y - 6:.1f}" fill="{lv.color}" '
|
|
f'text-anchor="end">{lv.text}</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'<polyline points="24,{y_start + dy:.1f} {x0 - 8:.0f},{y_start:.1f}" '
|
|
f'fill="none" stroke="#999" stroke-width="2"/>')
|
|
# 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'<text x="24" y="{ty:.1f}" fill="#999">'
|
|
f'{"downtrend anterior" if self.trend == "down" else "uptrend anterior"}</text>')
|
|
|
|
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'<line x1="{cx}" y1="{self._y(cd.h):.1f}" x2="{cx}" y2="{self._y(cd.l):.1f}" '
|
|
f'stroke="#1a1a1a" stroke-width="1.5"/>')
|
|
h = max(self._y(bot_body) - self._y(top_body), 2.0)
|
|
out.append(f'<rect x="{cx - self.body_w // 2}" y="{self._y(top_body):.1f}" '
|
|
f'width="{self.body_w}" height="{h:.1f}" fill="{color}"/>')
|
|
if cd.tag:
|
|
out.append(f'<text x="{cx}" y="{body_bottom + extra + 18:.1f}" fill="{color}" '
|
|
f'text-anchor="middle" style="font-weight:bold">{cd.tag}</text>')
|
|
|
|
# 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'<text x="24" y="{ny:.1f}" fill="{color}">{cd.note}</text>')
|
|
ny += 17
|
|
|
|
if self.caption:
|
|
out.append(f'<text x="24" y="{ny + 8:.0f}" fill="#555">{self.caption}</text>')
|
|
out.append("</svg>")
|
|
return "\n ".join(out).replace("\n </svg>", "\n</svg>")
|
|
|
|
|
|
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 "<svg" in svg and svg.count("\n\n") == 0, "linie goală în svg rupe markdown-ul"
|
|
print("selftest ok: 2R desenat la exact 2R, fără linii goale")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
selftest()
|