Sunt diagrame conceptuale - funnel, bara de scor, matrice, cele patru cadrane, Trinity, sectoare - nu grafice de pret, deci nu trec prin svg_candles / pchart, care raman obligatorii doar pentru candele si entry/SL/TP. Agent nou, diagramist, cate unul per diagrama, fiecare cu generatorul lui in gen/selectia/; substitutia in .md o face gen/az_selectia.py, ca sa nu scrie sapte agenti in acelasi fisier. Regula de continut a agentului: fiecare cifra si eticheta trebuie sa existe in document sau in citate. Ce nu reiese din sursa e scris pe desen ca atare - ordinea celor 7 sectoare, ce se intampla sub 100% relative volume. Fara semafor rosu/verde, care nu e in sursa. Verificat cu ochii pe doua diagrame randate in browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
196 lines
6.9 KiB
Python
196 lines
6.9 KiB
Python
"""Trei stări de cash flow, din `summaries/08_ANALIZA_ZILNICA/11_selectia_simbolului.md`
|
|
(secțiunea 1.5 Cash flow):
|
|
|
|
- pozitiv și în creștere = se finanțează, stă în picioare -> îl cauți
|
|
- pozitiv, dar în scădere = deteriorare -> nu-l pui pe lista de investiție
|
|
- negativ = speculă pe momentum, nu investiție
|
|
|
|
La ETF-uri, ETF-urile nu au scor, dar nu intră în această diagramă.
|
|
|
|
SVG scris de mână (nu e grafic de preț, deci NU se folosesc svg_candles / pchart).
|
|
Fără dependințe: doar stdlib. Rulare:
|
|
.venv/bin/python gen/selectia/cash_flow.py
|
|
Scrie `gen/selectia/cash_flow.svg`.
|
|
|
|
Notă de conținut: textul nu dă praguri, procente sau cod de culoare
|
|
(verde/roșu ca semafor), deci diagrama nu inventează niciunul. Toate cifrele
|
|
care apar în desen sunt referințe de citat (fișier:linii) existente în text.
|
|
"""
|
|
from pathlib import Path
|
|
from xml.sax.saxutils import escape
|
|
|
|
OUT = Path(__file__).resolve().parent / "cash_flow.svg"
|
|
|
|
BLACK = "#000000"
|
|
GRAY = "#777777"
|
|
BLUE = "#1a5fb4"
|
|
|
|
W = 700
|
|
MARGIN = 30
|
|
GAP = 20
|
|
CARD_W = 200
|
|
CARD_TOP = 80
|
|
CARD_H = 216
|
|
XS = [MARGIN + i * (CARD_W + GAP) for i in range(3)] # 30, 250, 470
|
|
|
|
TITLE_Y = 34
|
|
SUB_Y = 56
|
|
|
|
# Fiecare card: header (poate fi pe două rânduri), "ce înseamnă" (2 rânduri),
|
|
# acțiunea (1-2 rânduri, în caseta de jos) și sursa.
|
|
CARDS = [
|
|
{
|
|
"header": ["Pozitiv", "și în creștere"],
|
|
"meaning": ["se finanțează,", "stă în picioare"],
|
|
"action": ["Îl cauți chiar și la", "companiile pe", "pierdere"],
|
|
"source": "66_18052026.txt:120-121",
|
|
},
|
|
{
|
|
"header": ["Pozitiv, dar", "în scădere"],
|
|
"meaning": ["deși e pozitiv,", "s-a deteriorat"],
|
|
"action": ["Nu-l pui pe lista", "de investiție"],
|
|
"source": "72_08052026.txt:241-243",
|
|
},
|
|
{
|
|
"header": ["Negativ"],
|
|
"meaning": ["speculă pe momentum"],
|
|
"action": ["Nu investiție"],
|
|
"source": "„Ce faci”, secțiunea 1.5",
|
|
},
|
|
]
|
|
|
|
FOOTNOTES = [
|
|
("Cash flow-ul pozitiv e ce ține compania în picioare — îl cauți chiar și la",
|
|
BLACK),
|
|
("companiile încă pe pierdere.", BLACK),
|
|
("Pozitiv dar în scădere = deteriorare (53_09062026.txt:122-124).", GRAY),
|
|
("Surse: 66_18052026.txt:120-121; 72_08052026.txt:241-243.", GRAY),
|
|
("Secțiunea 1.5 Cash flow — summaries/08_ANALIZA_ZILNICA/11_selectia_simbolului.md.",
|
|
GRAY),
|
|
]
|
|
|
|
FOOT_TOP = CARD_TOP + CARD_H + 30
|
|
H = FOOT_TOP + (len(FOOTNOTES) - 1) * 20 + 30
|
|
|
|
labels = [] # (x_left, x_right, y_top, y_bottom, text) pentru verificare
|
|
|
|
|
|
def est_width(text, size, bold=False):
|
|
"""Lățime aproximativă pentru system-ui, pentru verificarea suprapunerilor."""
|
|
factor = 0.62 if bold else 0.56
|
|
return len(text) * size * factor
|
|
|
|
|
|
def txt(x, y, s, size=13, bold=False, fill=BLACK, anchor="middle"):
|
|
weight = "700" if bold else "400"
|
|
w = est_width(s, size, bold)
|
|
if anchor == "middle":
|
|
x0, x1 = x - w / 2, x + w / 2
|
|
elif anchor == "start":
|
|
x0, x1 = x, x + w
|
|
else: # end
|
|
x0, x1 = x - w, x
|
|
labels.append((x0, x1, y - size, y + 3, s))
|
|
return (
|
|
f'<text x="{x:g}" y="{y}" font-family="system-ui, sans-serif" '
|
|
f'font-size="{size}" font-weight="{weight}" fill="{fill}" '
|
|
f'text-anchor="{anchor}">{escape(s)}</text>'
|
|
)
|
|
|
|
|
|
def card(idx, spec):
|
|
x = XS[idx]
|
|
parts = [
|
|
f'<rect x="{x}" y="{CARD_TOP}" width="{CARD_W}" height="{CARD_H}" '
|
|
f'rx="10" fill="#ffffff" stroke="{BLUE}" stroke-width="1.5"/>',
|
|
]
|
|
# header
|
|
hy = CARD_TOP + 30
|
|
for line in spec["header"]:
|
|
parts.append(txt(x + CARD_W / 2, hy, line, size=16, bold=True))
|
|
hy += 21
|
|
sep_y = CARD_TOP + 64
|
|
parts.append(
|
|
f'<line x1="{x + 14}" y1="{sep_y}" x2="{x + CARD_W - 14}" y2="{sep_y}" '
|
|
f'stroke="{BLUE}" stroke-width="1"/>'
|
|
)
|
|
# ce înseamnă
|
|
parts.append(txt(x + 14, CARD_TOP + 88, "Ce înseamnă", size=13, fill=GRAY,
|
|
anchor="start"))
|
|
my = CARD_TOP + 108
|
|
for line in spec["meaning"]:
|
|
parts.append(txt(x + 14, my, line, size=13, anchor="start"))
|
|
my += 19
|
|
# caseta de acțiune
|
|
box_y = CARD_TOP + CARD_H - 74
|
|
parts.append(
|
|
f'<rect x="{x + 14}" y="{box_y}" width="{CARD_W - 28}" height="58" '
|
|
f'rx="6" fill="#ffffff" stroke="{BLUE}" stroke-width="1"/>'
|
|
)
|
|
ay = box_y + 19
|
|
for line in spec["action"]:
|
|
parts.append(txt(x + CARD_W / 2, ay, line, size=13, bold=True))
|
|
ay += 17
|
|
# sursa
|
|
parts.append(txt(x + CARD_W / 2, CARD_TOP + CARD_H + 15, spec["source"],
|
|
size=13, fill=GRAY))
|
|
return parts
|
|
|
|
|
|
svg = [
|
|
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" '
|
|
f'width="{W}" height="{H}" role="img" '
|
|
f'aria-label="Trei stări de cash flow: pozitiv în creștere, pozitiv dar '
|
|
f'în scădere, negativ">',
|
|
f'<rect x="0" y="0" width="{W}" height="{H}" fill="#ffffff"/>',
|
|
txt(W / 2, TITLE_Y, "Cash flow — trei stări", size=19, bold=True),
|
|
txt(W / 2, SUB_Y, "un cash flow pozitiv care scade e deteriorare, "
|
|
"nu semnal de cumpărare", size=13, fill=GRAY),
|
|
]
|
|
|
|
for i, spec in enumerate(CARDS):
|
|
svg.extend(card(i, spec))
|
|
|
|
fy = FOOT_TOP + 10
|
|
for line, color in FOOTNOTES:
|
|
svg.append(txt(W / 2, fy, line, size=13, fill=color))
|
|
fy += 20
|
|
|
|
svg.append("</svg>")
|
|
|
|
# ------------------------------------------------------------------ verificare
|
|
# 1. fiecare text încape în card / în panză
|
|
for i, spec in enumerate(CARDS):
|
|
inner = CARD_W - 28
|
|
for line in spec["header"]:
|
|
assert est_width(line, 16, bold=True) <= inner, \
|
|
f"header nu incape in card {i}: {line!r}"
|
|
for line in spec["meaning"] + spec["action"]:
|
|
assert est_width(line, 13, line in spec["action"]) <= inner, \
|
|
f"text nu incape in card {i}: {line!r}"
|
|
|
|
# 2. toate etichetele au coordonate în interiorul panzei
|
|
for x0, x1, y0, y1, s in labels:
|
|
assert 0 <= x0 and x1 <= W, f"eticheta iese din panza pe orizontala: {s!r}"
|
|
assert 0 <= y0 and y1 <= H, f"eticheta iese din panza pe verticala: {s!r}"
|
|
|
|
# 3. pe aceeași poziție / suprapuse
|
|
for i in range(len(labels)):
|
|
for j in range(i + 1, len(labels)):
|
|
ax0, ax1, ay0, ay1, a = labels[i]
|
|
bx0, bx1, by0, by1, b = labels[j]
|
|
inter_w = min(ax1, bx1) - max(ax0, bx0)
|
|
inter_h = min(ay1, by1) - max(ay0, by0)
|
|
if inter_w > 1.5 and inter_h > 1.5:
|
|
raise AssertionError(f"texte suprapuse: {a!r} / {b!r}")
|
|
|
|
# 4. nu se calcă pe verticală doua rânduri de text (baseline prea apropiat)
|
|
ys = sorted((round((y0 + y1) / 2), s) for _, _, y0, y1, s in labels)
|
|
for k in range(len(ys) - 1):
|
|
assert ys[k + 1][0] - ys[k][0] >= 14 or ys[k][0] == ys[k + 1][0], \
|
|
f"rânduri prea apropiate: {ys[k][1]!r} / {ys[k + 1][1]!r}"
|
|
|
|
OUT.write_text("\n".join(svg) + "\n", encoding="utf-8")
|
|
print(f"scris {OUT} ({W}x{H}), {len(CARDS)} stări, "
|
|
f"{len(labels)} etichete, verificare OK")
|