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
This commit is contained in:
Claude Agent
2026-09-20 12:43:12 +00:00
parent 63c01b5ee9
commit f1033a916c
19 changed files with 1407 additions and 670 deletions

View File

@@ -1,10 +1,16 @@
"""ULTA — daily (pivoți monthly): diagrama de ansamblu a selecției (TIP: PRET).
Citește `gen/strategii/selectia_ansamblu.spec.txt`. Un simbol care vine dintr-un
downtrend weekly, atinge suportul monthly 355.48 și face un buy doji la 373.30
cu RVOL 0.61; apoi confirmarea (intrare 374.80) și drumul spre pivot/TP1 383.74,
respectiv TP2 402.00. Grafic daily, 32 de candele, serie continuă, cu panou de
volum și SMA-20 — construit prin `svg_candles` (from_closes), la scară.
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
@@ -15,18 +21,20 @@ import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402
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%/bara)
# bare 12-19 : baza/churn 356-373, minimul atinge suportul 355.48 pe bara 15
# 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)
# bara 21 : confirmare, inchide peste intrarea 374.80
# bare 22-31 : urcare spre pivot/TP1 383.74 (bara 26) si apoi spre TP2 402.00
# 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
@@ -38,92 +46,106 @@ CLOSES = [
assert len(CLOSES) == 32, len(CLOSES)
WICKS = [(0.35, 0.40)] * 32
WICKS[0] = (0.40, 0.50) # prima bara, deschide dintr-un varf al downtrend-ului
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[20] = (0.80, 0.30) # doji: high 374.10, low 372.70
WICKS[31] = (0.55, 0.40) # high 402.05, atinge TP2 402.00
WICKS[DOJI] = (1.45, 1.45) # doji: corp 0.30 din 3.20 înălțime = 9 %
assert len(WICKS) == 32, len(WICKS)
# barele 25-31 in crestere; bara 20 (doji) sub media seriei (RVOL 0.61)
# 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 confirmare
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 VOLS[20] < media_vol, "bara 20 (doji) trebuie sub media seriei"
assert all(a < b for a, b in zip(VOLS[25:32], VOLS[26:32])), "barele 25-31 nu cresc"
# RVOL afisat (0.61) ~ raportul barei de doji fata de media seriei
assert abs(VOLS[20] / media_vol - 0.61) < 0.02, VOLS[20] / media_vol
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)
# ---------------------------------------------------------------- assert-urile din spec
assert abs(cds[20].c - 373.30) < 1e-9, "doji-ul trebuie sa inchida la 373.30"
assert abs(max(cds[20].o, cds[20].c) - min(cds[20].o, cds[20].c)) <= 0.40, "bara 20 nu e doji"
assert abs(min(c.l for c in cds[12:21]) - 355.48) < 0.05, "suportul 355.48 nu e atins"
assert 352.00 < 355.48 < 373.30 < 374.80 < 383.74 < 402.00, "ordinea nivelurilor"
risc = 374.80 - 352.00 # 22.80
assert abs((383.74 - 374.80) / risc - 0.39) < 0.02, "TP1 nu e ~0.39R"
assert abs((402.00 - 374.80) / risc - 1.19) < 0.02, "TP2 nu e ~1.19R"
assert cds[-1].c >= 383.74, "TP1 nu e atins in serie"
assert min(c.l for c in cds) >= 351.5, "SL-ul de 352.00 nu poate fi sub minimul seriei"
# 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 intre bare"
assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial între bare"
# structura ceruta explicit: doji mic cu fitilele date, confirmare peste intrare
assert abs(cds[20].h - 374.10) < 0.05 and abs(cds[20].l - 372.70) < 0.05, (cds[20].h, cds[20].l)
assert cds[21].c > 374.80, "bara 21 nu confirma intrarea"
assert max(cds[i].h for i in range(22, 27)) >= 383.74, "TP1 nu e atins in jurul barei 26"
assert cds[31].h >= 402.00, "TP2 nu e atins spre final"
# 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
# ---------------------------------------------------------------- niveluri, etichete
levels = [
Level(352.00, "SL 352.00", RED, label_at="left"),
Level(355.48, "suport monthly 355.48", BLUE, label_at="left"),
Level(374.80, "intrare 374.80", GREEN),
Level(383.74, "pivot / TP1 383.74", BLUE),
Level(402.00, "TP2 402.00", BLUE),
Level(SUPORT, f"suport monthly {SUPORT:.2f}", BLUE, label_at="left"),
Level(PIVOT, f"pivot monthly {PIVOT:.2f}", BLUE),
]
marks = [
# „sub bara 0" ar insemna peste candelele care coboara (staircase); dy-ul
# o ridica deasupra varfului de start, unde textul se citeste curat
Mark(0, "downtrend weekly", GRAY, "below", dy=-62),
Mark(20, "buy doji close 373.30 (RVOL 0.61)", TEAL, "below"),
Mark(21, "intrare 374.80", GREEN, "above"),
Mark(26, "TP1 383.74 = pivot", BLUE, "above", dy=-8),
Mark(31, "TP2 402.00", BLUE, "above", dy=-22),
zones = [
Zone(0, 11, "1. downtrend — de asta apare în scaner", RED),
Zone(12, DOJI, "", BLUE, p_bot=SUPORT - 0.6),
]
caption = ("Ale cursului: close 373.30, suport 355.48, pivot 383.74, RVOL 0.61. "
"Intrarea 374.80, SL 352.00 si TP2 402.00 sunt estimari din 04_MODUL_3/01; "
"structura barelor dintre niveluri e construita, la scara.")
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, marks=marks,
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 (pivoți monthly)",
title="ULTA — daily: ce citește selecția și unde se oprește",
caption=caption,
)
svg = ch.render()
# ---------------------------------------------------------------- verificarea de scara
ys = {p: ch._y(p) for p in (352.00, 355.48, 374.80, 383.74, 402.00)}
px_risc = ys[352.00] - ys[374.80] # y creste in jos, deci SL-ul are y mai mare
assert abs((ys[374.80] - ys[383.74]) / px_risc - (383.74 - 374.80) / risc) < 1e-9, "TP1 la scara gresita"
assert abs((ys[374.80] - ys[402.00]) / px_risc - (402.00 - 374.80) / risc) < 1e-9, "TP2 la scara gresita"
# ---------------------------------------------------------------- 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>")
# ---------------------------------------------------------------- finisaj text
# caption-ul pe un singur rand depaseste latimea panzei (se taie la viewBox):
# il rupem in linii si crestem inaltimea desenului.
# ---------------------------------------------------------------- caption pe mai multe rânduri
def _wrap(text: str, max_chars: int) -> list[str]:
lines, cur = [], ""
for w in text.split():
@@ -138,25 +160,27 @@ def _wrap(text: str, max_chars: int) -> list[str]:
cap_lines = _wrap(caption, 92)
m = re.search(r'<text x="24" y="(\d+)" fill="#555">' + re.escape(caption) + r"</text>\s*</svg>", svg)
assert m, "caption-ul nu a fost gasit in SVG"
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 + "\n</svg>"
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)}"', 1)
f'viewBox="0 0 {vm.group(1)} {int(vm.group(2)) + 17 * (len(cap_lines) - 1) + 24}"', 1)
# „SMA-20" (x=515, y=269) sta la 12.6px de eticheta „intrare 374.80" (y=281.6),
# adica lipite: o ridicam 12px pe linia libera de deasupra.
svg, n = re.subn(r'(<text x="\d+" y=")(\d+)(" fill="#b06000">SMA-20</text>)',
lambda mm: f'{mm.group(1)}{int(mm.group(2)) - 12}{mm.group(3)}', svg)
assert n == 1, f"eticheta SMA-20: {n} potriviri"
# ---------------------------------------------------------------- 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 goala in SVG"
assert "<svg" in svg and "\n\n" not in svg, "linie goală în SVG"
OUT.write_text(svg)
print(f"scris {OUT.name}: {len(cds)} candele, doji {cds[20].c:.2f} (RVOL {VOLS[20]/media_vol:.2f}), "
f"suport {min(c.l for c in cds[12:21]):.2f}, intrare 374.80, SL 352.00, "
f"TP1 383.74 = 0.39R, TP2 402.00 = 1.19R, ultimul close {cds[-1].c:.2f}")
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")