This commit is contained in:
Marius
2026-05-21 01:08:00 +03:00
parent c48beb6989
commit 504ab69eff
6 changed files with 189 additions and 23 deletions

View File

@@ -47,7 +47,7 @@ Se generează `data/backtest.xlsx`. Deschide-l în Excel sau LibreOffice Calc.
- **TF** (1min / 3min / 15min — dropdown; e TF-ul de entry, vezi mai jos) - **TF** (1min / 3min / 15min — dropdown; e TF-ul de entry, vezi mai jos)
- **Direcție** (Buy / Sell — dropdown) - **Direcție** (Buy / Sell — dropdown)
- **SL %**, **TP0 %**, **TP1 %**, **TP2 %** — distanțe față de entry, în procente (ex. 0.30 pentru 0.30%) - **SL %**, **TP0 %**, **TP1 %**, **TP2 %** — distanțe față de entry, în procente (ex. 0.30 pentru 0.30%)
- **Outcome** (SL / TP0 only / TP1 / TP2 — dropdown) - **Outcome** (SL / TP0 / TP1 / TP2 — dropdown)
- **Notes** (opțional) - **Notes** (opțional)
4. Coloanele albastre derivate (Zi, Sesiune, R_*, $_*, Bal_* pentru cele 5 strategii) se umplu automat. 4. Coloanele albastre derivate (Zi, Sesiune, R_*, $_*, Bal_* pentru cele 5 strategii) se umplu automat.
5. Mergi la sheet-ul **Dashboard** — metricile, breakdown-urile și equity curve se actualizează live. 5. Mergi la sheet-ul **Dashboard** — metricile, breakdown-urile și equity curve se actualizează live.
@@ -85,10 +85,10 @@ Pe baza Data + Ora RO, regulile M2D (vezi `strategie_M2D.md`):
Sheet-ul **Config** permite editarea: Sheet-ul **Config** permite editarea:
- **Account Size Start ($)** — balanța inițială (default $10,000) - **Account Size Start ($)** — balanța inițială (default $10,000)
- **Risk per Trade (%)** — % din account riscat per trade (default 1.0%) - **Risk reper (%)** — reper configurabil; sumele $ din coloanele `$_*` folosesc `SL % × Account Size Start`
- Listele pentru dropdown-uri: Strategii, Indicatori, TF, Direcție, Outcome - Listele pentru dropdown-uri: Strategii, Indicatori, TF, Direcție, Outcome
La schimbarea Account Size sau Risk %, toate sumele $ din Trades și Dashboard se recalculează. La schimbarea Account Size, sumele $ din `$_*`, `Bal_*` și Dashboard se recalculează. Risk-ul efectiv pe trade este variabil: `SL % × Account Size Start`.
## Formule R-multiples (referință) ## Formule R-multiples (referință)
@@ -99,20 +99,20 @@ Tabelul de mai jos arată R-multiple-ul rezultat pentru fiecare combinație (Out
| Outcome | TP0 only | TP1 only | TP2 only | Hybrid + BE | Hybrid no BE | | Outcome | TP0 only | TP1 only | TP2 only | Hybrid + BE | Hybrid no BE |
|---------|----------|----------|----------|-------------|--------------| |---------|----------|----------|----------|-------------|--------------|
| **SL** | 1 | 1 | 1 | 1 | 1 | | **SL** | 1 | 1 | 1 | 1 | 1 |
| **TP0 only** | +TP0/SL | 1 | 1 | +0.5·TP0/SL | +0.5·TP0/SL 0.5 | | **TP0** | +TP0/SL | 1 | 1 | +0.5·TP0/SL | +0.5·TP0/SL 0.5 |
| **TP1** | +TP0/SL | +TP1/SL | 1 | +0.5·(TP0+TP1)/SL | +0.5·(TP0+TP1)/SL | | **TP1** | +TP0/SL | +TP1/SL | 1 | +0.5·(TP0+TP1)/SL | +0.5·(TP0+TP1)/SL |
| **TP2** | +TP0/SL | +TP1/SL | +TP2/SL | +0.5·(TP0+TP1)/SL | +0.5·(TP0+TP1)/SL | | **TP2** | +TP0/SL | +TP1/SL | +TP2/SL | +0.5·(TP0+TP1)/SL | +0.5·(TP0+TP1)/SL |
**Citirea Outcome-ului**: **Citirea Outcome-ului**:
- `SL` — prețul a atins SL fără să atingă vreodată TP0 (loss complet). - `SL` — prețul a atins SL fără să atingă vreodată TP0 (loss complet).
- `TP0 only` — prețul a atins TP0, dar nu și TP1 (ulterior fie a venit înapoi la SL, fie a fost închis la BE pentru variantele cu BE move). - `TP0` — prețul a atins TP0, dar nu și TP1 (ulterior fie a venit înapoi la SL, fie a fost închis la BE pentru variantele cu BE move).
- `TP1` — prețul a atins TP1 (a trecut prin TP0). - `TP1` — prețul a atins TP1 (a trecut prin TP0).
- `TP2` — prețul a atins TP2 (a trecut prin TP0 și TP1). - `TP2` — prețul a atins TP2 (a trecut prin TP0 și TP1).
**Asumpții de simulare**: **Asumpții de simulare**:
- `TP1 only` și `TP2 only` simulează OCO pur, fără intervenție manuală. Outcome=`TP0 only` se închide la SL (presupunere worst-case). - `TP1 only` și `TP2 only` simulează OCO pur, fără intervenție manuală. Outcome=`TP0` se închide la SL (presupunere worst-case).
- `TP2 only` cu Outcome=`TP1` se închide la SL (TP1 a fost atins, dar SL ar fi venit înainte de TP2). - `TP2 only` cu Outcome=`TP1` se închide la SL (TP1 a fost atins, dar SL ar fi venit înainte de TP2).
- Diferența dintre Hybrid + BE și Hybrid no BE apare doar când Outcome=`TP0 only`; la TP1/TP2 ambele dau identic. - Diferența dintre Hybrid + BE și Hybrid no BE apare doar când Outcome=`TP0`; la TP1/TP2 ambele dau identic.
## Regenerare template ## Regenerare template

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,164 @@
param(
[string]$SessionFile = "",
[string]$Workdir = "",
[string]$CodexHome = ""
)
$ErrorActionPreference = "SilentlyContinue"
function Get-DefaultCodexHome {
if ($env:CODEX_HOME) { return $env:CODEX_HOME }
return (Join-Path $HOME ".codex")
}
function Format-Tokens([Int64]$Value) {
if ($Value -ge 1000000) { return ("{0:0.#}M" -f ($Value / 1000000.0)) }
if ($Value -ge 1000) { return ("{0:0.#}K" -f ($Value / 1000.0)) }
return "$Value"
}
function Format-ResetTime($EpochSeconds) {
if (-not $EpochSeconds) { return "--" }
try {
$dto = [DateTimeOffset]::FromUnixTimeSeconds([Int64]$EpochSeconds).ToLocalTime()
return $dto.ToString("ddd HH:mm")
} catch {
return "--"
}
}
function Format-Remaining($EpochSeconds) {
if (-not $EpochSeconds) { return "--" }
try {
$remaining = [DateTimeOffset]::FromUnixTimeSeconds([Int64]$EpochSeconds) - [DateTimeOffset]::Now
if ($remaining.TotalSeconds -le 0) { return "resetting" }
if ($remaining.TotalDays -ge 1) { return ("{0}d {1}h" -f [int]$remaining.TotalDays, $remaining.Hours) }
if ($remaining.TotalHours -ge 1) { return ("{0}h {1}m" -f [int]$remaining.TotalHours, $remaining.Minutes) }
return ("{0}m" -f [Math]::Max(1, [int]$remaining.TotalMinutes))
} catch {
return "--"
}
}
function Get-Number($Value, [Int64]$Default = 0) {
if ($null -eq $Value) { return $Default }
try { return [Int64]$Value } catch { return $Default }
}
function Read-JsonLine([string]$Line) {
if ([string]::IsNullOrWhiteSpace($Line)) { return $null }
try { return ($Line | ConvertFrom-Json) } catch { return $null }
}
function Get-TokenEventFromObject($Obj) {
if (-not $Obj) { return $null }
if ($Obj.type -eq "event_msg" -and $Obj.payload.type -eq "token_count") { return $Obj.payload }
if ($Obj.type -eq "token_count") { return $Obj }
if ($Obj.info -and $Obj.rate_limits) { return $Obj }
return $null
}
function Get-LatestSessionFile([string]$HomeDir) {
$sessionsDir = Join-Path $HomeDir "sessions"
if (-not (Test-Path $sessionsDir)) { return $null }
return Get-ChildItem -Path $sessionsDir -Recurse -Filter "*.jsonl" -File |
Where-Object { $_.Length -gt 0 } |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
}
function Get-LatestTokenEvent([string]$Path) {
if (-not $Path -or -not (Test-Path $Path)) { return $null }
$lines = Get-Content -LiteralPath $Path -Tail 500
for ($i = $lines.Count - 1; $i -ge 0; $i--) {
$obj = Read-JsonLine $lines[$i]
$event = Get-TokenEventFromObject $obj
if ($event) { return $event }
}
return $null
}
function Get-SessionMeta([string]$Path) {
if (-not $Path -or -not (Test-Path $Path)) { return $null }
foreach ($line in (Get-Content -LiteralPath $Path -TotalCount 100)) {
$obj = Read-JsonLine $line
if ($obj.type -eq "session_meta") { return $obj.payload }
}
return $null
}
function Get-ConfigModel([string]$HomeDir) {
$config = Join-Path $HomeDir "config.toml"
if (-not (Test-Path $config)) { return "codex" }
$line = Get-Content -LiteralPath $config | Where-Object { $_ -match '^\s*model\s*=' } | Select-Object -First 1
if ($line -match '=\s*"([^"]+)"') { return $Matches[1] }
return "codex"
}
if (-not $CodexHome) { $CodexHome = Get-DefaultCodexHome }
$stdinText = @($input) -join "`n"
$stdinObj = Read-JsonLine $stdinText
$event = Get-TokenEventFromObject $stdinObj
if (-not $SessionFile) {
$latest = Get-LatestSessionFile $CodexHome
if ($latest) { $SessionFile = $latest.FullName }
}
if (-not $event) { $event = Get-LatestTokenEvent $SessionFile }
$meta = Get-SessionMeta $SessionFile
if (-not $Workdir) {
if ($stdinObj.workspace.current_dir) { $Workdir = $stdinObj.workspace.current_dir }
elseif ($stdinObj.cwd) { $Workdir = $stdinObj.cwd }
elseif ($meta.cwd) { $Workdir = $meta.cwd }
else { $Workdir = (Get-Location).Path }
}
$model = if ($stdinObj.model.display_name) { $stdinObj.model.display_name } elseif ($stdinObj.model) { $stdinObj.model } elseif ($meta.model) { $meta.model } else { Get-ConfigModel $CodexHome }
$project = Split-Path -Path $Workdir -Leaf
if (-not $project) { $project = "?" }
$branch = "-"
if (Test-Path $Workdir) {
$branchRaw = git -C $Workdir -c gc.auto=0 rev-parse --abbrev-ref HEAD 2>$null
if ($LASTEXITCODE -eq 0 -and $branchRaw) {
$branch = $branchRaw.Trim()
$dirty = git -C $Workdir -c gc.auto=0 status --porcelain 2>$null | Select-Object -First 1
if ($dirty) { $branch = "$branch*" }
}
}
$last = $event.info.last_token_usage
$window = Get-Number $event.info.model_context_window
$ctxUsed = (Get-Number $last.input_tokens) + (Get-Number $last.output_tokens)
$ctxPct = if ($window -gt 0) { [Math]::Round(($ctxUsed * 100.0) / $window) } else { 0 }
$total = $event.info.total_token_usage
$sessionTokens = Get-Number $total.total_tokens
$primary = $event.rate_limits.primary
$secondary = $event.rate_limits.secondary
$limitName = $event.rate_limits.limit_id
if (-not $limitName) { $limitName = "codex" }
$primaryPct = if ($primary.used_percent -ne $null) { [Math]::Round([double]$primary.used_percent) } else { 0 }
$primaryReset = Format-ResetTime $primary.resets_at
$primaryRemain = Format-Remaining $primary.resets_at
$primaryWindow = if ($primary.window_minutes) { "{0}h" -f [Math]::Round([double]$primary.window_minutes / 60.0) } else { "window" }
$line1 = "{0} | ctx {1}/{2} {3}% | session {4} | {5} ({6})" -f `
$model, (Format-Tokens $ctxUsed), (Format-Tokens $window), $ctxPct, (Format-Tokens $sessionTokens), $project, $branch
$line2 = "limits {0} {1}%/{2} reset {3} ({4})" -f $limitName, $primaryPct, $primaryWindow, $primaryRemain, $primaryReset
if ($secondary) {
$secondaryPct = if ($secondary.used_percent -ne $null) { [Math]::Round([double]$secondary.used_percent) } else { 0 }
$line2 += " | secondary {0}% reset {1}" -f $secondaryPct, (Format-Remaining $secondary.resets_at)
}
if ($event.rate_limits.plan_type) {
$line2 += " | plan {0}" -f $event.rate_limits.plan_type
}
Write-Output $line1
Write-Output $line2

View File

@@ -55,7 +55,7 @@ SESSIONS = ["A1", "A2", "A3", "B", "C", "D", "Other"]
INDICATORS = ["DIA", "US30", "SPY", "QQQ", "ES", "NQ"] INDICATORS = ["DIA", "US30", "SPY", "QQQ", "ES", "NQ"]
TIMEFRAMES = ["1min", "3min", "15min"] TIMEFRAMES = ["1min", "3min", "15min"]
DIRECTIONS = ["Buy", "Sell"] DIRECTIONS = ["Buy", "Sell"]
OUTCOMES = ["SL", "TP0 only", "TP1", "TP2"] OUTCOMES = ["SL", "TP0", "TP1", "TP2"]
# Cele 5 strategii de management (sufix folosit în numele coloanelor) + label friendly # Cele 5 strategii de management (sufix folosit în numele coloanelor) + label friendly
STRAT_KEYS = ["tp0only", "tp1only", "tp2only", "hybrid_be", "hybrid_nobe"] STRAT_KEYS = ["tp0only", "tp1only", "tp2only", "hybrid_be", "hybrid_nobe"]
@@ -135,13 +135,13 @@ def build_config(wb: Workbook) -> None:
ws["B4"] = 10000 ws["B4"] = 10000
ws["C4"] = "Balanța inițială pentru calcule $ și HWM (model abstract)" ws["C4"] = "Balanța inițială pentru calcule $ și HWM (model abstract)"
ws["A5"] = "Risk per Trade (%)" ws["A5"] = "Risk reper (%)"
ws["B5"] = 1.0 ws["B5"] = 1.0
ws["C5"] = "% din account riscat per trade (= -1R)" ws["C5"] = "Reper opțional; $_* se calculează din SL% × Account Size Start"
ws["A6"] = "Risk per Trade ($)" ws["A6"] = "Risk reper ($)"
ws["B6"] = "=B4*B5/100" ws["B6"] = "=B4*B5/100"
ws["C6"] = "Auto — derivat din B4 și B5" ws["C6"] = "Auto — informativ; nu este folosit în formulele $_*"
for r in (4, 5): for r in (4, 5):
ws.cell(row=r, column=2).fill = INPUT_FILL ws.cell(row=r, column=2).fill = INPUT_FILL
@@ -270,7 +270,7 @@ def _f_r_tp1only(r: int) -> str:
tp1 = f'{COL["TP1 %"]}{r}' tp1 = f'{COL["TP1 %"]}{r}'
return ( return (
f'=IF({o}="","",' f'=IF({o}="","",'
f'IF(OR({o}="SL",{o}="TP0 only"),-1,{tp1}/{sl}))' f'IF(OR({o}="SL",{o}="TP0"),-1,{tp1}/{sl}))'
) )
@@ -289,7 +289,7 @@ def _f_r_hybrid_be(r: int) -> str:
return ( return (
f'=IF({o}="","",' f'=IF({o}="","",'
f'IF({o}="SL",-1,' f'IF({o}="SL",-1,'
f'IF({o}="TP0 only",0.5*{tp0}/{sl},' f'IF({o}="TP0",0.5*{tp0}/{sl},'
f'0.5*({tp0}+{tp1})/{sl})))' f'0.5*({tp0}+{tp1})/{sl})))'
) )
@@ -302,7 +302,7 @@ def _f_r_hybrid_nobe(r: int) -> str:
return ( return (
f'=IF({o}="","",' f'=IF({o}="","",'
f'IF({o}="SL",-1,' f'IF({o}="SL",-1,'
f'IF({o}="TP0 only",0.5*{tp0}/{sl}-0.5,' f'IF({o}="TP0",0.5*{tp0}/{sl}-0.5,'
f'0.5*({tp0}+{tp1})/{sl})))' f'0.5*({tp0}+{tp1})/{sl})))'
) )
@@ -317,8 +317,10 @@ R_FN: dict[str, callable] = {
def _f_dollar(r: int, r_col: str) -> str: def _f_dollar(r: int, r_col: str) -> str:
"""$ P&L pe contul abstract. Variabil per trade = R × SL%/100 × Account Size."""
rc = f"{COL[r_col]}{r}" rc = f"{COL[r_col]}{r}"
return f'=IF({rc}="","",{rc}*Config!$B$6)' sl = f"{COL['SL %']}{r}"
return f'=IF({rc}="","",{rc}*{sl}/100*Config!$B$4)'
def _f_balance(r: int, dollar_col: str) -> str: def _f_balance(r: int, dollar_col: str) -> str:
@@ -566,8 +568,8 @@ METRIC_HINTS: dict[str, str] = {
), ),
"Average Loss ($)": ( "Average Loss ($)": (
"Pierderea medie pe trade-urile negative (cifra apare cu minus).\n" "Pierderea medie pe trade-urile negative (cifra apare cu minus).\n"
"Cu Risk per Trade fix, ar trebui să fie aproape de 1R în dolari.\n" "În dolari reali, 1R depinde de SL%: pierdere ≈ SL% × Account Size Start.\n"
"Dacă e mult mai mare decât Risk per Trade, ai SL-uri sărite (slippage, gap-uri)." "Dacă e mult mai mare decât riscul calculat din SL, ai SL-uri sărite (slippage, gap-uri)."
), ),
"Best Trade ($)": ( "Best Trade ($)": (
"Cel mai mare câștig individual.\n" "Cel mai mare câștig individual.\n"
@@ -576,7 +578,7 @@ METRIC_HINTS: dict[str, str] = {
), ),
"Worst Trade ($)": ( "Worst Trade ($)": (
"Cea mai mare pierdere individuală.\n" "Cea mai mare pierdere individuală.\n"
"Ar trebui să fie aproximativ egală cu 1R (Risk per Trade din Config).\n" "Ar trebui să fie aproximativ egală cu 1R calculat din SL% × Account Size Start.\n"
"Dacă e semnificativ mai mare, ai depășit risk-ul plănuit — SL ratat, slippage, gap overnight." "Dacă e semnificativ mai mare, ai depășit risk-ul plănuit — SL ratat, slippage, gap overnight."
), ),
"Profit Factor": ( "Profit Factor": (
@@ -590,13 +592,13 @@ METRIC_HINTS: dict[str, str] = {
"Cu R:R mare poți avea Win Ratio mic și tot să faci bani." "Cu R:R mare poți avea Win Ratio mic și tot să faci bani."
), ),
"Expectancy (R)": ( "Expectancy (R)": (
"Cât bani câștigi în medie pe UN trade (în R; 1R = Risk per Trade, default $100).\n" "Cât câștigi în medie pe UN trade, exprimat în R.\n"
"+0.30R = câștigi $30 pe trade. Pe 100 trade-uri: +$3.000.\n" "+0.30R = câștigi 0.30 × riscul mediu al trade-urilor.\n"
"0.10R = pierzi $10 pe trade. Pe 100 trade-uri: $1.000.\n" "0.10R = pierzi 0.10 × riscul mediu al trade-urilor.\n"
"Pragul de GO LIVE: +0.20R sau mai mult." "Pragul de GO LIVE: +0.20R sau mai mult."
), ),
"Expectancy ($)": ( "Expectancy ($)": (
"Aceeași expectancy convertită în dolari, folosind Risk per Trade din Config.\n" "Aceeași expectancy convertită în dolari, folosind SL% × Account Size Start per trade.\n"
"Util ca să vezi cât câștigi în medie pe trade în bani reali, nu doar în R." "Util ca să vezi cât câștigi în medie pe trade în bani reali, nu doar în R."
), ),
"Cumulative P&L ($)": ( "Cumulative P&L ($)": (