diff --git a/README.md b/README.md index ddb9f26..ed4f5a7 100644 --- a/README.md +++ b/README.md @@ -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) - **Direcție** (Buy / Sell — dropdown) - **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) 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. @@ -85,10 +85,10 @@ Pe baza Data + Ora RO, regulile M2D (vezi `strategie_M2D.md`): Sheet-ul **Config** permite editarea: - **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 -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ță) @@ -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 | |---------|----------|----------|----------|-------------|--------------| | **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 | | **TP2** | +TP0/SL | +TP1/SL | +TP2/SL | +0.5·(TP0+TP1)/SL | +0.5·(TP0+TP1)/SL | **Citirea Outcome-ului**: - `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). - `TP2` — prețul a atins TP2 (a trecut prin TP0 și TP1). **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). -- 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 diff --git a/data/backtest.backup-20260521-002847.xlsx b/data/backtest.backup-20260521-002847.xlsx new file mode 100644 index 0000000..49884d5 Binary files /dev/null and b/data/backtest.backup-20260521-002847.xlsx differ diff --git a/data/backtest.corrupt-20260521-004058.xlsx b/data/backtest.corrupt-20260521-004058.xlsx new file mode 100644 index 0000000..59e6349 Binary files /dev/null and b/data/backtest.corrupt-20260521-004058.xlsx differ diff --git a/data/backtest.xlsx b/data/backtest.xlsx index 49884d5..7b95ec6 100644 Binary files a/data/backtest.xlsx and b/data/backtest.xlsx differ diff --git a/scripts/codex_statusline.ps1 b/scripts/codex_statusline.ps1 new file mode 100644 index 0000000..8bd3bab --- /dev/null +++ b/scripts/codex_statusline.ps1 @@ -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 diff --git a/scripts/generate_template.py b/scripts/generate_template.py index aae7558..d84b177 100644 --- a/scripts/generate_template.py +++ b/scripts/generate_template.py @@ -55,7 +55,7 @@ SESSIONS = ["A1", "A2", "A3", "B", "C", "D", "Other"] INDICATORS = ["DIA", "US30", "SPY", "QQQ", "ES", "NQ"] TIMEFRAMES = ["1min", "3min", "15min"] 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 STRAT_KEYS = ["tp0only", "tp1only", "tp2only", "hybrid_be", "hybrid_nobe"] @@ -135,13 +135,13 @@ def build_config(wb: Workbook) -> None: ws["B4"] = 10000 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["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["C6"] = "Auto — derivat din B4 și B5" + ws["C6"] = "Auto — informativ; nu este folosit în formulele $_*" for r in (4, 5): 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}' return ( 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 ( f'=IF({o}="","",' 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})))' ) @@ -302,7 +302,7 @@ def _f_r_hybrid_nobe(r: int) -> str: return ( f'=IF({o}="","",' 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})))' ) @@ -317,8 +317,10 @@ R_FN: dict[str, callable] = { 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}" - 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: @@ -566,8 +568,8 @@ METRIC_HINTS: dict[str, str] = { ), "Average Loss ($)": ( "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" - "Dacă e mult mai mare decât Risk per Trade, ai SL-uri sărite (slippage, gap-uri)." + "În dolari reali, −1R depinde de SL%: pierdere ≈ SL% × Account Size Start.\n" + "Dacă e mult mai mare decât riscul calculat din SL, ai SL-uri sărite (slippage, gap-uri)." ), "Best Trade ($)": ( "Cel mai mare câștig individual.\n" @@ -576,7 +578,7 @@ METRIC_HINTS: dict[str, str] = { ), "Worst Trade ($)": ( "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." ), "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." ), "Expectancy (R)": ( - "Cât bani câștigi în medie pe UN trade (în R; 1R = Risk per Trade, default $100).\n" - "+0.30R = câștigi $30 pe trade. Pe 100 trade-uri: +$3.000.\n" - "−0.10R = pierzi $10 pe trade. Pe 100 trade-uri: −$1.000.\n" + "Cât câștigi în medie pe UN trade, exprimat în R.\n" + "+0.30R = câștigi 0.30 × riscul mediu al trade-urilor.\n" + "−0.10R = pierzi 0.10 × riscul mediu al trade-urilor.\n" "Pragul de GO LIVE: +0.20R sau mai mult." ), "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." ), "Cumulative P&L ($)": (