Files
atm-curs-dl/gen/strategii/selectia_ansamblu.py
Claude Agent 84f933bcfa svg_candles: cautarea nivelurilor, scoasa in functie comuna; 11 generatoare reparate
Unsprezece generatoare nu mai rulau deloc la HEAD: cautau <line x1="24", iar
svg_candles scrie x1="24.0" de cand are Level.i0. Un generator mort inseamna
ca diagrama nu mai poate fi regenerata, si nimeni n-ar fi aflat pana cand ar
fi incercat cineva, peste luni.

Cele 11 copii ale aceluiasi regex au disparut: y_niveluri() si offset_render()
se agata acum de semnatura liniei de Level (stroke-width + dasharray), nu de
x-ul ei. Caz nou in selftest, cu Level(i0=) si panou de volum - combinatia la
care varianta veche fie crapa, fie prindea tacut linia de zero a volumului.

41 de SVG-uri regenerate, toate identice cu cele comise dupa normalizarea
x1=24 -> x1=24.0, deci niciun desen n-a sarit. Verificarea asta a prins o
regresie reala: la buy_doji_transe substitutia lasase scaderea veche pe randul
urmator, offsetul se scadea de doua ori si desenul urca 92 px.

Inca doua generatoare moarte din aceeasi familie, gasite pe drum
(intrarea_cazul_lui, intrarea_doua_variante): desenau peste grafic citind
ch._y() fara offset. Nu crapau zgomotos - produceau suprapuneri la 22 px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-10-02 06:12:32 +00:00

186 lines
8.8 KiB
Python

"""ULTA — daily (pivoți monthly): diagrama de ansamblu a selecției (TIP: PRET).
Rolul diagramei în capitol: arată **unde se termină selecția**. Filtrul citește
contextul (downtrend, suportul monthly 355.48, pivotul monthly 383.74, buy doji
la 373.30, RVOL 0.61) și se oprește la „simbolul intră pe listă". Intrarea,
stopul și ținta nu apar aici, pentru că nu aparțin selecției — sunt ale
capitolului 1 (Buy Doji). Partea de după linia verticală e desenată gri tocmai
ca să se vadă că nu mai e treaba filtrului.
Cifrele cursului: close 373.30, suport monthly 355.48, pivot monthly 383.74,
RVOL 0.61 (`04_MODUL_3/01_filtre_scaner_bull_bear_reversal_pp.md`). Procentele
de 5.01 % și 2.72 % se recalculează din ele, cu assert. Structura barelor e
construită, la scară, prin `svg_candles` (from_closes).
Rulează:
python3 gen/strategii/selectia_ansamblu.py
Scrie lângă el `selectia_ansamblu.svg`.
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import (Chart, Level, Arrow, Zone, MA, from_closes, # noqa: E402
offset_render)
OUT = Path(__file__).resolve().parent / "selectia_ansamblu.svg"
GRAY, RED, GREEN, BLUE, ORANGE, TEAL = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000", "#00a0aa"
SUPORT, PIVOT, DOJI_CLOSE = 355.48, 383.74, 373.30
DOJI = 20 # indexul candelei de buy doji
# ---------------------------------------------------------------- seria (32 bare)
# bare 0-11 : downtrend vizibil de la ~395 la ~362 (~0.8 %/bară)
# bare 12-19 : baza, minimul atinge suportul monthly 355.48 pe bara 15
# bara 20 : buy doji, close 373.30 (open = close[19] = 373.00, corp 0.30)
# bare 21-31 : ce urmează — nu mai e treaba selecției, desenat gri
CLOSES = [
395.00, 391.80, 388.60, 385.20, 382.00, 378.90, 375.70, 372.80, # 0-7
369.90, 367.00, 364.20, 361.80, # 8-11
363.20, 361.00, 358.50, 356.20, 360.00, 365.00, 369.50, 373.00, # 12-19
373.30, 375.60, # 20-21
377.00, 378.50, 380.40, 382.00, 384.50, 386.50, 389.50, 393.00, # 22-29
397.50, 401.50, # 30-31
]
assert len(CLOSES) == 32, len(CLOSES)
WICKS = [(0.35, 0.40)] * 32
WICKS[0] = (0.40, 0.50) # prima bară, deschide dintr-un vârf al downtrend-ului
WICKS[15] = (0.30, 0.72) # low exact 355.48 = suportul monthly
WICKS[16] = (0.40, 0.30) # rebound, nu sparge suportul
WICKS[DOJI] = (1.45, 1.45) # doji: corp 0.30 din 3.20 înălțime = 9 %
assert len(WICKS) == 32, len(WICKS)
# bara 20 (doji) sub media seriei: exact ce înseamnă RVOL 0.61
VOLS = [
42, 44, 41, 46, 43, 45, 40, 44, 47, 43, # 0-9
40, 42, 38, 41, 52, # 10-14
56, 50, 45, 42, 39, # 15-19
30, # 20 doji, sub medie
46, # 21
48, 50, 52, 54, 58, 62, 66, 70, 76, 82, # 22-31
]
assert len(VOLS) == 32, len(VOLS)
media_vol = sum(VOLS) / len(VOLS)
assert abs(VOLS[DOJI] / media_vol - 0.61) < 0.02, VOLS[DOJI] / media_vol
cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=396.50)
# barele de după doji sunt gri: nu mai sunt în sarcina filtrului
for cd in cds[DOJI + 1:]:
cd.color = "#bbb"
# ---------------------------------------------------------------- assert-uri de conținut
assert abs(cds[DOJI].c - DOJI_CLOSE) < 1e-9, "doji-ul trebuie să închidă la 373.30"
corp = abs(cds[DOJI].o - cds[DOJI].c)
inaltime = cds[DOJI].h - cds[DOJI].l
assert corp / inaltime <= 0.10, f"bara {DOJI} nu e doji: corp {corp / inaltime:.0%} din înălțime"
assert abs(min(c.l for c in cds[12:DOJI + 1]) - SUPORT) < 0.05, "suportul 355.48 nu e atins"
# de la intrarea în bază și până la doji, prețul stă sub pivot: de asta pivotul
# e rezistența cea mai apropiată deasupra doji-ului
assert max(c.h for c in cds[12:DOJI + 1]) < PIVOT, "pivotul nu are voie atins în bază"
for a, b in zip(cds, cds[1:]):
assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial între bare"
# procentele din etichete se calculează, nu se scriu de mână
pct_suport = (DOJI_CLOSE - SUPORT) / SUPORT * 100 # 5.01 %
pct_pivot = (PIVOT - DOJI_CLOSE) / PIVOT * 100 # 2.72 %
assert abs(pct_suport - 5.01) < 0.01, pct_suport
assert abs(pct_pivot - 2.72) < 0.01, pct_pivot
rvol = VOLS[DOJI] / media_vol
levels = [
Level(SUPORT, f"suport monthly {SUPORT:.2f}", BLUE, label_at="left"),
Level(PIVOT, f"pivot monthly {PIVOT:.2f}", BLUE),
]
zones = [
Zone(0, 11, "1. downtrend — de asta apare în scaner", RED),
Zone(12, DOJI, "", BLUE, p_bot=SUPORT - 0.6),
]
arrows = [
Arrow(15, f"2. suportul monthly {SUPORT:.2f} a mai întors prețul o dată:\n"
f"acolo stau ordine de cumpărare, deci coborârea are unde să se oprească",
where="below", price=SUPORT, color=BLUE, dx=-30, dy=34),
Arrow(DOJI, f"3. buy doji, close {DOJI_CLOSE:.2f} — aici se oprește selecția\n"
f"{pct_suport:.2f} % peste suport, {pct_pivot:.2f} % sub pivot: trece proximitatea față de pivot\n"
f"volumul zilei = {rvol:.2f} din medie (RVOL {rvol:.2f}), sub pragul de 1.00: minus, nu veto",
where="above", color=TEAL, dx=-250, dy=-66),
Arrow(27, "trigger, stop și țintă sunt\nale capitolului Buy Doji",
where="right", color=GRAY, dx=16, dy=-40),
]
caption = ("Ale cursului: close 373.30, suport monthly 355.48, pivot monthly 383.74, RVOL 0.61. "
"Procentele sunt calculate din ele. Structura barelor e construită, la scară. "
"Intrarea, stopul și ținta nu apar pe desen: nu aparțin selecției.")
ch = Chart(
candles=cds, levels=levels, zones=zones, arrows=arrows,
mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
scale=8.5, gap=15, body_w=9, left=44, width=640, vol_h=70,
vol_label="volum",
title="ULTA — daily: ce citește selecția și unde se oprește",
caption=caption,
)
svg = ch.render()
# ---------------------------------------------------------------- linia verticală de predare
# offsetul intern al randării, citit din prima linie de Level desenată
OFF = offset_render(svg, ch)
# render() mută și `left`; offsetul pe x se citește din dreptunghiul primei zone
m_dx = re.search(r'<rect x="([\d.]+)"[^>]*fill="' + RED + r'"', svg)
assert m_dx, "nu găsesc dreptunghiul primei zone, din care citesc offsetul pe x"
DX = float(m_dx.group(1)) - (ch._cx(0) - ch.body_w / 2 - 5)
x_sep = ch._cx(DOJI) + DX + ch.gap / 2
y_top = ch._y(max(c.h for c in cds)) + OFF + 4
y_bot = ch._y(min(c.l for c in cds)) + OFF + 16
sep = (f'<line x1="{x_sep:.1f}" y1="{y_top:.1f}" x2="{x_sep:.1f}" y2="{y_bot:.1f}" '
f'stroke="#444" stroke-width="1.5" stroke-dasharray="6 4"/>'
f'<text x="{x_sep - 6:.1f}" y="{y_bot + 14:.1f}" fill="#444" text-anchor="end" '
f'style="font-weight:bold">aici se termină selecția</text>'
f'<text x="{x_sep + 6:.1f}" y="{y_bot + 14:.1f}" fill="#999">aici începe capitolul 1</text>')
svg = svg.replace("</svg>", sep + "</svg>")
# ---------------------------------------------------------------- caption pe mai multe rânduri
def _wrap(text: str, max_chars: int) -> list[str]:
lines, cur = [], ""
for w in text.split():
if cur and len(cur) + 1 + len(w) > max_chars:
lines.append(cur)
cur = w
else:
cur = f"{cur} {w}".strip()
if cur:
lines.append(cur)
return lines
cap_lines = _wrap(caption, 92)
m = re.search(r'<text x="24" y="(\d+)" fill="#555">' + re.escape(caption) + r"</text>", svg)
assert m, "caption-ul nu a fost găsit în SVG"
y0 = int(m.group(1))
wrapped = "\n ".join(f'<text x="24" y="{y0 + 17 * i}" fill="#555">{ln}</text>'
for i, ln in enumerate(cap_lines))
svg = svg[:m.start()] + wrapped + svg[m.end():]
vm = re.search(r'viewBox="0 0 (\d+) (\d+)"', svg)
assert vm
svg = svg.replace(vm.group(0),
f'viewBox="0 0 {vm.group(1)} {int(vm.group(2)) + 17 * (len(cap_lines) - 1) + 24}"', 1)
# ---------------------------------------------------------------- nicio cifră inventată în etichete
desenate = {f"{SUPORT:.2f}", f"{PIVOT:.2f}", f"{DOJI_CLOSE:.2f}"}
for txt in re.findall(r"<text[^>]*>([^<]*)</text>", svg):
for nr in re.findall(r"\d+\.\d\d", txt):
assert nr in desenate | {f"{pct_suport:.2f}", f"{pct_pivot:.2f}",
f"{rvol:.2f}", "1.00"}, \
f"eticheta numește {nr}, care nu e un nivel desenat: {txt!r}"
assert "<svg" in svg and "\n\n" not in svg, "linie goală în SVG"
OUT.write_text(svg)
print(f"scris {OUT.name}: 32 candele, doji {cds[DOJI].c:.2f} (RVOL {rvol:.2f}), "
f"suport {SUPORT:.2f} ({pct_suport:.2f} % sub doji), pivot {PIVOT:.2f} "
f"({pct_pivot:.2f} % peste doji), fără intrare/stop/țintă pe desen")