manual_log: helper Python + simplified /m2d-log (6 fields obligatorii, restul derivate)
This commit is contained in:
@@ -1,99 +1,142 @@
|
||||
---
|
||||
description: Adaugă manual un rând în jurnal.csv (source=manual sau manual_calibration). Pentru calibrare P4 sau forward paper.
|
||||
argument-hint: "[--calibration] <screenshot_path>"
|
||||
description: Adaugă rapid un trade în jurnal.csv. 6 câmpuri minim, restul derivate auto. Manual entry rapidă pentru backtest/forward paper.
|
||||
argument-hint: "[--calibration]"
|
||||
---
|
||||
|
||||
# /m2d-log — manual M2D trade entry
|
||||
# /m2d-log — quick manual M2D entry
|
||||
|
||||
Marius extrage manual TOATE câmpurile trade-ului. Folosit pentru calibration P4 (împreună cu `/backtest --calibration` pe același screenshot) sau ca log direct fără vision.
|
||||
User-ul (Marius) loghează rapid un trade. 6 câmpuri esențiale obligatorii, restul derivate automat de `scripts/manual_log.py`.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Parse `$ARGUMENTS`** — detectează flag `--calibration` și `<screenshot_path>`. Dacă `<screenshot_path>` lipsește, întreabă user-ul. Calculează `basename = basename(<screenshot_path>)` și `basename_no_ext = basename` minus ultima extensie.
|
||||
1. **Parse `$ARGUMENTS`** — flag `--calibration` produce `source=manual_calibration`; altfel `source=manual`.
|
||||
|
||||
2. **Promptează user-ul în română**, pe rând, pentru fiecare câmp din schema `M2DExtraction` (vezi `scripts/vision_schema.py`). Ordinea + opțiuni valide:
|
||||
2. **Cere user-ului următoarele 6 câmpuri obligatorii** (afișează template-ul de mai jos și roagă să răspundă într-un singur mesaj, format `cheie: valoare`):
|
||||
|
||||
- `data` — `YYYY-MM-DD`
|
||||
- `ora_utc` — `HH:MM` (conversie din RO local: EEST=UTC+3 vară, EET=UTC+2 iarnă; întreabă user-ul direct dacă nu e clar)
|
||||
- `instrument` — `DIA` / `US30` / `other`
|
||||
- `directie` — `Buy` / `Sell`
|
||||
- `tf_mare` — `5min` / `15min`
|
||||
- `tf_mic` — `1min` / `3min`
|
||||
- `calitate` — `Clară` / `Mai mare ca impuls` / `Slabă` / `n/a`
|
||||
- `entry`, `sl`, `tp0`, `tp1`, `tp2` — float-uri
|
||||
- `risc_pct` — float (ex: `0.12` pentru 0.12%)
|
||||
- `outcome_path` — `SL` / `TP0→SL` / `TP0→TP1` / `TP0→TP2` / `TP0→pending` / `pending` (UNICODE `→`)
|
||||
- `max_reached` — `SL_first` / `TP0` / `TP1` / `TP2`
|
||||
- `be_moved` — `true` / `false`
|
||||
- `confidence` — default `high` (manual e by definition high)
|
||||
- `note` — string opțional, default `""`
|
||||
|
||||
`screenshot_file` se setează automat la `basename`; `ambiguities` se setează automat la `[]`. Dacă user-ul dă valoare invalidă, repetă întrebarea.
|
||||
|
||||
3. **Construiește JSON-ul** complet, valid contra `M2DExtraction`.
|
||||
|
||||
4. **Scrie JSON-ul** la `data/extractions/<basename_no_ext>.manual.json` — pretty-print indent 2, UTF-8, newline final. Sufixul `.manual` previne coliziunea cu output-ul vision (`<basename_no_ext>.json`).
|
||||
|
||||
5. **Determină source**: `manual_calibration` dacă `--calibration` e prezent, altfel `manual`.
|
||||
|
||||
6. **Append la CSV**:
|
||||
|
||||
```bash
|
||||
python -c "from pathlib import Path; from scripts.append_row import append_extraction; import json; r = append_extraction(Path('data/extractions/<basename_no_ext>.manual.json'), source='<source>'); print(json.dumps(r, default=str))"
|
||||
```
|
||||
data: 2026-05-13 (format YYYY-MM-DD)
|
||||
ora: 17:33 (ora RO local, HH:MM — DST gestionat automat)
|
||||
dir: Sell (Buy sau Sell)
|
||||
entry: 492.47 (prețul de intrare = close trigger candle)
|
||||
sl: 492.77 (SL absolute price)
|
||||
out: TP0→pending (SL | TP0→SL | TP0→TP1 | TP0→TP2 | TP0→pending | pending)
|
||||
```
|
||||
|
||||
Parsezi răspunsul JSON.
|
||||
Opționale (utilizatorul le poate omite — defaults aplicate):
|
||||
```
|
||||
inst: DIA (default DIA — alternative: US30, other)
|
||||
calitate: Clară (default n/a — Clară | Mai mare ca impuls | Slabă)
|
||||
tf_mic: 1min (default 1min — alternativ: 3min)
|
||||
tf_mare: 5min (default 5min — alternativ: 15min)
|
||||
note: ... (default empty)
|
||||
```
|
||||
|
||||
7. **Dacă `status == "ok"`**:
|
||||
3. **Parsează răspunsul user-ului**. Tolerant la spații în jurul valorilor. Dacă lipsește un câmp obligatoriu → afișează ce lipsește și ceri din nou.
|
||||
|
||||
4. **Construiește dict-ul** apelând helper-ul Python (printr-un singur `python -c`):
|
||||
|
||||
```bash
|
||||
python -c "
|
||||
import json
|
||||
from scripts.manual_log import build_extraction
|
||||
d = build_extraction(
|
||||
data='<data>',
|
||||
ora_ro='<ora>',
|
||||
directie='<dir>',
|
||||
entry=<entry>,
|
||||
sl=<sl>,
|
||||
outcome_path='<out>',
|
||||
instrument='<inst_or_DIA>',
|
||||
tf_mare='<tf_mare_or_5min>',
|
||||
tf_mic='<tf_mic_or_1min>',
|
||||
calitate='<calitate_or_n/a>',
|
||||
note='<note_or_empty>',
|
||||
)
|
||||
import pathlib
|
||||
basename_no_ext = d['screenshot_file'].rsplit('.', 1)[0]
|
||||
p = pathlib.Path(f'data/extractions/{basename_no_ext}.manual.json')
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(json.dumps(d, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
print(json.dumps({'json_path': str(p), 'screenshot_file': d['screenshot_file']}, default=str))
|
||||
"
|
||||
```
|
||||
|
||||
Helper-ul calculează automat:
|
||||
- `ora_utc` din `ora_ro` (DST-aware Europe/Bucharest)
|
||||
- `tp0` = entry ± 0.4·|entry−sl| (sign în funcție de directie)
|
||||
- `tp1` = entry ± 0.6·|entry−sl|
|
||||
- `tp2` = entry ± |entry−sl| (simetric SL)
|
||||
- `risc_pct` = 100·|entry−sl|/entry
|
||||
- `max_reached` din `outcome_path` (SL→SL_first, TP0→SL→TP0, TP0→TP1→TP1, ...)
|
||||
- `be_moved` din `outcome_path` (False pentru SL/pending, True pentru orice TP0→...)
|
||||
- `screenshot_file` generat dacă nu e prezent: `<data>-<inst>-<ora_compactă>.png`
|
||||
- Cross-field ordering validat (Buy: sl<entry<tp0<tp1<tp2; Sell invers)
|
||||
|
||||
5. **Append la CSV**:
|
||||
|
||||
```bash
|
||||
python -c "
|
||||
from pathlib import Path
|
||||
from scripts.append_row import append_extraction
|
||||
import json
|
||||
r = append_extraction(Path('<json_path>'), source='<source>')
|
||||
print(json.dumps(r, default=str))
|
||||
"
|
||||
```
|
||||
|
||||
6. **Dacă `status == "ok"`**:
|
||||
|
||||
```bash
|
||||
python -m scripts.regenerate_md
|
||||
```
|
||||
|
||||
Apoi afișezi:
|
||||
Apoi afișează concis:
|
||||
|
||||
```
|
||||
✅ Trade adăugat la jurnal. ID: <id>. Set: <set>. P/L Marius: <pl_marius>. outcome_path: <outcome_path>.
|
||||
✅ Trade #<id> adăugat — set=<set>, outcome=<outcome_path>, pl_marius=<pl>, pl_theoretical=<pl_t>
|
||||
```
|
||||
|
||||
8. **Dacă `status == "rejected"`**:
|
||||
7. **Dacă `status == "rejected"`**:
|
||||
|
||||
```
|
||||
❌ Trade respins: <reason>
|
||||
❌ Respins: <reason>
|
||||
```
|
||||
|
||||
NU regenera MD. Dacă `reason` conține "duplicate":
|
||||
- pentru `--calibration`: spui user-ului că există deja rând `manual_calibration` pentru acest screenshot; nu poți avea două leg-uri manual de calibrare pe același screenshot.
|
||||
- pentru `source=manual` simplu: user-ul decide dacă suprascrie (atunci șterge manual rândul din `data/jurnal.csv` și re-rulează).
|
||||
Dacă `reason` conține "duplicate" → trade-ul cu acel `(screenshot_file, source)` există deja. Dacă vrei să-l suprascrii, șterge linia din `data/jurnal.csv` și re-rulează (sau cere user-ului să specifice `note: <diferit>` ca să forțeze basename diferit).
|
||||
|
||||
Dacă `reason` conține "validation" → câmpurile au violat constraint-urile pydantic; reîntrebezi user-ul ce să corecteze.
|
||||
|
||||
8. **Errori în parsing user**: dacă user-ul răspunde ambiguu (ex: lipsește `dir`, sau `entry` nu e număr), afișează ce trebuie corectat și revii la step 2 cu valorile parțiale păstrate.
|
||||
|
||||
## Reguli
|
||||
|
||||
- NU edita CSV direct — folosește `append_extraction`.
|
||||
- NU regenera MD dacă append-ul a fost respins.
|
||||
- NU edita CSV direct.
|
||||
- NU regenera MD pe rejection.
|
||||
- Helper-ul `build_extraction` ridică `ValueError` dacă: `entry == sl`, `Buy` cu `sl >= entry`, `Sell` cu `sl <= entry`. Propaga eroarea către user cu mesaj clar.
|
||||
|
||||
## Output skeleton JSON
|
||||
## Exemple
|
||||
|
||||
```json
|
||||
{
|
||||
"screenshot_file": "2026-05-13-dia-1645.png",
|
||||
"data": "2026-05-13",
|
||||
"ora_utc": "14:45",
|
||||
"instrument": "DIA",
|
||||
"directie": "Buy",
|
||||
"tf_mare": "5min",
|
||||
"tf_mic": "1min",
|
||||
"calitate": "Clară",
|
||||
"entry": 497.42,
|
||||
"sl": 496.80,
|
||||
"tp0": 497.67,
|
||||
"tp1": 497.79,
|
||||
"tp2": 498.04,
|
||||
"risc_pct": 0.12,
|
||||
"outcome_path": "TP0→TP1",
|
||||
"max_reached": "TP1",
|
||||
"be_moved": true,
|
||||
"confidence": "high",
|
||||
"ambiguities": [],
|
||||
"note": ""
|
||||
}
|
||||
**Cel mai scurt input valid**:
|
||||
```
|
||||
data: 2026-05-13
|
||||
ora: 17:33
|
||||
dir: Sell
|
||||
entry: 492.47
|
||||
sl: 492.77
|
||||
out: TP0→pending
|
||||
```
|
||||
→ generează screenshot_file=`2026-05-13-dia-1733.png`, calculează ora_utc=14:33, tp0=492.35, tp1=492.29, tp2=492.17, risc_pct=0.0609, max_reached=TP0, be_moved=True, set=A2 (Mie 17:33).
|
||||
|
||||
**Cu calitate și note**:
|
||||
```
|
||||
data: 2026-05-13
|
||||
ora: 17:33
|
||||
dir: Sell
|
||||
entry: 492.47
|
||||
sl: 492.77
|
||||
out: TP0→TP1
|
||||
calitate: Clară
|
||||
note: bună retragere dimineața, news risc zero
|
||||
```
|
||||
|
||||
**Calibrare**: `/m2d-log --calibration` → source=manual_calibration. Folosit cu `/backtest --calibration <screenshot>` pe același screenshot pentru P4 mismatch report.
|
||||
|
||||
134
scripts/manual_log.py
Normal file
134
scripts/manual_log.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Helper for manual M2D trade entry — derives full M2DExtraction dict from minimal user inputs.
|
||||
|
||||
User provides 6 required fields: data, ora_ro, directie, entry, sl, outcome_path.
|
||||
All other fields default or are computed:
|
||||
- tp0 = entry ± 0.4 × |entry - sl|
|
||||
- tp1 = entry ± 0.6 × |entry - sl|
|
||||
- tp2 = entry ± 1.0 × |entry - sl| (symmetric with sl)
|
||||
- risc_pct = 100 × |entry - sl| / entry
|
||||
- ora_utc = ora_ro converted via Europe/Bucharest (DST-aware)
|
||||
- max_reached derived from outcome_path
|
||||
- be_moved = True if outcome contains TP0 else False
|
||||
- tf_mare/tf_mic default 5min/1min
|
||||
- calitate default 'n/a'
|
||||
- confidence = 'high' (manual entry)
|
||||
- screenshot_file generated if not provided: <data>-<instrument>-<ora_ro>.png
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, time
|
||||
from typing import Literal
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
RO_TZ = ZoneInfo("Europe/Bucharest")
|
||||
UTC_TZ = ZoneInfo("UTC")
|
||||
|
||||
|
||||
OUTCOME_TO_MAX_REACHED = {
|
||||
"SL": "SL_first",
|
||||
"TP0→SL": "TP0",
|
||||
"TP0→TP1": "TP1",
|
||||
"TP0→TP2": "TP2",
|
||||
"TP0→pending": "TP0",
|
||||
"pending": "SL_first", # placeholder; user can override
|
||||
}
|
||||
|
||||
OUTCOME_TO_BE_MOVED = {
|
||||
"SL": False,
|
||||
"TP0→SL": True, # BE move should have happened; True = rule-enforced
|
||||
"TP0→TP1": True,
|
||||
"TP0→TP2": True,
|
||||
"TP0→pending": True,
|
||||
"pending": False,
|
||||
}
|
||||
|
||||
|
||||
def ro_to_utc(data_iso: str, ora_ro_str: str) -> str:
|
||||
"""Convert (YYYY-MM-DD, HH:MM RO) -> HH:MM UTC string, DST-aware."""
|
||||
d = date.fromisoformat(data_iso)
|
||||
t = datetime.strptime(ora_ro_str, "%H:%M").time()
|
||||
dt_ro = datetime.combine(d, t, tzinfo=RO_TZ)
|
||||
dt_utc = dt_ro.astimezone(UTC_TZ)
|
||||
return dt_utc.strftime("%H:%M")
|
||||
|
||||
|
||||
def build_extraction(
|
||||
data: str,
|
||||
ora_ro: str,
|
||||
directie: Literal["Buy", "Sell"],
|
||||
entry: float,
|
||||
sl: float,
|
||||
outcome_path: Literal["SL", "TP0→SL", "TP0→TP1", "TP0→TP2", "TP0→pending", "pending"],
|
||||
instrument: Literal["DIA", "US30", "other"] = "DIA",
|
||||
tf_mare: Literal["5min", "15min"] = "5min",
|
||||
tf_mic: Literal["1min", "3min"] = "1min",
|
||||
calitate: Literal["Clară", "Mai mare ca impuls", "Slabă", "n/a"] = "n/a",
|
||||
max_reached: Literal["SL_first", "TP0", "TP1", "TP2"] | None = None,
|
||||
be_moved: bool | None = None,
|
||||
screenshot_file: str | None = None,
|
||||
note: str = "",
|
||||
) -> dict:
|
||||
"""Build a M2DExtraction-compatible dict from minimal manual inputs.
|
||||
|
||||
Derived fields:
|
||||
- ora_utc from ora_ro (DST-aware)
|
||||
- tp0/tp1/tp2 from entry/sl geometry
|
||||
- risc_pct from |entry-sl|/entry
|
||||
- max_reached/be_moved from outcome_path (overridable)
|
||||
- screenshot_file generated from data+instrument+ora_ro if not provided
|
||||
|
||||
The returned dict satisfies scripts.vision_schema.M2DExtraction.
|
||||
"""
|
||||
if entry == sl:
|
||||
raise ValueError("entry == sl — zero risk distance")
|
||||
|
||||
risk_abs = abs(entry - sl)
|
||||
risc_pct = round(100 * risk_abs / entry, 4)
|
||||
|
||||
if directie == "Buy":
|
||||
if sl >= entry:
|
||||
raise ValueError(f"Buy: sl ({sl}) must be < entry ({entry})")
|
||||
tp0 = round(entry + 0.4 * risk_abs, 4)
|
||||
tp1 = round(entry + 0.6 * risk_abs, 4)
|
||||
tp2 = round(entry + risk_abs, 4)
|
||||
else: # Sell
|
||||
if sl <= entry:
|
||||
raise ValueError(f"Sell: sl ({sl}) must be > entry ({entry})")
|
||||
tp0 = round(entry - 0.4 * risk_abs, 4)
|
||||
tp1 = round(entry - 0.6 * risk_abs, 4)
|
||||
tp2 = round(entry - risk_abs, 4)
|
||||
|
||||
ora_utc = ro_to_utc(data, ora_ro)
|
||||
|
||||
if max_reached is None:
|
||||
max_reached = OUTCOME_TO_MAX_REACHED[outcome_path]
|
||||
if be_moved is None:
|
||||
be_moved = OUTCOME_TO_BE_MOVED[outcome_path]
|
||||
|
||||
if screenshot_file is None:
|
||||
ora_compact = ora_ro.replace(":", "")
|
||||
screenshot_file = f"{data}-{instrument.lower()}-{ora_compact}.png"
|
||||
|
||||
return {
|
||||
"screenshot_file": screenshot_file,
|
||||
"data": data,
|
||||
"ora_utc": ora_utc,
|
||||
"instrument": instrument,
|
||||
"directie": directie,
|
||||
"tf_mare": tf_mare,
|
||||
"tf_mic": tf_mic,
|
||||
"calitate": calitate,
|
||||
"entry": round(float(entry), 4),
|
||||
"sl": round(float(sl), 4),
|
||||
"tp0": tp0,
|
||||
"tp1": tp1,
|
||||
"tp2": tp2,
|
||||
"risc_pct": risc_pct,
|
||||
"outcome_path": outcome_path,
|
||||
"max_reached": max_reached,
|
||||
"be_moved": be_moved,
|
||||
"confidence": "high",
|
||||
"ambiguities": [],
|
||||
"note": note,
|
||||
}
|
||||
175
tests/test_manual_log.py
Normal file
175
tests/test_manual_log.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""Tests for scripts.manual_log — derivation logic + vision_schema compatibility."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.manual_log import build_extraction, OUTCOME_TO_BE_MOVED, OUTCOME_TO_MAX_REACHED, ro_to_utc
|
||||
from scripts.vision_schema import M2DExtraction
|
||||
|
||||
|
||||
def test_buy_happy_path():
|
||||
d = build_extraction(
|
||||
data="2026-05-13",
|
||||
ora_ro="17:33",
|
||||
directie="Buy",
|
||||
entry=497.42,
|
||||
sl=496.80,
|
||||
outcome_path="TP0→TP1",
|
||||
)
|
||||
# Satisfies pydantic
|
||||
M2DExtraction.model_validate(d)
|
||||
assert d["directie"] == "Buy"
|
||||
assert d["entry"] == 497.42
|
||||
assert d["sl"] == 496.80
|
||||
# tp0 = entry + 0.4 * 0.62 = 497.42 + 0.248 = 497.668
|
||||
assert abs(d["tp0"] - 497.668) < 0.01
|
||||
assert abs(d["tp1"] - 497.792) < 0.01
|
||||
assert abs(d["tp2"] - 498.04) < 0.01
|
||||
assert d["max_reached"] == "TP1"
|
||||
assert d["be_moved"] is True
|
||||
|
||||
|
||||
def test_sell_happy_path():
|
||||
d = build_extraction(
|
||||
data="2026-05-13",
|
||||
ora_ro="17:33",
|
||||
directie="Sell",
|
||||
entry=492.47,
|
||||
sl=492.77,
|
||||
outcome_path="TP0→TP1",
|
||||
)
|
||||
M2DExtraction.model_validate(d)
|
||||
assert d["directie"] == "Sell"
|
||||
# For Sell: tp0 = entry - 0.4 * 0.30 = 492.47 - 0.12 = 492.35
|
||||
assert abs(d["tp0"] - 492.35) < 0.01
|
||||
assert abs(d["tp1"] - 492.29) < 0.01
|
||||
assert abs(d["tp2"] - 492.17) < 0.01
|
||||
assert d["sl"] > d["entry"] > d["tp0"] > d["tp1"] > d["tp2"]
|
||||
|
||||
|
||||
def test_ora_utc_dst_summer():
|
||||
# May = EEST = UTC+3
|
||||
d = build_extraction(
|
||||
data="2026-05-13", ora_ro="17:33", directie="Sell",
|
||||
entry=492.47, sl=492.77, outcome_path="pending",
|
||||
)
|
||||
assert d["ora_utc"] == "14:33"
|
||||
|
||||
|
||||
def test_ora_utc_dst_winter():
|
||||
# January = EET = UTC+2
|
||||
d = build_extraction(
|
||||
data="2026-01-15", ora_ro="17:00", directie="Buy",
|
||||
entry=400, sl=399, outcome_path="pending",
|
||||
)
|
||||
assert d["ora_utc"] == "15:00"
|
||||
|
||||
|
||||
def test_outcome_sl_max_reached_consistent():
|
||||
d = build_extraction(
|
||||
data="2026-05-13", ora_ro="17:33", directie="Buy",
|
||||
entry=400, sl=399, outcome_path="SL",
|
||||
)
|
||||
M2DExtraction.model_validate(d)
|
||||
assert d["max_reached"] == "SL_first"
|
||||
assert d["be_moved"] is False
|
||||
|
||||
|
||||
def test_outcome_tp0_pending_max_reached_tp0():
|
||||
d = build_extraction(
|
||||
data="2026-05-13", ora_ro="17:33", directie="Buy",
|
||||
entry=400, sl=399, outcome_path="TP0→pending",
|
||||
)
|
||||
M2DExtraction.model_validate(d)
|
||||
assert d["max_reached"] == "TP0"
|
||||
assert d["be_moved"] is True
|
||||
|
||||
|
||||
def test_outcome_tp0_sl_be_moved_true():
|
||||
d = build_extraction(
|
||||
data="2026-05-13", ora_ro="17:33", directie="Buy",
|
||||
entry=400, sl=399, outcome_path="TP0→SL",
|
||||
)
|
||||
M2DExtraction.model_validate(d)
|
||||
assert d["max_reached"] == "TP0"
|
||||
assert d["be_moved"] is True # rule-enforced
|
||||
|
||||
|
||||
def test_explicit_max_reached_override():
|
||||
d = build_extraction(
|
||||
data="2026-05-13", ora_ro="17:33", directie="Buy",
|
||||
entry=400, sl=399, outcome_path="pending",
|
||||
max_reached="TP0",
|
||||
)
|
||||
M2DExtraction.model_validate(d)
|
||||
assert d["max_reached"] == "TP0"
|
||||
|
||||
|
||||
def test_screenshot_file_generated():
|
||||
d = build_extraction(
|
||||
data="2026-05-13", ora_ro="17:33", directie="Sell",
|
||||
entry=492.47, sl=492.77, outcome_path="pending",
|
||||
instrument="DIA",
|
||||
)
|
||||
assert d["screenshot_file"] == "2026-05-13-dia-1733.png"
|
||||
|
||||
|
||||
def test_screenshot_file_explicit():
|
||||
d = build_extraction(
|
||||
data="2026-05-13", ora_ro="17:33", directie="Sell",
|
||||
entry=492.47, sl=492.77, outcome_path="pending",
|
||||
screenshot_file="custom.png",
|
||||
)
|
||||
assert d["screenshot_file"] == "custom.png"
|
||||
|
||||
|
||||
def test_risc_pct_computed():
|
||||
d = build_extraction(
|
||||
data="2026-05-13", ora_ro="17:33", directie="Sell",
|
||||
entry=500.00, sl=501.00, outcome_path="pending",
|
||||
)
|
||||
assert abs(d["risc_pct"] - 0.20) < 0.001 # 1/500 = 0.002 = 0.20%
|
||||
|
||||
|
||||
def test_entry_equals_sl_raises():
|
||||
with pytest.raises(ValueError, match="zero risk"):
|
||||
build_extraction(
|
||||
data="2026-05-13", ora_ro="17:33", directie="Buy",
|
||||
entry=500, sl=500, outcome_path="pending",
|
||||
)
|
||||
|
||||
|
||||
def test_buy_inverted_ordering_raises():
|
||||
with pytest.raises(ValueError, match="Buy"):
|
||||
build_extraction(
|
||||
data="2026-05-13", ora_ro="17:33", directie="Buy",
|
||||
entry=400, sl=401, outcome_path="pending", # sl > entry for Buy
|
||||
)
|
||||
|
||||
|
||||
def test_sell_inverted_ordering_raises():
|
||||
with pytest.raises(ValueError, match="Sell"):
|
||||
build_extraction(
|
||||
data="2026-05-13", ora_ro="17:33", directie="Sell",
|
||||
entry=400, sl=399, outcome_path="pending", # sl < entry for Sell
|
||||
)
|
||||
|
||||
|
||||
def test_optional_fields_defaults():
|
||||
d = build_extraction(
|
||||
data="2026-05-13", ora_ro="17:33", directie="Buy",
|
||||
entry=400, sl=399, outcome_path="pending",
|
||||
)
|
||||
assert d["instrument"] == "DIA"
|
||||
assert d["tf_mare"] == "5min"
|
||||
assert d["tf_mic"] == "1min"
|
||||
assert d["calitate"] == "n/a"
|
||||
assert d["confidence"] == "high"
|
||||
assert d["ambiguities"] == []
|
||||
assert d["note"] == ""
|
||||
|
||||
|
||||
def test_ro_to_utc_helper():
|
||||
assert ro_to_utc("2026-05-13", "17:33") == "14:33"
|
||||
assert ro_to_utc("2026-01-15", "10:00") == "08:00"
|
||||
Reference in New Issue
Block a user