cele 7 diagrame din 11_selectia
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
This commit is contained in:
195
gen/selectia/cash_flow.py
Normal file
195
gen/selectia/cash_flow.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user