buy_doji: refacut ca tutorial pe 3 niveluri, cu diagrame adnotate

Userul a respins mostra de format tutorial: prea lunga, prea multe
trimiteri in text, termeni tehnici nedefiniti, prea multe cifre fara
desen. Cerintele lui sunt acum contract in STYLE.md (FORMAT TUTORIAL v2).

Textul: 8430 -> 4190 de cuvinte. Nivel 1 (din avion, cu mini-glosar) ->
nivel 2 (6 pasi mecanici, o diagrama fiecare) -> oglinda short -> nivel 3
(rafinari). Trimiterile fisier:linie ies din corp si se aduna la final,
cu legenda de prescurtari. Doua tabele in tot documentul.

Toate cele 14 diagrame refacute: zone dreptunghiulare de evidentiere si
sageti care spun si de ce, nu doar ce. svg_candles primeste Zone si
Arrow; render() nu mai muta obiectul la iesire.

Trei corecturi de continut:
- un singur trade prin toata procedura (doji 50.10/49.25, intrare 50.40,
  stop 49.20, 1R 1.20, TP1 51.60); seria e acum o functie comuna,
  importata de stop/transe/iesire
- short: stopul urca la 100.15, ca sa fie deasupra maximului cum cere
  regula oglindita; cifra cursului ramane in text ca observatie
- LABU: trei minime dar doua atingeri ale pivotului 86.00, ceea ce
  impaca "triple bottom" cu "al doilea test", ambele din transcriere

Reparat si bug-ul vechi din buy_doji_sell.svg: titlul peste eticheta
dojiului.

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-19 21:09:31 +00:00
parent aff03870e8
commit 8b7b5f6b44
50 changed files with 7011 additions and 6921 deletions

View File

@@ -23,7 +23,7 @@ import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Candle, Chart, Level, Mark, MA, from_closes # noqa: E402
from svg_candles import Arrow, Candle, Chart, Level, Zone, from_closes # noqa: E402
OUT = Path(__file__).resolve().parent / "buy_doji_aplus.svg"
@@ -118,19 +118,25 @@ PW = 560 # lățimea fiecărui panou
ch_a = Chart(
candles=cds_a,
levels=[Level(42.00, "suport 42.00", GREEN, label_at="left")],
marks=[Mark(DOJI_A, "doji la nivel (low 41.95)", ORANGE, "below"),
Mark(CONF_A, "acumulare", GREEN, "above", dy=-6)],
zones=[Zone(DOJI_A, DOJI_A, "", GREEN, p_top=43.2, p_bot=41.7)],
arrows=[Arrow(DOJI_A, "doji chiar pe nivel:\nvânzarea s-a oprit pe suport",
where="below", color=GREEN, dx=-40),
Arrow(CONF_A, "confirmarea, pe volum mare:\ncumpărătorii mari intră",
where="above", color=GREEN, dx=60)],
mas=[], scale=22.0, gap=15, body_w=9, left=44, width=PW, vol_h=60,
vol_label="volum", title="A+ — doji la suport, volum în acumulare",
caption="acumulare: volum peste media ultimelor 10 bare.",
)
ch_n = Chart(
candles=cds_n,
levels=[Level(78.50, "rezistență 78.50", RED, label_at="right")],
marks=[Mark(DOJI_N, "doji 78.50 / 78.10", ORANGE, "below", dy=93)],
# eticheta zonei ar urca peste titlul panoului (zona e chiar în vârf):
# textul stă pe săgeata din stânga
zones=[Zone(DOJI_N - 1, CONF_N, "", RED, p_top=79.62)],
arrows=[Arrow(DOJI_N, "gap-up în rezistență:\naici cumpără cei întârziați",
where="left", color=RED, dx=-30, dy=30),
Arrow(18, "cade înapoi sub 78.00:\nsemnalul a mințit", where="below", color=RED, dx=40)],
mas=[], scale=22.0, gap=15, body_w=9, left=44, width=PW, vol_h=60,
vol_label="volum", title="Mediocru — gap-up în rezistență, volum varză",
caption="gap-up + doji în rezistență, confirmare pe volum slab.",
)
# ============================================================ wrapper: două panouri alăturate
@@ -145,20 +151,51 @@ def vb_height(svg: str) -> float:
return float(m.group(1))
PANEL_GAP, PAD = 24, 8
TITLE_H, FOOTER_H = 30, 22
svg_a, svg_n = ch_a.render(), ch_n.render()
def vol_note(svg: str, i0: int, i1: int, text: str, color: str, box: bool) -> str:
"""Evidențiere în panoul de volum (Zone/Arrow din svg_candles lucrează doar pe preț).
# „gap-up, volum slab" stă în colțul liber din stânga-sus, cu linie indicatoare
# spre golul dintre bara 13 (high 76.80) și dojiul barei 14 (open 78.20).
GAP_X = (ch_n._cx(DOJI_N - 1) + ch_n._cx(DOJI_N)) / 2
GAP_Y = ch_n._y(77.50)
gap_ann = (
f'<text x="28" y="120" fill="{RED}" style="font-weight:bold">gap-up, volum slab</text>'
f'<line x1="152" y1="116" x2="{GAP_X:.1f}" y2="{GAP_Y:.1f}" stroke="{RED}" stroke-width="1"/>'
f'<circle cx="{GAP_X:.1f}" cy="{GAP_Y:.1f}" r="2.5" fill="{RED}"/>'
)
svg_n = svg_n.replace("</svg>", gap_ann + "</svg>")
Coordonatele se citesc din SVG-ul randat (barele de volum sunt rect-urile cu
opacity 0.55, în ordinea candelelor), ca să nu depindă de deplasarea internă
a lui Chart. Eticheta stă sub linia de bază, unde nu acoperă nimic;
`box` = dreptunghi peste bare, altfel săgeată de jos în sus, sub bară.
"""
bars = [tuple(map(float, m)) for m in re.findall(
r'<rect x="([\d.]+)" y="([\d.]+)" width="([\d.]+)" height="([\d.]+)" '
r'fill="#[0-9a-f]+" opacity="0.55"/>', svg)]
base = bars[0][1] + bars[0][3]
x0, x1 = bars[i0][0] - 4, bars[i1][0] + bars[i1][2] + 4
top = min(b[1] for b in bars[i0:i1 + 1]) - 4
if box:
mark = (f'<rect x="{x0:.1f}" y="{top:.1f}" width="{x1 - x0:.1f}" height="{base - top:.1f}" '
f'fill="{color}" fill-opacity="0.12" stroke="{color}" stroke-width="1"/>')
ty = base + 15
else:
cx = (x0 + x1) / 2
mark = (f'<line x1="{cx:.1f}" y1="{base + 22:.1f}" x2="{cx:.1f}" y2="{base + 9:.1f}" '
f'stroke="{color}" stroke-width="1.8"/>'
f'<polygon points="{cx:.1f},{base + 2:.1f} {cx - 4:.1f},{base + 10:.1f} '
f'{cx + 4:.1f},{base + 10:.1f}" fill="{color}"/>')
ty = base + 34
label = (f'<text x="{x0:.1f}" y="{ty:.1f}" fill="{color}" '
f'style="font-weight:bold">{text}</text>')
# dreptunghiul/săgeata în spatele barelor, eticheta la final
i = svg.index('<rect', svg.index(f'y1="{base:.1f}"'))
svg = svg[:i] + mark + svg[i:]
return svg.replace("</svg>", label + "</svg>")
def grow(svg: str, dh: float) -> str:
"""Mărește viewBox-ul unui panou cu dh px jos, pentru eticheta de sub volum."""
h = vb_height(svg)
return svg.replace(f'0 0 {PW} {h:.0f}"', f'0 0 {PW} {h + dh:.0f}"', 1)
PANEL_GAP, PAD = 24, 8
TITLE_H, FOOTER_H = 30, 0
svg_a = grow(vol_note(ch_a.render(), CONF_A, CONF_A + 4,
"volum în creștere = acumulare", GREEN, box=True), 10)
svg_n = grow(vol_note(ch_n.render(), CONF_N, CONF_N,
"volum slab: nimeni mare nu cumpără", RED, box=False), 30)
HA, HN = vb_height(svg_a), vb_height(svg_n)
assert svg_a.count("\n\n") == 0 and svg_n.count("\n\n") == 0, "linie goală în panou"
@@ -168,9 +205,6 @@ TOTAL_H = PAD + TITLE_H + max(HA, HN) + FOOTER_H + PAD
OFF_A = (PAD, PAD + TITLE_H)
OFF_N = (PAD + PW + PANEL_GAP, PAD + TITLE_H)
FOOTER = ("Surse: 13_strategia_3_byh.md:188-190 (doji la nivel); "
"01_buy_doji_si_strategia_mici.md:227-235 (context invalid).")
wrapper = (
f'<svg viewBox="0 0 {TOTAL_W} {TOTAL_H:.0f}" width="{TOTAL_W}" height="{TOTAL_H:.0f}" '
f'xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;font:12px sans-serif">'
@@ -178,8 +212,6 @@ wrapper = (
f'style="font-weight:bold">Setup A+ vs. setup mediocru (două panouri, exemple construite)</text>'
f'<g transform="translate({OFF_A[0]},{OFF_A[1]})">{inner(svg_a)}</g>'
f'<g transform="translate({OFF_N[0]},{OFF_N[1]})">{inner(svg_n)}</g>'
f'<text x="{TOTAL_W / 2:.0f}" y="{TOTAL_H - PAD:.0f}" fill="#555" '
f'text-anchor="middle">{FOOTER}</text>'
f'</svg>'
)
@@ -217,8 +249,7 @@ for frag, (ox, oy) in ((inner(svg_a), OFF_A), (inner(svg_n), OFF_N)):
all_txt.append((gx, gy, anchor, t))
# textele din wrapper (titlu + footer), în coordonate globale
for x, y, anchor, t in ((TOTAL_W / 2, 20.0, "middle",
"Setup A+ vs. setup mediocru (două panouri, exemple construite)"),
(TOTAL_W / 2, TOTAL_H - PAD, "middle", FOOTER)):
"Setup A+ vs. setup mediocru (două panouri, exemple construite)"),):
gl, gr = span(x, anchor, t)
assert -1 <= gl and gr <= TOTAL_W + 1, f"text din wrapper iese din pânză: {t!r}"
assert 0 <= y <= TOTAL_H, f"text din wrapper în afara înălțimii: {t!r}"
@@ -236,8 +267,10 @@ for i in range(len(all_txt)):
raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
# etichetele cerute de spec există în desen
for cheie in ("suport 42.00", "doji la nivel", "acumulare",
"rezistență 78.50", "gap-up, volum slab",
assert "Surse" not in wrapper and ".md:" not in wrapper, "trimiteri la surse în SVG"
for cheie in ("suport 42.00", "doji chiar pe nivel", "vânzarea s-a oprit", "volum în creștere = acumulare",
"rezistență 78.50", "gap-up în rezistență", "cei întârziați",
"semnalul a mințit", "volum slab: nimeni mare nu cumpără",
"A+ — doji la suport, volum în acumulare",
"Mediocru — gap-up în rezistență, volum varză"):
assert cheie in wrapper, f"lipsește din desen: {cheie!r}"