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)
- **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

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"]
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 ($)": (