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
187 lines
8.9 KiB
Python
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")
|