From 0dd358802d28b487723391cbf46848e942b9f4b7 Mon Sep 17 00:00:00 2001
From: Claude Agent
Date: Fri, 2 Oct 2026 02:32:27 +0000
Subject: [PATCH] MODUL 5: ultimele patru suprapuneri de eticheta, si
verificatorul scos in gen/eticheta_libera.py
Assert-ul care verifica daca o eticheta sta lipita de o linie a devenit modul
propriu, importat din ambele generatoare de patternuri. Vede acum si
(mediile mobile - punctul lui orb) si, optional, corpurile
candelelor. Avea si un bug tacut: regexul cerea culoare pe 6 cifre, deci sarea
peste tot ce e desenat cu #c00, adica exact sagetile si nivelurile rosii. Are
demo() cu self-check.
La bull_flag eticheta SMA-20 nu e mutabila din generator - pozitia ei o
calculeaza svg_candles - asa ca media e numita in caption, ca la cup with
handle.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
---
gen/eticheta_libera.py | 103 ++++++++++++++++++
gen/patternuri_complexe_3.py | 15 ++-
gen/patternuri_complexe_4.py | 47 +++-----
...02_patternuri_complexe_partea_a_treia.html | 28 ++---
.../02_patternuri_complexe_partea_a_treia.md | 28 ++---
...03_patternuri_complexe_partea_a_patra.html | 15 ++-
.../03_patternuri_complexe_partea_a_patra.md | 15 ++-
7 files changed, 171 insertions(+), 80 deletions(-)
create mode 100644 gen/eticheta_libera.py
diff --git a/gen/eticheta_libera.py b/gen/eticheta_libera.py
new file mode 100644
index 0000000..c12a6eb
--- /dev/null
+++ b/gen/eticheta_libera.py
@@ -0,0 +1,103 @@
+"""O etichetă de diagramă nu are voie să stea peste desen.
+
+`audit_diagrams.py` prinde doar text peste text. Astea sunt celelalte trei
+feluri de suprapunere, găsite vizual pe 02.10.2026 și ușor de regresat la
+următoarea mutare de etichetă:
+
+- `` — niveluri, laturi de wedge, bracket-uri, cozi de săgeată;
+- `` — mediile mobile (punctul orb al primei versiuni);
+- `` — corpurile candelelor și barele de volum.
+
+Verificarea e opt-in, pe etichete numite: pe un grafic de candele aproape
+orice text atinge ceva, iar o regulă globală ar bloca tot repo-ul.
+
+ eticheta_libera(svg, "wedge_desc", {"baza = 4.00"}, rects=True)
+"""
+import re
+
+CW = 6.2 # lățimea unui caracter bold de 11-12px
+WICK = "#1a1a1a" # fitilele: desenul le traversează oricum, prin construcție
+
+
+def _bbox(m, min_px):
+ """Caseta etichetei, lărgită cu marginea cerută."""
+ tx, ty, attrs, s = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
+ w = CW * len(s)
+ x0 = tx if 'anchor="start"' in attrs else tx - w if 'anchor="end"' in attrs else tx - w / 2
+ return s, (x0 - min_px, x0 + w + min_px, ty - 9 - min_px, ty + 3 + min_px)
+
+
+def _segmente(svg):
+ """Toate segmentele desenate, ca perechi de capete: linii + polilinii."""
+ for m in re.finditer(r' x1:
+ return False
+ if ax == bx:
+ ys = (ay, by)
+ else: # y-ul segmentului pe porțiunea comună
+ ys = tuple(ay + (by - ay) * (min(max(x, min(ax, bx)), max(ax, bx)) - ax) / (bx - ax)
+ for x in (x0, x1))
+ return min(ys) <= bot and max(ys) >= top
+
+
+def eticheta_libera(svg, name, etichete, min_px=5.0, rects=False, segmente=True):
+ """Ridică AssertionError dacă vreuna din `etichete` stă peste desen.
+
+ `segmente=False` pentru etichetele de nivel: alea stau lipite de linia lor
+ prin construcție (y − 6), deci doar candelele sunt o suprapunere reală.
+ """
+ segs = list(_segmente(svg)) if segmente else []
+ boxes = []
+ if rects:
+ for m in re.finditer(r'', svg):
+ x, y, w, h = map(float, m.groups()[:4])
+ if "fill-opacity" in m.group(5): # zonele colorate stau în fundal
+ continue
+ boxes.append((x, y, x + w, y + h))
+ for m in re.finditer(r']*)>([^<]*)', svg):
+ if m.group(4) not in etichete:
+ continue
+ s, box = _bbox(m, min_px)
+ for kind, ax, ay, bx, by in segs:
+ if _atinge(box, ax, ay, bx, by):
+ raise AssertionError(f"{name}: eticheta '{s}' sta pe {kind} "
+ f"({ax:.0f},{ay:.0f})-({bx:.0f},{by:.0f})")
+ for rx0, ry0, rx1, ry1 in boxes:
+ if rx0 < box[1] and rx1 > box[0] and ry0 < box[3] and ry1 > box[2]:
+ raise AssertionError(f"{name}: eticheta '{s}' sta pe dreptunghiul "
+ f"({rx0:.0f},{ry0:.0f})-({rx1:.0f},{ry1:.0f})")
+
+
+def demo():
+ svg = ('')
+ for et in ("pe linie", "pe curba", "pe candela"):
+ try:
+ eticheta_libera(svg, "demo", {et}, rects=True)
+ except AssertionError:
+ pass
+ else:
+ raise AssertionError(f"'{et}' trebuia prinsa")
+ eticheta_libera(svg, "demo", {"libera"}, rects=True)
+ print("eticheta_libera: ok")
+
+
+if __name__ == "__main__":
+ demo()
diff --git a/gen/patternuri_complexe_3.py b/gen/patternuri_complexe_3.py
index 876e3ed..5a54f74 100644
--- a/gen/patternuri_complexe_3.py
+++ b/gen/patternuri_complexe_3.py
@@ -10,7 +10,9 @@ import re
import sys
sys.path.insert(0, "/workspace/atm-curs-dl")
+sys.path.insert(0, "/workspace/atm-curs-dl/gen")
from svg_candles import Chart, Level, Mark, MA, Zone, from_closes # noqa: E402
+from eticheta_libera import eticheta_libera # noqa: E402
MD = "/workspace/atm-curs-dl/summaries/06_MODUL_5/02_patternuri_complexe_partea_a_treia.md"
@@ -25,7 +27,7 @@ def hi(cds, a, b):
return round(max(c.h for c in cds[a:b + 1]), 2)
-def check(ch, name, distante=()):
+def check(ch, name, distante=(), clear=(), clear_rect=()):
"""Nicio etichetă nu poate numi un preț care nu e un nivel desenat."""
svg = ch.render()
assert "\n\n" not in svg, f"{name}: linie goală în svg"
@@ -42,6 +44,10 @@ def check(ch, name, distante=()):
for lv in ch.levels:
assert ch._y(lv.price) > sus - 0.06 * (jos - sus), \
f"{name}: nivelul {lv.price} e lipit de marginea de sus"
+ if clear:
+ eticheta_libera(svg, name, set(clear))
+ if clear_rect: # etichete de nivel: doar peste candele e o problemă
+ eticheta_libera(svg, name, set(clear_rect), rects=True, segmente=False)
SVG[name] = svg
@@ -410,13 +416,14 @@ check(Chart(
Level(FUND_C, f"fundul ceștii {FUND_C:.2f}", "#b06000", label_at="left")],
marks=[Mark(5, "top 1", "#c00"), Mark(23, "top 2", "#c00", dy=-18),
Mark(13, "fundul ceștii", "#b06000", "below"),
- Mark(29, "breakout", "#188a3e", dy=-14)],
+ Mark(29, "breakout", "#188a3e", dy=-68)],
zones=[Zone(24, 27, "mânerul", "#b06000")],
mas=[MA(20, "#777", dash="4 3")],
- scale=4.2, gap=15, body_w=9, left=46, width=620, vol_h=70, top=100,
+ scale=4.2, gap=15, body_w=9, left=46, width=700, vol_h=70, top=100,
vol_label="volum", title="Cup with handle — exemplu construit (fără ticker), monthly",
caption="Mânerul: 4 bare, −30% din adâncime. Două top-uri la 120.00 = double top. Gri punctat: SMA-20."),
- "cu", [AD_C, RIM_C - MANER])
+ "cu", [AD_C, RIM_C - MANER],
+ clear=("breakout",), clear_rect=(f"minimul mânerului {MANER:.2f}", "breakout"))
# ----------------------------------------- cup with handle: mâner prea adânc
closes_cn = ([99.0, 105.0, 111.0, 116.0, 119.5] # 0-4 uptrend + rim 1
diff --git a/gen/patternuri_complexe_4.py b/gen/patternuri_complexe_4.py
index 00fd662..b933837 100644
--- a/gen/patternuri_complexe_4.py
+++ b/gen/patternuri_complexe_4.py
@@ -15,7 +15,9 @@ import re
import sys
sys.path.insert(0, "/workspace/atm-curs-dl")
+sys.path.insert(0, "/workspace/atm-curs-dl/gen")
from svg_candles import Chart, Level, Mark, MA, Zone, Arrow, from_closes # noqa: E402
+from eticheta_libera import eticheta_libera # noqa: E402
MD = ("/workspace/atm-curs-dl/summaries/06_MODUL_5/"
"03_patternuri_complexe_partea_a_patra.md")
@@ -58,31 +60,7 @@ def txt(i, p, s, color="#8b5cf6", anchor="middle", dx=0.0, dy=0.0, bold=True):
return dict(i=i, p=p, s=s, color=color, anchor=anchor, dx=dx, dy=dy, bold=bold)
-def _desprinse(svg, name, etichete, min_px=5.0):
- """Etichetele date nu au voie sa stea pe nicio linie (regresia 11/12)."""
- # fitilele candelelor (#1a1a1a) nu se numara: desenul le traverseaza oricum
- lines = [tuple(map(float, m.groups()[:4])) for m in re.finditer(
- r']*)>([^<]*)', svg):
- tx, ty, attrs, s = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
- if s not in etichete:
- continue
- w = 6.2 * len(s)
- x0 = tx if 'anchor="start"' in attrs else tx - w if 'anchor="end"' in attrs else tx - w / 2
- x1, top, bot = x0 + w, ty - 9 - min_px, ty + 3 + min_px
- for lx1, ly1, lx2, ly2 in lines:
- if max(lx1, lx2) < x0 or min(lx1, lx2) > x1:
- continue
- ys = [ly1 + (ly2 - ly1) * (max(min(x, max(lx1, lx2)), min(lx1, lx2)) - lx1)
- / ((lx2 - lx1) or 1) for x in (x0, x1)] if lx1 != lx2 else [ly1, ly2]
- if min(ys) <= bot and max(ys) >= top:
- raise AssertionError(
- f"{name}: eticheta '{s}' sta pe linia "
- f"({lx1:.0f},{ly1:.0f})-({lx2:.0f},{ly2:.0f})")
-
-
-def deco(ch, name, segs=(), texts=(), distante=(), clear=()):
+def deco(ch, name, segs=(), texts=(), distante=(), clear=(), rects=False):
"""Randează graficul, injectează liniile/textele, verifică, salvează."""
saved = ch.top, ch.left, ch.width
try:
@@ -114,7 +92,7 @@ def deco(ch, name, segs=(), texts=(), distante=(), clear=()):
assert m.group(0) in desenate, \
f"{name}: eticheta numește {m.group(0)}, care nu e un nivel desenat"
if clear:
- _desprinse(svg, name, set(clear))
+ eticheta_libera(svg, name, set(clear), rects=rects)
sus = min(ch._y(c.h) for c in ch.candles)
jos = max(ch._y(c.l) for c in ch.candles)
for lv in ch.levels:
@@ -126,6 +104,9 @@ def deco(ch, name, segs=(), texts=(), distante=(), clear=()):
CAP = ("Exemplu construit (fără ticker), la scară: lecția nu rostește nicio "
"cifră. Regula de măsurare e a cursului.")
+# la bull flag eticheta SMA-20 ar cadea exact pe linia TP1: o mut in caption
+CAP_SMA = ("Exemplu construit (fără ticker), la scară. Portocaliu punctat: SMA-20."
+ " Regula de măsurare e a cursului.")
# ============================================================ 1. wedge descendent
U0, L0 = 52.00, 48.00 # laturile la deschiderea triunghiului
@@ -190,12 +171,12 @@ deco(Chart(
Mark(13, "picior 1", "#c00", "below", dy=16),
Mark(19, "picior 2 (mai jos)", "#c00", "below"),
Mark(25, "hammer in capul triunghiului", "#b06000", "below", dy=8),
- Mark(27, "breakout cu volum", "#188a3e")],
+ Mark(27, "breakout cu volum", "#188a3e", "above", dy=-24)],
zones=[Zone(10, 26, "wedge descendent = consolidare contra-trend (17 bare)",
"#8b5cf6")],
arrows=[Arrow(29, "stopul sta sub low-ul candelei de breakout:\n"
"daca pretul se intoarce in triunghi, ideea a murit",
- price=SL_D, where="below", color="#c00", dy=30, dx=44)],
+ price=SL_D, where="below", color="#c00", dy=30, dx=56)],
mas=[MA(20, "#b06000", dash="4 3", text="SMA-20")],
scale=24, gap=16, body_w=9, left=46, width=730, top=62, vol_h=70,
title="Wedge descendent — bull",
@@ -206,11 +187,13 @@ deco(Chart(
seg(13, lo_d(13), 27, lo_d(27)),
seg(10.0, L0, 10.0, U0, "#8b5cf6", "", 2.2),
seg(35.6, E_D, 35.6, TP_D, "#1a5fb4", "", 2.2)],
- texts=[txt(10.1, (U0 + L0) / 2, f"baza = {BAZA_D:.2f}", anchor="start", dx=4),
+ texts=[txt(10.1, (U0 + L0) / 2, f"baza = {BAZA_D:.2f}", anchor="start",
+ dx=4, dy=16),
txt(35.8, (E_D + TP_D) / 2, f"aceeasi {BAZA_D:.2f}", "#1a5fb4",
anchor="start", dx=4)],
distante=(BAZA_D,),
- clear=("picior 1", "breakout cu volum", "hammer in capul triunghiului"))
+ clear=("picior 1", "breakout cu volum", "hammer in capul triunghiului",
+ f"baza = {BAZA_D:.2f}", f"aceeasi {BAZA_D:.2f}"))
# ================================================ 2. wedge vs flag (forma)
cl = (ramp(29.6, 34.00, 6) # 0-5 urcare
@@ -449,10 +432,10 @@ deco(Chart(
Mark(20, "C = aici era by the dip", "#b06000", "below"),
Mark(23, "breakout cu volum", "#188a3e", dy=-36)],
zones=[Zone(11, 22, "flamura (12 bare)", "#999")],
- mas=[MA(20, "#b06000", dash="4 3", text="SMA-20")],
+ mas=[MA(20, "#b06000", dash="4 3")],
scale=13, gap=16, body_w=9, left=46, width=730, top=62, vol_h=70,
title="Bull flag = ABCD long",
- caption=CAP),
+ caption=CAP_SMA),
"bull_flag",
segs=[seg(11, 40.00, 24, up_f(24), "#1a5fb4"),
seg(11, 37.00, 14, lo_f(14), "#1a5fb4", "4 3", 1.2),
diff --git a/summaries/06_MODUL_5/02_patternuri_complexe_partea_a_treia.html b/summaries/06_MODUL_5/02_patternuri_complexe_partea_a_treia.html
index a372d02..eb78017 100644
--- a/summaries/06_MODUL_5/02_patternuri_complexe_partea_a_treia.html
+++ b/summaries/06_MODUL_5/02_patternuri_complexe_partea_a_treia.html
@@ -910,23 +910,23 @@ diferența față de rounding bottom, pentru că formele sunt aproape identice:<
vezi după o urcare, e o ceașcă. Aceeași curbă, două interpretări diferite,
pentru că trendul dinainte e diferit.
-
top 1top 2fundul ceștii
- breakout
+ breakoutMânerul: 4 bare, −30% din adâncime. Două top-uri la 120.00 = double top. Gri punctat: SMA-20.
Părțile formei, cu numele din curs:
diff --git a/summaries/06_MODUL_5/02_patternuri_complexe_partea_a_treia.md b/summaries/06_MODUL_5/02_patternuri_complexe_partea_a_treia.md
index 9c3638b..b39dd14 100644
--- a/summaries/06_MODUL_5/02_patternuri_complexe_partea_a_treia.md
+++ b/summaries/06_MODUL_5/02_patternuri_complexe_partea_a_treia.md
@@ -920,23 +920,23 @@ vezi după o urcare, e o ceașcă. Aceeași curbă, două interpretări diferite
pentru că trendul dinainte e diferit.
-
+Cup with handle — exemplu construit (fără ticker), monthlymânerul
-
- target 160.00 = 120.00 + 40.00
-
+
+ target 160.00 = 120.00 + 40.00
+ intrare 121.00
-
- rezistență 120.00
-
- minimul mânerului 108.00
-
+
+ rezistență 120.00
+
+ minimul mânerului 108.00
+ SL 107.00
-
- limita de 50% 100.00
-
+
+ limita de 50% 100.00
+ fundul ceștii 80.00
@@ -1011,7 +1011,7 @@ pentru că trendul dinainte e diferit.
-
+
@@ -1052,7 +1052,7 @@ pentru că trendul dinainte e diferit.
top 1top 2fundul ceștii
- breakout
+ breakoutMânerul: 4 bare, −30% din adâncime. Două top-uri la 120.00 = double top. Gri punctat: SMA-20.
diff --git a/summaries/06_MODUL_5/03_patternuri_complexe_partea_a_patra.html b/summaries/06_MODUL_5/03_patternuri_complexe_partea_a_patra.html
index 7a4674a..819d806 100644
--- a/summaries/06_MODUL_5/03_patternuri_complexe_partea_a_patra.html
+++ b/summaries/06_MODUL_5/03_patternuri_complexe_partea_a_patra.html
@@ -241,18 +241,18 @@ cursului (buy doji, buy momentum, buy hammer, bull engulfing) și dacă zona e
picior 1picior 2 (mai jos)hammer in capul triunghiului
- breakout cu volum
-
-
- stopul sta sub low-ul candelei de breakout:
- daca pretul se intoarce in triunghi, ideea a murit
+ breakout cu volum
+
+
+ stopul sta sub low-ul candelei de breakout:
+ daca pretul se intoarce in triunghi, ideea a muritExemplu construit (fără ticker), la scară: lecția nu rostește nicio cifră. Regula de măsurare e a cursului.
- baza = 4.00
+ baza = 4.00aceeasi 4.00
Măsurarea, cuvânt cu cuvânt din curs: „dacă măsori distanța de aici
@@ -851,7 +851,6 @@ urmat de o flamură: un canal îngust, cu laturi paralele, înclinat u
TP2 47.80 = batul steagului = 5.6R
- SMA-20
@@ -963,7 +962,7 @@ urmat de o flamură: un canal îngust, cu laturi paralele, înclinat u
BC = aici era by the dipbreakout cu volum
- Exemplu construit (fără ticker), la scară: lecția nu rostește nicio cifră. Regula de măsurare e a cursului.
+ Exemplu construit (fără ticker), la scară. Portocaliu punctat: SMA-20. Regula de măsurare e a cursului.
diff --git a/summaries/06_MODUL_5/03_patternuri_complexe_partea_a_patra.md b/summaries/06_MODUL_5/03_patternuri_complexe_partea_a_patra.md
index dd58b71..815046a 100644
--- a/summaries/06_MODUL_5/03_patternuri_complexe_partea_a_patra.md
+++ b/summaries/06_MODUL_5/03_patternuri_complexe_partea_a_patra.md
@@ -219,18 +219,18 @@ cursului (buy doji, buy momentum, buy hammer, bull engulfing) și dacă zona e
picior 1picior 2 (mai jos)hammer in capul triunghiului
- breakout cu volum
-
-
- stopul sta sub low-ul candelei de breakout:
- daca pretul se intoarce in triunghi, ideea a murit
+ breakout cu volum
+
+
+ stopul sta sub low-ul candelei de breakout:
+ daca pretul se intoarce in triunghi, ideea a muritExemplu construit (fără ticker), la scară: lecția nu rostește nicio cifră. Regula de măsurare e a cursului.
- baza = 4.00
+ baza = 4.00aceeasi 4.00
@@ -860,7 +860,6 @@ urmat de o *flamură*: un canal îngust, cu laturi paralele, înclinat ușor
TP2 47.80 = batul steagului = 5.6R
- SMA-20
@@ -972,7 +971,7 @@ urmat de o *flamură*: un canal îngust, cu laturi paralele, înclinat ușor
BC = aici era by the dipbreakout cu volum
- Exemplu construit (fără ticker), la scară: lecția nu rostește nicio cifră. Regula de măsurare e a cursului.
+ Exemplu construit (fără ticker), la scară. Portocaliu punctat: SMA-20. Regula de măsurare e a cursului.