Eticheta spunea "fara sa rupa low-ul doji-ului weekly", dar nivelul nu se vedea nicaieri — scara panoului mic nu ajunge pana la el. Extinderea scarii ar fi facut ghidajele de lupa false si ar fi taiat magnificarea la jumatate, asa ca referinta se marcheaza pe panoul MARE, cu o bifa pana la low-ul doji-ului, si eticheta trimite acolo. Nivelul traieste pe TF-ul mare, declansatorul pe cel mic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
485 lines
25 KiB
Python
485 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
"""Buy Doji — aceeași regulă pe trei perechi de timeframe-uri, cu lupă.
|
|
|
|
Citește `gen/strategii/buy_doji_perechi_tf.spec.txt` și scrie
|
|
`gen/strategii/buy_doji_perechi_tf.svg`. Trei rânduri (weekly→daily,
|
|
monthly→weekly, daily→60 min). Pe fiecare rând: stânga = TF mare, dreapta =
|
|
TF mic. Graficul mic NU e o serie care vine după cel mare — e
|
|
**descompunerea reală** a ultimelor candele mari: fiecare candelă mare din
|
|
caseta de lupă se sparge în `k` candele mici, cu open/close/high/low
|
|
agregate exact.
|
|
|
|
Sursa de adevăr e seria FINĂ (`FINE`). Candelele mari se CALCULEAZĂ din ea
|
|
prin agregare pe k bare; barele mari dinaintea lupei (`BASE`) sunt date
|
|
separat și se leagă de lupă prin close == open.
|
|
|
|
rând TF mare → TF mic k candele mari mărite bare mici
|
|
A weekly → daily 5 3 15
|
|
B monthly → weekly 4 3 12
|
|
C daily → 60 min 7 2 14
|
|
|
|
Rândurile B și C se obțin din A prin p' = round(100 + f*(p - 100), 2), cu
|
|
f = 2.5 și 0.4. Transformarea e afină, deci agregarea se păstrează; gruparea
|
|
(k) diferă însă per rând, deci barele mari se recalculează per rând.
|
|
|
|
Toate cifrele din desen ies din serii; niciuna nu e scrisă de mână.
|
|
Coordonatele sunt absolute (fără <g transform>), ca audit_diagrams.py să
|
|
vadă pozițiile reale ale textelor.
|
|
|
|
Rulare: python3 gen/strategii/buy_doji_perechi_tf.py
|
|
"""
|
|
from pathlib import Path
|
|
|
|
OUT = Path(__file__).with_suffix(".svg")
|
|
W = 720
|
|
MARJA = 0.05 # 5 cenți sub cel mai jos low (regula capitolului, Pasul 5)
|
|
|
|
# --- seria fină (TF mic), rândul A: (open, high, low, close) ---------------
|
|
# 0..7 pullback de 8 candele, în jos, spre nivelul din TF-ul mare
|
|
# 8 doji la nivel 9 confirmare => intrare 10..14 drumul spre țintă
|
|
FINE_A = [(107.0, 107.2, 106.4, 106.6), (106.6, 106.8, 106.0, 106.1),
|
|
(106.1, 106.6, 105.95, 106.4), (106.4, 106.5, 105.8, 105.9),
|
|
(105.9, 106.0, 105.5, 105.6), (105.6, 105.95, 105.5, 105.8),
|
|
(105.8, 105.9, 105.4, 105.5), (105.5, 105.6, 105.25, 105.35),
|
|
(105.35, 105.75, 105.10, 105.40), # 8: doji la nivel
|
|
(105.40, 106.40, 105.30, 106.30), # 9: confirmare => intrare
|
|
(106.3, 107.0, 106.2, 106.9), (106.9, 107.7, 106.7, 107.4),
|
|
(107.4, 107.55, 107.0, 107.2), (107.2, 108.0, 107.1, 107.9),
|
|
(107.9, 108.6, 107.8, 108.5)]
|
|
|
|
# --- barele TF-ului mare dinaintea casetei de lupă, rândul A ---------------
|
|
# 6 = doji mare, 7 = confirmare mare (close 107.0 = open-ul primei bare fine)
|
|
BASE_A = [(118.4, 118.8, 114.3, 114.8), (114.8, 115.3, 111.5, 112.0),
|
|
(112.0, 113.4, 111.6, 112.9), (112.9, 113.2, 109.1, 109.6),
|
|
(109.6, 110.0, 106.3, 106.8), (106.8, 107.2, 103.6, 104.0),
|
|
(104.0, 105.2, 101.9, 103.8), # 6: doji mare
|
|
(103.8, 107.4, 103.5, 107.0)] # 7: confirmare mare
|
|
DOJI_B, CONF_B = 6, 7
|
|
PULLBACK_N = 8 # câte bare mici are pullback-ul (0..7)
|
|
FINE_DOJI, FINE_CONF = 8, 9
|
|
STOP_BARS = 4 # stopul se măsoară sub ultimele 4 bare (doji + 3 dinainte)
|
|
|
|
|
|
def scaled(bars, f):
|
|
return [tuple(round(100 + f * (p - 100), 2) for p in b) for b in bars]
|
|
|
|
|
|
def aggregate(fine, k, n):
|
|
"""n candele mari, fiecare din k bare fine consecutive."""
|
|
out_ = []
|
|
for j in range(n):
|
|
part = fine[j * k:(j + 1) * k]
|
|
out_.append((part[0][0], max(b[1] for b in part),
|
|
min(b[2] for b in part), part[-1][3]))
|
|
return out_
|
|
|
|
|
|
# key, f, TF mare, TF mic, k, nr. candele mari mărite, valori din spec
|
|
ROWS = [
|
|
("A", 1.0, "weekly", "daily", 5, 3,
|
|
dict(dh=105.20, dl=101.90, conf=107.00, entry=106.30, sl=105.05, risk=1.25,
|
|
pct=1.18, tp=107.55, b_entry=107.00, b_sl=101.85, b_risk=5.15, b_pct=4.81,
|
|
zhi=108.60, zlo=105.10, first=11, span=2, doji_big=1)),
|
|
("B", 2.5, "monthly", "weekly", 4, 3,
|
|
dict(dh=113.00, dl=104.75, conf=117.50, entry=115.75, sl=112.70, risk=3.05,
|
|
pct=2.63, tp=118.80, b_entry=117.50, b_sl=104.70, b_risk=12.80, b_pct=10.89,
|
|
zhi=119.25, zlo=112.75, first=11, span=2, doji_big=2)),
|
|
("C", 0.4, "daily", "60 min", 7, 2,
|
|
dict(dh=102.08, dl=100.76, conf=102.80, entry=102.52, sl=101.99, risk=0.53,
|
|
pct=0.52, tp=103.05, b_entry=102.80, b_sl=100.71, b_risk=2.09, b_pct=2.03,
|
|
zhi=103.20, zlo=102.04, first=11, span=2, doji_big=1)),
|
|
]
|
|
|
|
|
|
def eq(a, b):
|
|
return abs(a - b) < 0.005
|
|
|
|
|
|
def check(key, base, zoom, fine, k, nz, exp):
|
|
"""Assert-urile obligatorii din spec; întoarce nivelurile folosite în desen."""
|
|
big = base + zoom
|
|
for name, bars in (("mare", big), ("mic", fine)):
|
|
for i, (o, h, l, c) in enumerate(bars):
|
|
assert h >= max(o, c) and l <= min(o, c), f"{key} {name}[{i}] h/l incoerent"
|
|
if i:
|
|
assert eq(o, bars[i - 1][3]), f"{key} {name}[{i}] open != close precedent"
|
|
assert len(fine) == k * nz, f"{key}: {len(fine)} bare fine != {k}*{nz}"
|
|
assert len(big) == len(BASE_A) + nz
|
|
|
|
# (1) UNDE CAD GRANIȚELE candelelor mari peste poveste. Nu reface calculul
|
|
# agregării (ar fi tautologic: zoom vine chiar din aggregate) — afirmă ce
|
|
# spune textul capitolului și poate fi fals: peste câte candele mari se
|
|
# întinde pullback-ul și în a câta candelă mare cade doji-ul mic.
|
|
span = len({i // k for i in range(PULLBACK_N)})
|
|
assert span == exp["span"], f"{key}: pullback-ul se întinde peste {span} candele mari"
|
|
assert span >= 2, f"{key}: pullback-ul ar încăpea într-o singură candelă mare"
|
|
doji_big = FINE_DOJI // k
|
|
assert doji_big == exp["doji_big"], f"{key}: doji-ul mic cade în candela mare {doji_big}"
|
|
assert doji_big < nz, f"{key}: doji-ul mic cade în afara zonei mărite"
|
|
# lupa se leagă de restul graficului mare: confirmarea mare dă open-ul lupei
|
|
assert eq(fine[0][0], base[CONF_B][3]), f"{key}: open fine[0] != close confirmare mare"
|
|
assert eq(zoom[doji_big][2], fine[FINE_DOJI][2]), \
|
|
f"{key}: low-ul candelei mari cu doji-ul nu e low-ul doji-ului mic"
|
|
|
|
# (2) setup-ul pe TF mare: doji + confirmare
|
|
for name, (o, h, l, c) in (("doji mare", base[DOJI_B]), ("doji mic", fine[FINE_DOJI])):
|
|
assert abs(o - c) <= 0.10 * (h - l) + 1e-9, f"{key}: {name} nu e doji"
|
|
assert base[CONF_B][3] > base[DOJI_B][1], f"{key}: fără confirmare pe TF mare"
|
|
assert fine[FINE_CONF][3] > fine[FINE_DOJI][1], f"{key}: fără confirmare pe TF mic"
|
|
|
|
# (3) trade-ul pe TF mic: stopul e cu MARJA sub minimul ultimelor 4 bare
|
|
low4 = min(b[2] for b in fine[FINE_CONF - STOP_BARS + 1:FINE_CONF + 1])
|
|
assert eq(low4, fine[FINE_DOJI][2]), f"{key}: minimul celor 4 bare nu e low-ul dojiului"
|
|
assert eq(low4, min(b[2] for b in fine)), f"{key}: low-ul dojiului nu e minimul zonei mărite"
|
|
sl = round(low4 - MARJA, 2)
|
|
entry = fine[FINE_CONF][3]
|
|
risk = round(entry - sl, 2)
|
|
tp = round(entry + risk, 2)
|
|
first = next(i for i in range(FINE_CONF + 1, len(fine)) if fine[i][1] >= tp - 1e-9)
|
|
assert first == exp["first"], f"{key}: TP1 atins la {first}, nu la {exp['first']}"
|
|
pct = risk / entry * 100
|
|
|
|
# (4) pullback = revenire scurtă care coboară spre nivel, dar NU rupe
|
|
# low-ul doji-ului de pe TF-ul mare (referința mișcării)
|
|
pull = fine[:PULLBACK_N]
|
|
assert pull[0][3] > pull[-1][3], f"{key}: pullback-ul nu coboară"
|
|
assert min(b[2] for b in pull) > base[DOJI_B][2], f"{key}: pullback-ul rupe low-ul doji mare"
|
|
|
|
b_entry = base[CONF_B][3]
|
|
b_sl = round(base[DOJI_B][2] - MARJA, 2)
|
|
b_risk = round(b_entry - b_sl, 2)
|
|
b_pct = b_risk / b_entry * 100
|
|
got = dict(dh=base[DOJI_B][1], dl=base[DOJI_B][2], conf=base[CONF_B][3], entry=entry,
|
|
sl=sl, risk=risk, pct=pct, tp=tp, b_entry=b_entry, b_sl=b_sl,
|
|
b_risk=b_risk, b_pct=b_pct, first=first, span=span, doji_big=doji_big,
|
|
zhi=max(b[1] for b in fine), zlo=min(b[2] for b in fine))
|
|
for kk, v in exp.items():
|
|
assert eq(round(got[kk], 2), v), f"{key}: {kk} = {got[kk]:.4f}, spec {v}"
|
|
# (5) zona mărită de pe graficul mare are EXACT extremele graficului mic
|
|
assert eq(got["zhi"], max(b[1] for b in zoom)) and eq(got["zlo"], min(b[2] for b in zoom))
|
|
# (6) doji-ul mic chiar atinge nivelul din TF-ul mare (sub pragul strict de 0,5 %)
|
|
assert abs(got["zlo"] - got["dh"]) / got["dh"] <= 0.005, f"{key}: doji mic nu e la nivel"
|
|
assert 3.9 <= b_pct / pct <= 4.5, f"{key}: raport stopuri {b_pct / pct:.2f} nu e ~4x"
|
|
# (7) nivelul legat între panouri (close-ul confirmării mari = open fine[0])
|
|
# cade în mijlocul zonei mărite, nu lipit de o extremă
|
|
t = (got["conf"] - sl) / (got["zhi"] - sl)
|
|
assert 0.2 <= t <= 0.8, f"{key}: nivelul comun e la {t:.2f} din interval, prea la margine"
|
|
return got
|
|
|
|
|
|
# --- desen ------------------------------------------------------------------
|
|
GREEN, RED, GRAY, INK, BLUE = "#188a3e", "#c00", "#888", "#333", "#1a5fb4"
|
|
LINK = "#aaa" # nivelul comun legat între panouri
|
|
PLOT_H = 160
|
|
BX0, BGAP = 105, 15 # TF mare
|
|
SX0, SGAP = 372, 15 # TF mic
|
|
BODY_W = 9
|
|
FRAME_L = SX0 - 9 # marginea stângă a cadrului graficului mic
|
|
COL_X = 604 # coloana de etichete a graficului mic
|
|
MIDX = FRAME_L - 20 # granița stânga/dreapta pentru verificarea de panou
|
|
PAD = 0.06 # aer sus/jos pe graficul mic (nu lipi liniile de cadru)
|
|
|
|
out = []
|
|
LABELS = [] # (x0, x1, y, text) al fiecărei etichete, pentru verificări
|
|
|
|
|
|
def text(x, y, s, fill=INK, anchor="start", bold=False, size=12):
|
|
st = [f"font-size:{size}px"]
|
|
if bold:
|
|
st.append("font-weight:bold")
|
|
a = f' text-anchor="{anchor}"' if anchor != "start" else ""
|
|
w = len(s) * 5.7 if size >= 12 else len(s) * size * 0.5
|
|
x0 = x if anchor == "start" else (x - w / 2 if anchor == "middle" else x - w)
|
|
assert 4 <= x0 and x0 + w <= W - 4, f"text iese din viewBox: {s!r} ({x0:.0f}..{x0 + w:.0f})"
|
|
LABELS.append((x0, x0 + w, y, s))
|
|
out.append(f'<text x="{x:.1f}" y="{y:.1f}" fill="{fill}"{a} '
|
|
f'style="{";".join(st)}">{s}</text>')
|
|
|
|
|
|
def hline(x1, x2, y, color, dash, width=1.5):
|
|
out.append(f'<line x1="{x1:.1f}" y1="{y:.1f}" x2="{x2:.1f}" y2="{y:.1f}" '
|
|
f'stroke="{color}" stroke-width="{width}" stroke-dasharray="{dash}"/>')
|
|
|
|
|
|
def line(x1, y1, x2, y2, color, dash="", width=1.2, marker=""):
|
|
d = f' stroke-dasharray="{dash}"' if dash else ""
|
|
m = f' marker-end="url(#{marker})"' if marker else ""
|
|
out.append(f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
|
|
f'stroke="{color}" stroke-width="{width}"{d}{m}/>')
|
|
|
|
|
|
def rect(x0_, y0_, x1, y1, color, fill_op=0.10, dash=""):
|
|
d = f' stroke-dasharray="{dash}"' if dash else ""
|
|
out.append(f'<rect x="{x0_:.1f}" y="{y0_:.1f}" width="{x1 - x0_:.1f}" '
|
|
f'height="{y1 - y0_:.1f}" fill="{color}" fill-opacity="{fill_op}" '
|
|
f'stroke="{color}" stroke-width="1.2"{d}/>')
|
|
|
|
|
|
def candles(bars, x0, gap, yf):
|
|
for i, (o, h, l, c) in enumerate(bars):
|
|
cx = x0 + i * gap
|
|
color = GREEN if c >= o else RED
|
|
line(cx, yf(h), cx, yf(l), "#1a1a1a", width=1.2)
|
|
top_ = yf(max(o, c))
|
|
hh = max(yf(min(o, c)) - top_, 1.6)
|
|
out.append(f'<rect x="{cx - BODY_W / 2:.1f}" y="{top_:.1f}" width="{BODY_W}" '
|
|
f'height="{hh:.1f}" fill="{color}"/>')
|
|
|
|
|
|
def bracket(x, ya, yb_, color, label, side="right", below=False):
|
|
"""Paranteză verticală între două prețuri, cu eticheta în lateral sau dedesubt."""
|
|
tip = x + 5 if side == "right" else x - 5
|
|
out.append(f'<path d="M{tip:.1f},{ya:.1f} H{x:.1f} V{yb_:.1f} H{tip:.1f}" '
|
|
f'fill="none" stroke="{color}" stroke-width="1.6"/>')
|
|
if below:
|
|
text((x + tip) / 2, max(ya, yb_) + 14, label, fill=color, anchor="middle",
|
|
bold=True, size=11)
|
|
else:
|
|
text(tip + (4 if side == "right" else -4), (ya + yb_) / 2 + 4, label, fill=color,
|
|
anchor="start" if side == "right" else "end", bold=True, size=11)
|
|
|
|
|
|
def stack(items, x_line, minsep=15, lead=13):
|
|
"""Coloana din dreapta: împinge etichetele ca să nu se suprapună și trage
|
|
o liniuță de legătură de la linia de preț la eticheta ei, ca perechea
|
|
etichetă-linie să rămână neambiguă chiar dacă eticheta a fost împinsă."""
|
|
y_prev = None
|
|
for y, lines, fill in sorted(items, key=lambda t: t[0]):
|
|
y_lab = y if y_prev is None or y >= y_prev + minsep else y_prev + minsep
|
|
if y_lab - y > 2:
|
|
out.append(f'<path d="M{x_line + 2:.1f},{y - 4:.1f} H{x_line + 7:.1f} '
|
|
f'L{COL_X - 4:.1f},{y_lab - 4:.1f}" fill="none" stroke="{fill}" '
|
|
f'stroke-width="0.8" stroke-opacity="0.8"/>')
|
|
else:
|
|
line(x_line + 2, y - 4, COL_X - 4, y_lab - 4, fill, width=0.8)
|
|
for j, s in enumerate(lines):
|
|
text(COL_X, y_lab + j * lead, s, fill=fill, size=10)
|
|
y_prev = y_lab + (len(lines) - 1) * lead
|
|
return y_prev
|
|
|
|
|
|
def seg_dist(p, a, b):
|
|
"""Distanța de la punctul p la segmentul ab."""
|
|
(px, py), (ax, ay), (bx, by) = p, a, b
|
|
dx, dy = bx - ax, by - ay
|
|
t = 0.0 if dx == dy == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy)
|
|
/ (dx * dx + dy * dy)))
|
|
return ((px - ax - t * dx) ** 2 + (py - ay - t * dy) ** 2) ** 0.5
|
|
|
|
|
|
out.append('<defs><marker id="pt-red" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" '
|
|
'markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#c00"/></marker>'
|
|
'</defs>')
|
|
text(20, 22, "Aceeași regulă pe trei perechi — graficul mic e lupa pe ultimele candele mari",
|
|
bold=True, size=13)
|
|
|
|
y0 = 40
|
|
for key, f, tf_big, tf_small, k, nz, exp in ROWS:
|
|
fine = scaled(FINE_A, f)[:k * nz]
|
|
base = scaled(BASE_A, f)
|
|
zoom = aggregate(fine, k, nz)
|
|
big = base + zoom
|
|
g = check(key, base, zoom, fine, k, nz, exp)
|
|
first = key == "A"
|
|
lab0 = len(LABELS)
|
|
|
|
text(20, y0 + 14, f"{key}. {tf_big} → {tf_small}", bold=True, size=13)
|
|
text(20, y0 + 32, f"{tf_big}: direcția", fill="#555", size=11)
|
|
text(FRAME_L, y0 + 32, f"{tf_small}: descompunerea ultimelor {nz} candele {tf_big}",
|
|
fill="#555", size=11)
|
|
top = y0 + 50
|
|
|
|
# Intervalul de preț al lupei: de la stop (sub minim) la maximul zonei, cu
|
|
# aer sus/jos. ACELAȘI interval e încadrat pe ambele panouri — asta fac
|
|
# ghidajele; altfel linia de stop ar cădea exact pe marginea cadrului.
|
|
zlo_, zhi_ = g["sl"], g["zhi"]
|
|
pad = PAD * (zhi_ - zlo_)
|
|
lo, hi = zlo_ - pad, zhi_ + pad
|
|
|
|
# scară proprie pe fiecare grafic, constantă în interiorul lui
|
|
bmax = max(b[1] for b in big)
|
|
bmin = min(min(b[2] for b in big), g["b_sl"], lo)
|
|
bs, ss = PLOT_H / (bmax - bmin), PLOT_H / (hi - lo)
|
|
yb = lambda p, t=top, m=bmax, s=bs: t + (m - p) * s # noqa: E731
|
|
ys = lambda p, t=top, m=hi, s=ss: t + (m - p) * s # noqa: E731
|
|
|
|
# ---- TF mare -----------------------------------------------------------
|
|
# zona verde peste doji + confirmare (acolo se decide direcția)
|
|
zx0, zx1 = BX0 + DOJI_B * BGAP - 9, BX0 + CONF_B * BGAP + 6.5
|
|
rect(zx0, yb(base[CONF_B][1]) - 5, zx1, yb(g["dl"]) + 6, GREEN)
|
|
text((zx0 + zx1) / 2, top + 178, "doji + confirmare:", fill=GREEN, anchor="middle",
|
|
bold=True, size=11)
|
|
text((zx0 + zx1) / 2, top + 192, "aici se decide direcția", fill=GREEN, anchor="middle",
|
|
bold=True, size=11)
|
|
# caseta de lupă (punctată, ca în buy_doji_directie) peste exact candelele
|
|
# mari care se sparg în dreapta, pe intervalul de preț al cadrului mic
|
|
lx0, lx1 = BX0 + len(base) * BGAP - 6, BX0 + (len(big) - 1) * BGAP + 9
|
|
assert lx0 > zx1 + 1, f"{key}: caseta de lupă intră peste caseta verde"
|
|
ly0, ly1 = yb(hi), yb(lo)
|
|
rect(lx0, ly0, lx1, ly1, BLUE, fill_op=0.06, dash="4 3")
|
|
# nivelul-reper de pe TF-ul mare: high-ul dojiului, retestat jos
|
|
hline(BX0 - 15, lx1, yb(g["dh"]), GRAY, "3 3")
|
|
text(42, yb(g["dh"]) + 11, f"high doji {tf_big}", fill="#666", size=11)
|
|
text(42, yb(g["dh"]) + 24, f"{g['dh']:.2f}", fill="#666", size=11)
|
|
# nivelul comun legat de panoul mic: close-ul confirmării mari = open-ul
|
|
# primei bare mici (prin construcție), deci e exact același preț pe ambele
|
|
hline(BX0 - 15, lx1, yb(g["conf"]), LINK, "3 3", width=1.0)
|
|
# BIFA: minimul pe care pullback-ul de pe panoul mic nu are voie să-l rupă.
|
|
# Nu se poate desena pe panoul mic (e cu mult sub scara lui, iar coborând
|
|
# scara ghidajele ar deveni false), deci trăiește aici, pe TF-ul mare,
|
|
# sub paranteza de stop — nu în interiorul ei.
|
|
line(BX0 - 19, yb(g["dl"]), BX0 + DOJI_B * BGAP + BODY_W / 2, yb(g["dl"]),
|
|
"#555", dash="2 3", width=1.2)
|
|
candles(big, BX0, BGAP, yb)
|
|
|
|
# ---- ghidajele de lupă -------------------------------------------------
|
|
guides = [((lx1, ly0), (FRAME_L, top)), ((lx1, ly1), (FRAME_L, top + PLOT_H))]
|
|
for a_, b_ in guides:
|
|
line(a_[0], a_[1], b_[0], b_[1], BLUE, dash="5 3", width=1.1)
|
|
|
|
# ---- nivelul comun: conector subțire între cele două panouri -----------
|
|
# Scările au pante diferite (e o lupă), deci același preț cade la înălțimi
|
|
# diferite. Conectorul le leagă; numărul e scris o singură dată, în cot.
|
|
ycb, ycs = yb(g["conf"]), ys(g["conf"])
|
|
|
|
def clearance(mx):
|
|
conn = [((lx1, ycb), (mx, ycb)), ((mx, ycb), (mx, ycs)), ((mx, ycs), (FRAME_L, ycs))]
|
|
pts = [(sx + (ex - sx) * t / 24, sy + (ey - sy) * t / 24)
|
|
for (sx, sy), (ex, ey) in conn for t in range(25)]
|
|
return min(seg_dist(q, ga, gb) for q in pts if q[0] >= lx1 + 10 for ga, gb in guides)
|
|
|
|
# cotul se alege unde conectorul stă cel mai departe de ghidajele albastre:
|
|
# pantele diferă per rând, deci mijlocul nu e mereu bun.
|
|
mid = max(range(int(lx1) + 14, int(FRAME_L) - 13), key=clearance)
|
|
dmin = clearance(mid)
|
|
assert dmin > 10, f"{key}: conectorul trece la {dmin:.1f} px de un ghidaj"
|
|
out.append(f'<path d="M{lx1:.1f},{ycb:.1f} H{mid:.1f} V{ycs:.1f} H{FRAME_L:.1f}" '
|
|
f'fill="none" stroke="{LINK}" stroke-width="1.0" stroke-dasharray="3 3"/>')
|
|
tx = min(mid + 5, FRAME_L - 62)
|
|
text(tx, (ycb + ycs) / 2 + 4, f"{g['conf']:.2f}", fill="#777", size=10)
|
|
text(tx, (ycb + ycs) / 2 + 18, "același preț", fill="#999", size=10)
|
|
print(f" conector: cot la x={mid}, la {dmin:.1f} px de ghidaje")
|
|
|
|
# ---- TF mic ------------------------------------------------------------
|
|
y_dh, y_e, y_sl, y_tp = ys(g["dh"]), ys(g["entry"]), ys(g["sl"]), ys(g["tp"])
|
|
assert abs((y_sl - y_e) / (y_e - y_tp) - 1) < 1e-6, f"{key}: SL-intrare != intrare-TP1 în px"
|
|
assert top + PLOT_H - y_sl > 6, f"{key}: linia de stop e lipită de marginea cadrului"
|
|
assert y_sl > ys(g["zlo"]), f"{key}: stopul nu e sub minimul barelor"
|
|
frame_r = SX0 + (len(fine) - 1) * SGAP + 9
|
|
rect(FRAME_L, top, frame_r, top + PLOT_H, BLUE, fill_op=0.03)
|
|
# zona verde peste doji-ul mic + confirmare
|
|
sx0, sx1 = SX0 + FINE_DOJI * SGAP - 9, SX0 + FINE_CONF * SGAP + 9
|
|
rect(sx0, y_e - 6, sx1, ys(g["zlo"]) + 4, GREEN)
|
|
line((sx0 + sx1) / 2, top + PLOT_H, (sx0 + sx1) / 2, top + PLOT_H + 40, GREEN, width=1)
|
|
text((sx0 + sx1) / 2, top + PLOT_H + 52, "doji la nivel + confirmare: aici intri", fill=GREEN,
|
|
anchor="middle", bold=True, size=11)
|
|
hline(FRAME_L, frame_r, ycs, LINK, "3 3", width=1.0)
|
|
hline(FRAME_L, frame_r, y_dh, GRAY, "3 3")
|
|
hline(FRAME_L, frame_r, y_e, GREEN, "6 3")
|
|
hline(FRAME_L, frame_r, y_sl, RED, "6 3")
|
|
hline(FRAME_L, frame_r, y_tp, GREEN, "2 3")
|
|
col = [(y_tp + 4, [f"ținta 1:1 {g['tp']:.2f}"], GREEN),
|
|
(y_e + 4, [f"intrare {g['entry']:.2f}"], GREEN),
|
|
(y_dh + 4, ["retest: high doji", f"{tf_big} {g['dh']:.2f}"], "#666"),
|
|
(y_sl + 4, [f"stop {g['sl']:.2f}"]
|
|
+ (["5 cenți sub minimul", f"ultimelor {STOP_BARS} bare"] if first else []), RED)]
|
|
candles(fine, SX0, SGAP, ys)
|
|
stack(col, frame_r)
|
|
|
|
# pullback: paranteză sub cadru, peste primele 8 bare
|
|
pxa, pxb = SX0 - 6, SX0 + (PULLBACK_N - 1) * SGAP + 6
|
|
out.append(f'<path d="M{pxa:.1f},{top + PLOT_H + 2:.1f} V{top + PLOT_H + 8:.1f} '
|
|
f'H{pxb:.1f} V{top + PLOT_H + 2:.1f}" fill="none" stroke="#555" stroke-width="1"/>')
|
|
text((pxa + pxb) / 2, top + PLOT_H + 22,
|
|
f"pullback: {PULLBACK_N} candele care coboară spre nivel",
|
|
fill="#555", anchor="middle", size=11)
|
|
text((pxa + pxb) / 2, top + PLOT_H + 35,
|
|
f"fără să rupă low-ul doji-ului {tf_big} (bifa din stânga)",
|
|
fill="#555", anchor="middle", size=11)
|
|
text(20, top + PLOT_H + 52,
|
|
f"bifa de sub doji = {g['dl']:.2f}, minimul care nu se rupe", fill="#555", size=11)
|
|
if key == "C":
|
|
text(20, top + PLOT_H + 72,
|
|
"(șapte bare de 60 min pe zi: ședința 9:30-16:00 are 6 ore și jumătate"
|
|
" — ultima bară a zilei e de 30 min)", fill="#777", size=10)
|
|
|
|
if first:
|
|
# același setup, stopul măsurat pe fiecare TF
|
|
bx = 34
|
|
bracket(bx, yb(g["b_entry"]), yb(g["b_sl"]), RED,
|
|
f"stop {g['b_pct']:.2f} %".replace(".", ","), below=True)
|
|
# paranteza stă în golul dintre două candele, ca să nu atingă niciun fitil
|
|
kx = SX0 + (FINE_CONF + 1) * SGAP + BODY_W / 2 + 0.3
|
|
assert min(abs(kx + d - (SX0 + i * SGAP)) for i in range(len(fine)) for d in (0, 5)) \
|
|
> BODY_W / 2, "paranteza de stop atinge o candelă"
|
|
bracket(kx, y_e, y_sl, RED, f"stop {g['pct']:.2f} %".replace(".", ","))
|
|
ratio = g["b_pct"] / g["pct"]
|
|
assert 3.9 <= ratio < 4.5, f"raportul stopurilor {ratio:.2f} nu e „de 4 ori”"
|
|
ya = top + PLOT_H + 76
|
|
line(bx, ya, kx, ya, RED, width=1.6, marker="pt-red")
|
|
text((bx + kx) / 2, ya + 17,
|
|
f"același setup, stop de {ratio:.1f} ori mai mic".replace(".", ","),
|
|
fill=RED, anchor="middle", bold=True, size=11)
|
|
text((bx + kx) / 2, ya + 31,
|
|
f"→ de {ratio:.1f} ori mai multe acțiuni la același risc".replace(".", ","),
|
|
fill=RED, anchor="middle", bold=True, size=11)
|
|
|
|
# --- fiecare preț stă pe panoul lui, cu excepția NIVELURILOR COMUNE -----
|
|
# Nivelurile comune (high-ul dojiului mare, retestat jos; close-ul
|
|
# confirmării mari = open-ul primei bare mici) au voie de ambele părți.
|
|
# Restul prețurilor rămân fiecare pe panoul lui: un preț al TF-ului mare
|
|
# pe panoul mic se citește greșit, ca trigger al panoului mic.
|
|
COMUNE = {"dh", "conf"}
|
|
cval = {round(g[kk], 2) for kk in COMUNE}
|
|
MARE = {kk: g[kk] for kk in ("dh", "dl", "conf", "b_sl", "b_entry")
|
|
if round(g[kk], 2) not in cval}
|
|
MIC = {kk: g[kk] for kk in ("entry", "sl", "tp") if round(g[kk], 2) not in cval}
|
|
for x0_, _x1, _y, s_ in LABELS[lab0:]:
|
|
pe_mic = x0_ >= MIDX
|
|
rau = MARE if pe_mic else MIC
|
|
for kk, v in rau.items():
|
|
assert f"{v:.2f}" not in s_, (
|
|
f"{key}: eticheta {s_!r} de pe panoul "
|
|
f"{'mic' if pe_mic else 'mare'} conține prețul {kk}={v:.2f} al celuilalt panou")
|
|
# dh apare pe AMBELE panouri, cu exact același număr
|
|
v = f"{g['dh']:.2f}"
|
|
st_ = [s_ for x0_, _x1, _y, s_ in LABELS[lab0:] if v in s_ and x0_ < MIDX]
|
|
dr_ = [s_ for x0_, _x1, _y, s_ in LABELS[lab0:] if v in s_ and x0_ >= MIDX]
|
|
assert st_ and dr_, f"{key}: nivelul comun dh={v} lipsește de pe un panou ({st_} / {dr_})"
|
|
# conf apare o SINGURĂ dată, în cotul conectorului dintre panouri
|
|
v = f"{g['conf']:.2f}"
|
|
cx_ = [x0_ for x0_, _x1, _y, s_ in LABELS[lab0:] if v in s_]
|
|
assert len(cx_) == 1 and lx1 < cx_[0] < FRAME_L, \
|
|
f"{key}: {v} nu e scris exact o dată, în cotul conectorului ({cx_})"
|
|
for kk in ("entry", "sl", "tp"):
|
|
assert any(f"{g[kk]:.2f}" in s_ and x0_ >= MIDX for x0_, _x1, _y, s_ in LABELS[lab0:]), \
|
|
f"{key}: {kk} nu apare pe panoul mic"
|
|
|
|
print(f"{key}: {tf_big}→{tf_small} k={k} ({nz} candele mari = {len(fine)} mici) | "
|
|
f"doji mare {g['dh']:.2f}/{g['dl']:.2f} conf {g['conf']:.2f} | lupă {g['zlo']:.2f}-{g['zhi']:.2f} | "
|
|
f"intrare {g['entry']:.2f} SL {g['sl']:.2f} risc {g['risk']:.2f} ({g['pct']:.2f} %) "
|
|
f"TP1 {g['tp']:.2f} la bara {g['first']} | doar mare: SL {g['b_sl']:.2f} risc "
|
|
f"{g['b_risk']:.2f} ({g['b_pct']:.2f} %) = x{g['b_pct'] / g['pct']:.2f} | "
|
|
f"pullback peste {g['span']} candele mari, doji în candela mare {g['doji_big']}")
|
|
y0 = top + (296 if first else (274 if key == "C" else 254))
|
|
if key != "C":
|
|
out.append(f'<line x1="20" y1="{y0 - 14:.0f}" x2="{W - 20}" y2="{y0 - 14:.0f}" stroke="#ddd"/>')
|
|
|
|
H = y0 - 20
|
|
|
|
# nicio pereche de etichete nu se suprapune
|
|
for a in range(len(LABELS)):
|
|
for b in range(a + 1, len(LABELS)):
|
|
xa0, xa1, ya_, sa = LABELS[a]
|
|
xb0, xb1, yb_, sb = LABELS[b]
|
|
assert not (abs(ya_ - yb_) < 13 and xa0 < xb1 - 4 and xb0 < xa1 - 4), \
|
|
f"etichete suprapuse: {sa!r} / {sb!r}"
|
|
assert 0 < LABELS[a][2] < H, f"etichetă în afara înălțimii: {LABELS[a][3]!r}"
|
|
|
|
svg = (f'<svg viewBox="0 0 {W} {H:.0f}" xmlns="http://www.w3.org/2000/svg" '
|
|
f'style="width:100%;height:auto;font:12px sans-serif">\n ' + "\n ".join(out) + "\n</svg>")
|
|
assert "\n\n" not in svg
|
|
assert ".md:" not in svg and "Surse" not in svg, "trimiteri la surse în SVG"
|
|
OUT.write_text(svg, encoding="utf-8")
|
|
print(f"scris {OUT} ({W}x{H:.0f})")
|