Verificarea cu agent separat a gasit doua contradictii de continut pe care auditul mecanic nu le prinde: - textul spunea ca pullback-ul "incape intr-o singura candela a TF-ului mare"; pe desen se intinde peste doua, iar prima e o saptamana rosie vizibila. Rescris: pe TF-ul mare vezi candelele, dar nu vezi ce e in ele — unde s-a oprit coborarea si doji-ul care o termina, deci n-ai unde intra si n-ai de unde masura stopul. Asta face lupa. - definitia pullback-ului zicea "nu mai face minim nou", absolut, dar desenul arata minime tot mai joase — asa face orice pullback care coboara spre un nivel. Ancorata de minimul de plecare; eticheta de pe desen numeste referinta si trimite la panoul unde e marcata. Restul: perechi_tf punea stopul fix pe low, fara marja de 5 centi pe care o cere Pasul 5 si o respecta directie — aliniat, cu cifrele recalculate; "nivel weekly" devine "retest", pentru ca high-ul unui doji nu e suport si Pasul 3 cere altceva; conector intre nivelul comun al celor doua panouri (nu se pot alinia — sunt scari diferite, e o lupa); leader lines in coloana de etichete; assert-ul de agregare, care era tautologic, verifica acum granitele candelelor mari fata de pullback si doji — exact locul unde textul se rupsese. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
275 lines
13 KiB
Python
275 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Buy Doji — NU: Sell Doji daily contra unui Buy Doji weekly.
|
|
|
|
Citește `gen/strategii/buy_doji_directie.spec.txt` și scrie
|
|
`gen/strategii/buy_doji_directie.svg`. Două panouri stivuite: sus weekly
|
|
(12 bare, Buy Doji confirmat), jos daily (15 bare = ultimele 3 bare weekly
|
|
desfăcute zi cu zi, 5 zile pe săptămână).
|
|
|
|
Seria weekly 9..11 NU e scrisă de mână: e agregarea reală a celor 15 zile
|
|
(open = open-ul primei zile, close = close-ul ultimei, high/low = extremele).
|
|
Panoul weekly poartă un dreptunghi peste exact acele 3 bare, plus două
|
|
ghidaje către colțurile cadrului daily, iar high-ul și low-ul doji-ului daily
|
|
sunt desenate ca aceeași linie punctată, cu același număr, pe ambele panouri.
|
|
|
|
Coordonatele sunt absolute (fără <g transform>), ca audit_diagrams.py să vadă
|
|
pozițiile reale ale textelor.
|
|
|
|
Rulare: python3 gen/strategii/buy_doji_directie.py
|
|
"""
|
|
import re
|
|
from pathlib import Path
|
|
|
|
OUT = Path(__file__).with_suffix(".svg")
|
|
W = 900
|
|
GROUP = 5 # zile pe săptămână
|
|
ZOOM = 3 # câte bare weekly sunt desfăcute jos
|
|
|
|
|
|
def trend(start, closes, wick=0.15):
|
|
bars, o = [], start
|
|
for c in closes:
|
|
bars.append((o, round(max(o, c) + wick, 2), round(min(o, c) - wick, 2), c))
|
|
o = c
|
|
return bars
|
|
|
|
|
|
def agg(days):
|
|
"""Agregă GROUP zile într-o bară weekly."""
|
|
return (days[0][0], max(b[1] for b in days), min(b[2] for b in days), days[-1][3])
|
|
|
|
|
|
# --- seria daily: 15 bare = 3 săptămâni --------------------------------------
|
|
DY = trend(81.30, [81.70, 82.20, 82.00, 82.60, 83.10, 83.50]) + [
|
|
(83.50, 84.30, 83.40, 83.60), # 6: zi cu respingere sus (dă high-ul de referință)
|
|
(83.60, 83.90, 83.05, 83.55), # 7: doji daily
|
|
(83.55, 83.65, 82.60, 82.70), # 8: confirmare => Sell Doji, intrare short
|
|
(82.70, 82.80, 82.00, 82.25), # 9: singura zi de scădere
|
|
(82.25, 83.05, 82.15, 82.95),
|
|
(82.95, 83.70, 82.85, 83.60),
|
|
(83.60, 84.45, 83.50, 84.30), # 12: SL lovit
|
|
(84.30, 84.95, 84.20, 84.80),
|
|
(84.80, 85.45, 84.70, 85.30)]
|
|
|
|
# --- seria weekly: 9 bare de context + cele 3 agregate din daily -------------
|
|
WK = trend(89.0, [88.0, 86.2, 84.9, 83.1, 81.6, 80.2, 78.9]) + [
|
|
(78.90, 79.80, 77.60, 78.80), # 7: doji weekly
|
|
(78.80, 81.60, 78.60, 81.30)] # 8: confirmare => Buy Doji weekly
|
|
WK += [agg(DY[k:k + GROUP]) for k in range(0, len(DY), GROUP)]
|
|
Z0 = len(WK) - ZOOM # prima bară weekly mărită jos
|
|
|
|
|
|
def eq(a, b):
|
|
return abs(a - b) < 0.005
|
|
|
|
|
|
# --- assert-uri: coerență, agregare, poveste ---------------------------------
|
|
assert len(DY) == ZOOM * GROUP and len(WK) == 9 + ZOOM
|
|
for name, bars in (("weekly", WK), ("daily", DY)):
|
|
for i, (o, h, l, c) in enumerate(bars):
|
|
assert h >= max(o, c) and l <= min(o, c), f"{name}[{i}] h/l incoerent"
|
|
if i:
|
|
assert eq(o, bars[i - 1][3]), f"{name}[{i}] open != close precedent"
|
|
assert eq(DY[0][0], WK[8][3]), "daily nu începe la close-ul confirmării weekly"
|
|
# agregarea, verificată din nou pe valorile finale (nu pe cele care au construit-o)
|
|
for k in range(ZOOM):
|
|
days = DY[k * GROUP:(k + 1) * GROUP]
|
|
o, h, l, c = WK[Z0 + k]
|
|
assert eq(o, days[0][0]) and eq(c, days[-1][3]), f"weekly[{Z0+k}] o/c != zile"
|
|
assert eq(h, max(b[1] for b in days)), f"weekly[{Z0+k}] high != max(high zile)"
|
|
assert eq(l, min(b[2] for b in days)), f"weekly[{Z0+k}] low != min(low zile)"
|
|
for name, (o, h, l, c) in (("doji weekly", WK[7]), ("doji daily", DY[7])):
|
|
assert abs(o - c) <= 0.10 * (h - l) + 1e-9, f"{name} nu e doji"
|
|
assert WK[8][3] > WK[7][1], "weekly: fără confirmare"
|
|
assert DY[8][3] < DY[7][2], "daily: fără confirmare Sell Doji"
|
|
|
|
DH, DL = DY[7][1], DY[7][2] # nivelurile comune: high/low doji daily
|
|
ENTRY = DY[8][3]
|
|
SL = round(max(b[1] for b in DY[5:9]) + 0.05, 2) # 5 cenți peste high-ul de 4 candele
|
|
RISK = round(SL - ENTRY, 2)
|
|
TP1 = round(ENTRY - RISK, 2)
|
|
assert eq(ENTRY, 82.70) and eq(SL, 84.35) and eq(RISK, 1.65) and eq(TP1, 81.05)
|
|
assert SL - DH > 0.30, "stopul e prea lipit de high-ul doji-ului (etichete suprapuse)"
|
|
assert DL - ENTRY > 0.30, "intrarea e prea lipită de low-ul doji-ului"
|
|
LOW_AFTER = min(b[2] for b in DY[9:])
|
|
assert eq(LOW_AFTER, 82.00) and LOW_AFTER > TP1, "TP1 atins"
|
|
HIT = next(i for i in range(9, len(DY)) if DY[i][1] >= SL - 1e-9)
|
|
assert HIT == 12, f"SL lovit la {HIT}"
|
|
assert min(b[2] for b in DY) < DH < max(b[1] for b in WK), "nivel comun în afara scării"
|
|
|
|
# --- desen -------------------------------------------------------------------
|
|
GREEN, RED, ORANGE, GRAY, INK, BLUE = "#188a3e", "#c00", "#d06a00", "#777", "#333", "#1a5fb4"
|
|
BODY_W = 9
|
|
out, texts = [], []
|
|
PANEL = "none"
|
|
|
|
|
|
def text(x, y, s, fill=INK, anchor="start", bold=False, size=None, panel=None):
|
|
st = (["font-weight:bold"] if bold else []) + ([f"font-size:{size}px"] if size else [])
|
|
style = f' style="{";".join(st)}"' if st else ""
|
|
a = f' text-anchor="{anchor}"' if anchor != "start" else ""
|
|
out.append(f'<text x="{x:.1f}" y="{y:.1f}" fill="{fill}"{a}{style}>{s}</text>')
|
|
texts.append((panel or PANEL, s))
|
|
|
|
|
|
def line(x1, y1, x2, y2, color, width=1.5, dash=None):
|
|
d = f' stroke-dasharray="{dash}"' if dash else ""
|
|
out.append(f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
|
|
f'stroke="{color}" stroke-width="{width}"{d}/>')
|
|
|
|
|
|
def rect(x, y, w, h, fill, opacity, stroke="none", dash=None):
|
|
d = f' stroke-dasharray="{dash}"' if dash else ""
|
|
out.append(f'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}" fill="{fill}" '
|
|
f'fill-opacity="{opacity}" stroke="{stroke}" stroke-width="1.5"{d} rx="3"/>')
|
|
|
|
|
|
def arrow(x1, y1, x2, y2, color):
|
|
d = max(((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5, 1e-9)
|
|
ux, uy = (x2 - x1) / d, (y2 - y1) / d
|
|
bx, by = x2 - 9 * ux, y2 - 9 * uy
|
|
line(x1, y1, bx, by, color, 1.8)
|
|
out.append(f'<polygon points="{x2:.1f},{y2:.1f} {bx - 4.5 * uy:.1f},{by + 4.5 * ux:.1f} '
|
|
f'{bx + 4.5 * uy:.1f},{by - 4.5 * ux:.1f}" fill="{color}"/>')
|
|
|
|
|
|
def candles(bars, x0, gap, yf):
|
|
for i, (o, h, l, c) in enumerate(bars):
|
|
cx = x0 + i * gap
|
|
line(cx, yf(h), cx, yf(l), "#1a1a1a", 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="{GREEN if c >= o else RED}"/>')
|
|
|
|
|
|
text(20, 24, "NU: Sell Doji daily contra unui Buy Doji weekly — exemplu construit",
|
|
bold=True, size=14)
|
|
|
|
# ---- panoul de sus: weekly ----------------------------------------------------
|
|
PANEL = "weekly"
|
|
WX0, WGAP, WS, WT = 60, 16, 16, 70
|
|
wmax, wmin = max(b[1] for b in WK), min(b[2] for b in WK)
|
|
yw = lambda p: WT + (wmax - p) * WS # noqa: E731
|
|
xw = lambda i: WX0 + i * WGAP # noqa: E731
|
|
w_bottom = yw(wmin)
|
|
text(20, 50, "weekly: direcția", fill="#555", bold=True)
|
|
rect(xw(7) - 8, yw(WK[8][1]) - 4, WGAP + 16, yw(WK[7][2]) - yw(WK[8][1]) + 8, GREEN, 0.12, GREEN)
|
|
# dreptunghiul de corespondență: exact barele weekly desfăcute în panoul daily
|
|
ZX1, ZX2 = xw(Z0) - 8, xw(len(WK) - 1) + 8
|
|
ZY1 = yw(max(b[1] for b in WK[Z0:])) - 8 # exact intervalul de preț al celor
|
|
ZY2 = yw(min(b[2] for b in WK[Z0:])) + 8 # 3 bare mărite, nu tot panoul
|
|
rect(ZX1, ZY1, ZX2 - ZX1, ZY2 - ZY1, BLUE, 0.07, BLUE, "5 4")
|
|
text(ZX2 + 12, ZY1 + 12, f"ultimele {ZOOM} săptămâni,", fill=BLUE, size=11)
|
|
text(ZX2 + 12, ZY1 + 28, "mărite jos", fill=BLUE, size=11)
|
|
|
|
# nivelurile comune, desenate identic pe ambele panouri
|
|
COMMON = [(DH, f"{DH:.2f}", "high doji"), (DL, f"{DL:.2f}", "low doji")]
|
|
candles(WK, WX0, WGAP, yw)
|
|
for k, (lvl, lab, _) in enumerate(COMMON):
|
|
line(44, yw(lvl), ZX2 + 8, yw(lvl), BLUE, 1.2, "3 3")
|
|
text(40, yw(lvl) + (-2 if k == 0 else 11), lab, fill=BLUE, anchor="end", size=10,
|
|
panel="common")
|
|
for k in range(ZOOM):
|
|
text(xw(Z0 + k), ZY2 + 14, f"S{k + 1}", fill=BLUE, anchor="middle",
|
|
size=10, bold=True, panel="common")
|
|
# săgeata verde intră în bara de confirmare pe sub dreptunghiul albastru
|
|
AP = round((WK[7][1] + WK[8][2]) / 2, 2)
|
|
assert WK[8][2] < AP < WK[8][1], "săgeata verde nu cade în bara de confirmare"
|
|
assert yw(AP) > ZY2 + 10, "săgeata verde traversează dreptunghiul S1-S3"
|
|
arrow(470, yw(AP), xw(8) + 11, yw(AP), GREEN)
|
|
text(476, yw(AP) - 4, f"weekly: Buy Doji confirmat (close {WK[8][3]:.2f}, peste "
|
|
f"high-ul doji-ului {WK[7][1]:.2f})", fill=GREEN, bold=True, size=11)
|
|
text(476, yw(AP) + 14, "deci direcția permisă e LONG: pe daily cauți doar Buy Doji",
|
|
fill=GREEN, size=11)
|
|
text(xw(7) + 8, w_bottom + 16, "doji weekly", fill=ORANGE, bold=True, size=10)
|
|
|
|
# ---- panoul de jos: daily -----------------------------------------------------
|
|
PANEL = "daily"
|
|
DX0, DGAP, DS = 60, 18, 40.0
|
|
FT = w_bottom + 92 # marginea de sus a cadrului daily
|
|
DT = FT + 14
|
|
dmax, dmin = max(b[1] for b in DY), min(b[2] for b in DY)
|
|
yd = lambda p: DT + (dmax - p) * DS # noqa: E731
|
|
xd = lambda i: DX0 + i * DGAP # noqa: E731
|
|
FX1, FX2 = xd(0) - 14, xd(len(DY) - 1) + 14
|
|
FB = max(yd(dmin), yd(TP1)) + 14
|
|
|
|
# ghidajele: colțurile de jos ale dreptunghiului -> colțurile de sus ale cadrului
|
|
line(ZX1, ZY2, FX1, FT, BLUE, 1.2, "5 4")
|
|
line(ZX2, ZY2, FX2, FT, BLUE, 1.2, "5 4")
|
|
out.append(f'<rect x="{FX1:.1f}" y="{FT:.1f}" width="{FX2 - FX1:.1f}" height="{FB - FT:.1f}" '
|
|
f'fill="none" stroke="{BLUE}" stroke-width="1.5" stroke-dasharray="5 4" rx="3"/>')
|
|
text(FX2 + 34, w_bottom + 12, f"daily: aceleași {ZOOM} săptămâni, zi cu zi",
|
|
fill=BLUE, bold=True)
|
|
text(FX2 + 34, w_bottom + 30, f"({GROUP} zile = o bară weekly; open, close și extremele coincid)",
|
|
fill="#555", size=11)
|
|
|
|
rect(xd(7) - 8, yd(DY[7][1]) - 3, DGAP + 16, yd(DY[8][2]) - yd(DY[7][1]) + 6, ORANGE, 0.15, ORANGE)
|
|
for k, (lvl, lab, _) in enumerate(COMMON):
|
|
line(44, yd(lvl), FX2 + 14, yd(lvl), BLUE, 1.2, "3 3")
|
|
text(40, yd(lvl) + 4, lab, fill=BLUE, anchor="end", size=10, panel="common")
|
|
|
|
LX2, COL = 340, 348
|
|
y_sl, y_e, y_tp = yd(SL), yd(ENTRY), yd(TP1)
|
|
assert abs((y_sl - y_e) - (y_e - y_tp)) < 1e-6, "SL-intrare != intrare-TP1 în px"
|
|
line(44, y_sl, LX2, y_sl, RED, 1.5, "6 3")
|
|
line(xd(8) - 8, y_e, LX2, y_e, ORANGE, 1.5, "6 3")
|
|
line(xd(8) - 8, y_tp, LX2, y_tp, GRAY, 1.5, "2 3")
|
|
text(COL, y_sl + 4, f"stop {SL:.2f}", fill=RED, size=11, bold=True)
|
|
text(COL, y_e + 4, f"intrare short {ENTRY:.2f}", fill=ORANGE, size=11, bold=True)
|
|
text(COL, y_tp + 4, f"ținta {TP1:.2f} = 1R — neatinsă", fill=GRAY, size=11)
|
|
candles(DY, DX0, DGAP, yd)
|
|
for k in range(ZOOM):
|
|
text((xd(k * GROUP) + xd(k * GROUP + GROUP - 1)) / 2, FB + 16, f"S{k + 1}",
|
|
fill=BLUE, anchor="middle", size=10, bold=True, panel="common")
|
|
text((xd(7) + xd(8)) / 2, yd(DY[7][1]) - 6, "doji daily", fill=ORANGE, anchor="middle",
|
|
bold=True, size=10)
|
|
|
|
cx_hit = xd(HIT)
|
|
out.append(f'<circle cx="{cx_hit:.1f}" cy="{y_sl:.1f}" r="7" fill="none" stroke="{RED}" '
|
|
f'stroke-width="2"/>')
|
|
arrow(512, y_sl - 26, cx_hit + 9, y_sl - 9, RED)
|
|
text(520, y_sl - 30, f"aici te-a scos: stopul {SL:.2f} (5 cenți peste high-ul de 4 candele),",
|
|
fill=RED, bold=True, size=11)
|
|
text(520, y_sl - 12, f"lovit a {HIT - 8}-a zi — banii mari de pe weekly cumpără mai departe",
|
|
fill=RED, size=11)
|
|
|
|
arrow(512, y_e + 52, xd(8) + 8, y_e + 4, ORANGE)
|
|
text(520, y_e + 48, f"aici ai intrat SHORT, la {ENTRY:.2f}: Sell Doji corect ca formă",
|
|
fill=ORANGE, bold=True, size=11)
|
|
text(520, y_e + 66, f"(close sub low-ul doji-ului {DL:.2f}) — dar contra weekly-ului",
|
|
fill=ORANGE, size=11)
|
|
text(520, y_e + 84, f"a scăzut o singură zi, până la {LOW_AFTER:.2f}", fill=ORANGE, size=11)
|
|
|
|
PANEL = "none"
|
|
y0 = FB + 42
|
|
text(20, y0, "Liniile albastre punctate sunt aceleași niveluri pe ambele panouri: "
|
|
"high-ul și low-ul doji-ului daily.", fill="#555", size=11)
|
|
text(20, y0 + 18, "Seria e construită: un singur caz ilustrează regula, nu o dovedește.",
|
|
fill="#555", size=11)
|
|
H = y0 + 40
|
|
|
|
# --- niciun număr scris de mână: fiecare preț din etichete vine din serii ------
|
|
WNUM = {f"{v:.2f}" for b in WK for v in b}
|
|
DNUM = {f"{v:.2f}" for b in DY for v in b} | {f"{v:.2f}" for v in (SL, ENTRY, TP1, RISK)}
|
|
CNUM = {f"{DH:.2f}", f"{DL:.2f}"}
|
|
ALLOWED = {"weekly": WNUM, "daily": DNUM, "common": CNUM, "none": set()}
|
|
for panel, s in texts:
|
|
for n in re.findall(r"\d+\.\d\d", s):
|
|
assert n in ALLOWED[panel], f"eticheta {panel} conține prețul străin {n}: {s!r}"
|
|
# nivelurile comune apar cu exact același număr pe ambele panouri
|
|
com = [s for p, s in texts if p == "common" and re.fullmatch(r"\d+\.\d\d", s)]
|
|
for lab in (f"{DH:.2f}", f"{DL:.2f}"):
|
|
assert com.count(lab) == 2, f"nivelul comun {lab} nu apare pe ambele panouri: {com}"
|
|
|
|
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
|
|
OUT.write_text(svg, encoding="utf-8")
|
|
for k in range(ZOOM):
|
|
print(f"S{k+1}: weekly {WK[Z0+k]} = zilele {k*GROUP}..{k*GROUP+GROUP-1}")
|
|
print(f"intrare {ENTRY:.2f} SL {SL:.2f} risc {RISK:.2f} TP1 {TP1:.2f} | "
|
|
f"doji high {DH:.2f} low {DL:.2f} | min după {LOW_AFTER:.2f} | SL lovit la bara {HIT}")
|
|
print(f"scris {OUT} ({W}x{H:.0f})")
|