Files
atm-curs-dl/gen/strategii/selectia_ansamblu.py
Claude Agent f1033a916c selectia: diagramele refacute dupa sablon (textul urmeaza)
Agentul a atins pragul de context dupa diagrame; capitolul 04_selectia
e inca in formatul vechi si se reia separat.

Ce s-a schimbat: ansamblul nu mai arata ULTA mergand la TP2 — ULTA pica
chiar filtrul de selectie (RVOL 0.61 si 5.01 % de suport), deci nu putea
fi exemplul-vitrina; acum arata doar ce citeste selectia, cu granita
"aici se termina selectia" marcata. Funnel-ul trece de la 8 trepte la 6,
aliniat cu pasii nivelului 2, si fiecare treapta spune de ce. Cadranele
primesc mini-histograme RS, ca sa se recunoasca dintr-un desen, nu
dintr-o propozitie. Trimiterile fisier:linie sunt scoase de pe desene.

Noi: univers (pragurile de pret si volum), sectoare (dominantul din cele
11), proximitate (banda de 3 % peste suportul monthly, cu observatia ca
nu e pragul de 1-2 % din Buy Doji — masoara altceva).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-20 12:43:12 +00:00

187 lines
8.9 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
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ă
m_off = re.search(r'<line x1="24" y1="([\d.]+)"', svg)
assert m_off, "nu găsesc linia primului Level, din care citesc offsetul pe y"
OFF = float(m_off.group(1)) - ch._y(levels[0].price)
# 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")