Files
atm-curs-dl/gen/strategii/selectia_rvol.py
Claude Agent 76011566ee diagrame: ultimele patru etichete ramase in urma textului corectat
selectia_rvol: 'sub jumatate fata de normal' pentru 0,61 -> '61 % dintr-o zi
normala'. mdsrt_exemplu: legenda spunea ca nivelurile SPY sunt 'reale, din
curs'; sunt reconstruite dupa ilustratia unui sumar Q&A. mbt_iesire: 2.04R ->
2,0R, cifra sursei, prin rotunjire la afisare, fara sa se atinga nicio serie;
assert-ul verifica acum textul din SVG-ul randat, nu doar valoarea bruta.
atmi_exemplu: '13 luni mai tarziu' era citit ca fapt al materialului - acum
spune ca 13 e cifra desenului, materialul da 'aproximativ un an'; si cele
patru marcaje de completare din desenele atmi sunt uniformizate.

Plus cei trei agenti opencode scrisi pentru rundele astea: verif_strat,
corector_strat, reparator_grafic.

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

327 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Relative volume (RVOL) — bara zilei față de media ultimelor 10 bare.
Diagramă conceptuală (NU grafic de preț): pentru
`summaries/17_STRATEGII/04_selectia.md`, Nivel 2, Pasul 6. Citește
specificația `gen/strategii/selectia_rvol.spec.txt` și scrie
`gen/strategii/selectia_rvol.svg`.
Nu folosește `svg_candles` / `pchart`: SVG-ul e construit ca string, doar
stdlib. Fără `<g transform>` — toate coordonatele sunt absolute.
Ce arată:
- două panouri cu ACELAȘI istoric de 10 bare de volum și aceeași medie;
- linia mediei = 100 %, punctată, etichetată pe ambele panouri;
- bara „azi": stânga de 2x media (RVOL 200 %, cazul HIG), dreapta 0.61x
media (RVOL 0.61, cazul ULTA);
- banda de jos: sub 100 % e minus, nu veto (cele trei rezultate reale ale
scanerului de Buy Doji: 0.86, 1.03, 0.61);
- nota: media e pe 10 bare, nu pe 50 ca în setările implicite.
Singurul prag din material e „peste 100 %"; nu se desenează alt prag.
Rulare: .venv/bin/python gen/strategii/selectia_rvol.py
"""
import html
import re
import xml.etree.ElementTree as ET
from pathlib import Path
W, H = 700, 764
BLACK = "#000000"
GRAY = "#777777"
RED = "#c00"
GREEN = "#188a3e"
BLUE = "#1a5fb4"
ORANGE = "#b06000"
GREEN_BG = "#eef7f0"
RED_BG = "#fdeeee"
NOTE_BG = "#fdf0e0"
GRAY_BG = "#f7f7f7"
# ------------------------------------------------------- date (unități de volum)
# Istoricul e ales ca media lui să fie exact 100 -> „media = 100 %".
ISTORIC = [82, 95, 88, 91, 97, 140, 103, 110, 120, 74]
MEDIA = sum(ISTORIC) / len(ISTORIC) # = 100.0
AZI_HIG = 2.00 * MEDIA # RVOL 200 % (cazul HIG)
AZI_ULTA = 0.61 * MEDIA # RVOL 0.61 (cazul ULTA)
# ------------------------------------------------------- geometrie panouri
PAN_Y0, PAN_Y1 = 128.0, 438.0 # cadrul panoului
BASE = 430.0 # linia de 0 a volumului
UNIT = 0.84 # px per unitate de volum
PAN_W = 284.0
PAN_AX, PAN_BX = 40.0, 376.0 # marginea stângă a fiecărui panou
TEXTS = [] # (x, y, s, size, weight, fill, anchor)
RECTS = [] # (x, y, w, h, tag)
def add(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"):
TEXTS.append((float(x), float(y), s, size, weight, fill, anchor))
return s
def est_w(s, size, weight):
factor = 0.62 if weight == "bold" else 0.56
return len(s) * size * factor
def txt(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"):
return (f'<text x="{x:.1f}" y="{y:.1f}" font-size="{size}" '
f'font-weight="{weight}" fill="{fill}" text-anchor="{anchor}">'
f'{html.escape(s, quote=False)}</text>')
def rect(x, y, w, h, tag, fill="#ffffff", stroke=GRAY, sw=1.5, dash=None,
opacity=None):
RECTS.append((float(x), float(y), float(w), float(h), tag))
d = f' stroke-dasharray="{dash}"' if dash else ""
o = f' fill-opacity="{opacity}"' if opacity is not None else ""
return (f'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}" '
f'fill="{fill}"{o} stroke="{stroke}" stroke-width="{sw}"{d}/>')
def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None):
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
f'stroke="{stroke}" stroke-width="{width}"{d}/>')
def arrow(x1, y1, x2, y2, stroke=BLACK, width=1.6):
"""Săgeată verticală simplă (vârf triunghiular la capătul (x2, y2))."""
dy = 1 if y2 > y1 else -1
return [line(x1, y1, x2, y2 - dy * 7, stroke, width),
f'<polygon points="{x2:.1f},{y2:.1f} {x2 - 5:.1f},{y2 - dy * 8:.1f} '
f'{x2 + 5:.1f},{y2 - dy * 8:.1f}" fill="{stroke}"/>']
# ---------------------------------------------------------------- panou
BARS = {} # tag panou -> listă de (x_centru, y_top, înălțime, valoare)
def panou(tag, px, azi, culoare_azi, eticheta_azi, subeticheta_azi, captura):
"""Desenează un panou: 10 bare de istoric + linia mediei + bara „azi"."""
out = [rect(px, PAN_Y0, PAN_W, PAN_Y1 - PAN_Y0, f"pan_{tag}",
fill="#ffffff", stroke=GRAY, sw=1.2)]
vals = ISTORIC + [azi]
slot = PAN_W / len(vals)
bw = slot * 0.62
bars = []
for i, v in enumerate(vals):
cx = px + slot * (i + 0.5)
h = v * UNIT
top = BASE - h
ultima = (i == len(vals) - 1)
col = culoare_azi if ultima else "#9aa3ad"
out.append(f'<rect x="{cx - bw / 2:.1f}" y="{top:.1f}" width="{bw:.1f}" '
f'height="{h:.1f}" fill="{col}" '
f'fill-opacity="{1.0 if ultima else 0.55}" '
f'stroke="{col if ultima else "#7a828a"}" stroke-width="1"/>')
bars.append((cx, top, h, v))
BARS[tag] = bars
# linia de bază (volum 0) și linia mediei
out.append(line(px, BASE, px + PAN_W, BASE, BLACK, 1.2))
y_med = BASE - MEDIA * UNIT
out.append(line(px, y_med, px + PAN_W, y_med, ORANGE, 2.0, "5 3"))
# eticheta stă în stânga, unde barele sunt sub linie, ca să nu cadă peste ele
add(px + 6, y_med - 6, "media = 100 %", 13, "bold", ORANGE, "start")
# eticheta barei „azi" + săgeata către vârful ei
cx, top, _h, _v = bars[-1]
add(px + PAN_W / 2, PAN_Y0 + 24, eticheta_azi, 15, "bold", culoare_azi)
add(px + PAN_W / 2, PAN_Y0 + 44, subeticheta_azi, 13, "normal", BLACK)
out += arrow(cx, PAN_Y0 + 54, cx, top - 4, culoare_azi, 1.8)
# axa de jos: ce sunt barele
add(px + (PAN_W - slot) / 2 - slot * 0.3, BASE + 20,
"ultimele 10 bare de volum", 13, "normal", GRAY)
add(cx, BASE + 20, "azi", 13, "bold", culoare_azi)
# captura: de ce contează
add(px + PAN_W / 2, BASE + 48, captura[0], 13, "bold", culoare_azi)
add(px + PAN_W / 2, BASE + 68, captura[1], 13, "normal", BLACK)
return out
# =============================================================== CONȚINUT
p = []
add(W / 2, 34, "Relative volume (RVOL): volumul zilei față de media pe 10 bare",
17, "bold")
add(W / 2, 58, "RVOL = volumul de azi ÷ media ultimelor 10 bare de volum",
14, "bold", GRAY)
add(W / 2, 80,
"Măsoară dacă azi chiar participă lumea, nu doar tu. Singurul prag din curs: peste 100 %.",
13, "normal", GRAY)
add(PAN_AX + PAN_W / 2, 110, "Cazul HIG — peste prag", 15, "bold", GREEN)
add(PAN_BX + PAN_W / 2, 110, "Cazul ULTA — sub prag", 15, "bold", RED)
p += panou("hig", PAN_AX, AZI_HIG, GREEN,
"azi = 2 × media → RVOL 200 %",
"cel mai mare din cele 16 rezultate",
("peste 100 % = confirmare",
"se tranzacționează dublu față de normal"))
p += panou("ulta", PAN_BX, AZI_ULTA, RED,
"azi = 0,61 × media → RVOL 0.61",
"61 % dintr-o zi normală",
("sub 100 % = minus, NU veto",
"îl notezi, dar analiza continuă"))
# ---- banda „minus, nu veto"
BAND_Y = 526.0
p.append(rect(40, BAND_Y, 620, 112, "band_veto", fill=GRAY_BG, stroke=GRAY, sw=1.2))
add(60, BAND_Y + 26, "Sub 100 % e un minus, nu un veto", 15, "bold", BLACK, "start")
add(60, BAND_Y + 50,
"Pe scanerul real de Buy Doji, cele trei rezultate aveau RVOL 0.86, 1.03 și 0.61.",
13, "normal", BLACK, "start")
add(60, BAND_Y + 70,
"Un prag dur de genul „RVOL ≥ 1.2” ar fi dat zero rezultate în ziua aia.",
13, "normal", BLACK, "start")
add(60, BAND_Y + 94,
"De aceea RVOL e o coloană pe care o citești, nu un filtru care taie automat.",
13, "bold", BLACK, "start")
# ---- nota: media pe 10, nu pe 50
NOTE_Y = 654.0
p.append(rect(40, NOTE_Y, 620, 90, "note_10", fill=NOTE_BG, stroke=ORANGE, sw=1.4))
add(60, NOTE_Y + 26,
"Media e pe 10 bare, nu pe 50 (implicit în platforme).",
13, "bold", BLACK, "start")
add(60, NOTE_Y + 48,
"Cifra cursului e mai sensibilă la ziua curentă. Dacă vezi altă valoare în",
13, "normal", GRAY, "start")
add(60, NOTE_Y + 68,
"altă platformă, nu e greșeală de calcul, e altă lungime de medie.",
13, "normal", GRAY, "start")
# =============================================================== EMITERE
head = (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" '
f'width="{W}" height="{H}" font-family="system-ui, sans-serif" role="img" '
f'aria-label="Relative volume: doua panouri de volum cu aceeasi medie '
f'pe 10 bare; in stanga ziua de azi e de doua ori media (RVOL 200 la '
f'suta, cazul HIG), in dreapta 0.61 din medie (cazul ULTA); sub 100 la '
f'suta e minus, nu veto">')
body = [head, f'<rect x="0" y="0" width="{W}" height="{H}" fill="#ffffff"/>']
body += p
for x, y, s, size, weight, fill, anchor in TEXTS:
body.append(txt(x, y, s, size, weight, fill, anchor))
body.append("</svg>")
svg = "\n".join(body) + "\n"
# =============================================================== VERIFICARE
# 1. geometria desenată chiar spune ce spun etichetele: raportul dintre
# înălțimea barei „azi" și înălțimea liniei mediei (măsurate în px, de la
# linia de 0) trebuie să fie 2.00, respectiv 0.61.
h_media_px = BASE - (BASE - MEDIA * UNIT)
for tag, rvol_cerut in (("hig", 2.00), ("ulta", 0.61)):
h_azi_px = BARS[tag][-1][2]
assert abs(h_azi_px / h_media_px - rvol_cerut) < 0.005, \
f"{tag}: bara desenată dă RVOL {h_azi_px / h_media_px:.3f}, nu {rvol_cerut}"
# 2. istoricul e chiar de 10 bare și media lui e chiar linia trasată
for tag in ("hig", "ulta"):
istoric = [b[3] for b in BARS[tag][:-1]]
assert len(istoric) == 10, f"{tag}: {len(istoric)} bare de istoric, nu 10"
assert abs(sum(istoric) / 10 - MEDIA) < 1e-9, f"{tag}: media desenată nu e media barelor"
# istoricul e identic în ambele panouri (același simbol de referință vizual)
assert [b[3] for b in BARS["hig"][:-1]] == [b[3] for b in BARS["ulta"][:-1]]
# 3. bara „azi" din HIG e chiar cea mai înaltă, iar cea din ULTA e sub medie
assert BARS["hig"][-1][2] == max(b[2] for b in BARS["hig"]), "bara HIG nu e cea mai înaltă"
assert BARS["ulta"][-1][2] < h_media_px, "bara ULTA nu e sub linia mediei"
# 3c. eticheta de sub bara ULTA afirmă ce se vede în desen: 0,61 din medie =
# 61 % dintr-o zi normală, adică PESTE jumătate. Vechiul text „sub jumătate
# față de o zi normală" contrazicea bara desenată (61 % > 50 %) și pica.
_rvol_ulta = BARS["ulta"][-1][2] / h_media_px
assert round(_rvol_ulta * 100) == 61, \
f"bara ULTA măsoară {_rvol_ulta * 100:.0f} % din medie, eticheta nu poate spune 61 %"
assert _rvol_ulta > 0.5, \
f"bara ULTA e {_rvol_ulta:.2f} din medie, deci PESTE jumătate, nu sub ea"
assert "61 % dintr-o zi normală" in svg, "lipsește din desen procentul ULTA"
assert "jumătate" not in svg, 'a reapărut formularea greșită "sub jumătate"'
# și barele nu ies din cadrul panoului
for tag in ("hig", "ulta"):
for cx, top, h, v in BARS[tag]:
assert top >= PAN_Y0, f"{tag}: bară peste marginea de sus a panoului"
assert abs(top + h - BASE) < 1e-6, f"{tag}: bară care nu pleacă de la linia de 0"
# 3b. eticheta mediei nu cade peste nicio bară (e pusă în stânga, unde barele
# sunt sub linia mediei)
_y_med = BASE - MEDIA * UNIT
for tag in ("hig", "ulta"):
px = PAN_AX if tag == "hig" else PAN_BX
lat = est_w("media = 100 %", 13, "bold")
for cx, top, h, v in BARS[tag]:
if cx - 9 < px + 6 + lat and cx + 9 > px + 6:
assert top > _y_med - 6 + 3, \
f"{tag}: eticheta mediei cade peste o bară (valoare {v})"
# 4. nicio cifră care nu e desenată sau care nu vine din material
PERMISE = {"2", "61", "200", "100", "10", "50", "0.61", "0,61", "0.86", "1.03", "1.2", "16"}
for _x, _y, s, *_ in TEXTS:
for n in re.findall(r"\d+(?:[.,]\d+)?", s):
assert n in PERMISE, f"cifră fără acoperire în material: {n!r} în {s!r}"
# pragul inventat „ideal > 200 %" nu are voie să apară
assert "ideal" not in svg.lower(), "prag inventat de tip ideal peste N la suta"
# 5. textele obligatorii
for req in ("Relative volume (RVOL): volumul zilei față de media pe 10 bare",
"RVOL = volumul de azi ÷ media ultimelor 10 bare de volum",
"Singurul prag din curs: peste 100 %",
"media = 100 %",
"azi = 2 × media → RVOL 200 %",
"azi = 0,61 × media → RVOL 0.61",
"peste 100 % = confirmare",
"sub 100 % = minus, NU veto",
"Sub 100 % e un minus, nu un veto",
"RVOL 0.86, 1.03 și 0.61",
"zero rezultate în ziua aia",
"nu un filtru care taie automat",
"Media e pe 10 bare, nu pe 50"):
assert req in svg, f"lipsește din desen: {req!r}"
# eticheta mediei apare o dată pe fiecare panou
assert svg.count("media = 100 %") == 2, "linia mediei neetichetată pe ambele panouri"
# 6. fiecare etichetă: mărime >= 13, în pânză, fără suprapuneri
boxes = []
for x, y, s, size, weight, fill, anchor in TEXTS:
assert size >= 13, f"font sub 13px: {s!r} ({size})"
w = est_w(s, size, weight)
if anchor == "end":
left = x - w
elif anchor == "start":
left = x
else:
left = x - w / 2.0
right = left + w
top, bottom = y - size * 0.78, y + size * 0.22
assert -2 <= left and right <= W + 2, f"text în afara pânzei orizontal: {s!r}"
assert 0 <= top and bottom <= H, f"text în afara pânzei vertical: {s!r}"
boxes.append((left, right, top, bottom, s))
for i in range(len(boxes)):
for j in range(i + 1, len(boxes)):
l1, r1, t1, b1, s1 = boxes[i]
l2, r2, t2_, b2, s2 = boxes[j]
if r1 > l2 + 2 and r2 > l1 + 2 and b1 > t2_ + 2 and b2 > t1 + 2:
raise AssertionError(f"etichete suprapuse: {s1!r} / {s2!r}")
# 7. stil + XML valid, fără transformări (audit_diagrams dă fals-pozitive pe ele)
assert "filter=" not in svg and "gradient" not in svg, "fără umbre/gradienți"
assert "transform=" not in svg, "fără <g transform>: coordonate absolute"
assert 'font-family="system-ui, sans-serif"' in svg, "lipsește fontul cerut"
assert svg.lstrip().startswith("<svg "), "rădăcina nu e <svg>"
assert ET.fromstring(svg).tag.endswith("svg")
OUT = Path(__file__).with_suffix(".svg")
OUT.write_text(svg, encoding="utf-8")
print(f"scris {OUT} ({len(svg)} bytes, {len(TEXTS)} etichete, "
f"2 panouri x 11 bare, medie {MEDIA:.0f}, "
f"RVOL desenat {BARS['hig'][-1][2] / h_media_px:.2f} / "
f"{BARS['ulta'][-1][2] / h_media_px:.2f}, {W}x{H}), verificare OK")