Skip to content

Commit e5a93ef

Browse files
author
mani
committed
Release v1.95.21
1 parent 4a5432b commit e5a93ef

10 files changed

Lines changed: 191 additions & 64 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Release notes live in dedicated files under `releases/`.
44

55
- [Unreleased](releases/unreleased.md)
6+
- [v1.95.21](releases/v1.95.21.md)
67
- [v1.95.20](releases/v1.95.20.md)
78
- [v1.95.19](releases/v1.95.19.md)
89
- [v1.95.18](releases/v1.95.18.md)

api/serial.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2119
1+
2120

docs/help/35_pbv7_backtest.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ The Configs table also offers **V8** for the saved V7 backtest config. Both conv
191191
Clicking a row opens a full-featured chart panel with:
192192
- **Equity curve** (log scale toggle)
193193
- **Price (PBGui MarketData)** selector for overlaying one result exchange/coin close-price series on a contrasting second Y-axis; coverage is evaluated against the visible equity-chart range, and partial or unavailable data is shown beside the selector while the equity chart remains visible
194-
- **PnL** over time
194+
- **PnL** over time, grouped by symbol and shown net of recorded trading fees on authoritative fill timestamps
195195
- **TWE** (total wallet exposure) chart
196196
- **Hedged PnL** if available
197197
- Full **analysis metrics** table

docs/help/42_pbv8_backtest.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ PB8 stores reusable datasets under `pb8/caches/hlcvs_data` and temporary materia
4747

4848
## Results
4949

50-
The **Version** filter defaults to PBv8 and can switch to PBv7 or **Both**. The list shows each result's version, config, active PB8 strategy, exchange, run directory, and compact scalar metrics. The sortable Strategy column appears whenever V8 rows are visible and remains absent from a pure V7 view. Final balance and equity use the terminal values recorded in PB8's `balance_and_equity.csv` or compressed `.csv.gz` artifact when analysis metadata does not provide explicit totals. Opening a result also provides a **Price (PBGui MarketData)** selector that overlays one configured exchange/coin close-price series on a contrasting second Y-axis and reports coverage against the visible equity-chart range without hiding that chart. Combined and suite results use the real exchanges stored in their configs. Select PBv7 and PBv8 rows together and click **Compare** to overlay their equity and balance series; each file is read from its matching result root. **Delete Selected** also supports a mixed selection and sends every result to its owning PBv7 or PBv8 backend.
50+
The **Version** filter defaults to PBv8 and can switch to PBv7 or **Both**. The list shows each result's version, config, active PB8 strategy, exchange, run directory, and compact scalar metrics. The sortable Strategy column appears whenever V8 rows are visible and remains absent from a pure V7 view. Final balance and equity use the terminal values recorded in PB8's `balance_and_equity.csv` or compressed `.csv.gz` artifact when analysis metadata does not provide explicit totals. Per-symbol PnL uses the authoritative fill timestamps and includes recorded trading fees, so its timing and net total remain comparable to the balance chart. Opening a result also provides a **Price (PBGui MarketData)** selector that overlays one configured exchange/coin close-price series on a contrasting second Y-axis and reports coverage against the visible equity-chart range without hiding that chart. For combined results, PBGui initially selects a configured exchange with full visible price coverage when available; suite results use their real configured exchange as well. Select PBv7 and PBv8 rows together and click **Compare** to overlay their equity and balance series; each file is read from its matching result root. **Delete Selected** also supports a mixed selection and sends every result to its owning PBv7 or PBv8 backend.
5151

5252
PB8 may download historical data when a backtest starts. Review the config, exchanges, coin selection, dates, and PB8 migration report before running a large backtest.
5353

docs/help_de/35_pbv7_backtest.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ Auch die Configs-Tabelle bietet **V8** für die gespeicherte V7-Backtest-Config.
191191
Ein Klick auf eine Zeile öffnet ein vollständiges Chart-Panel mit:
192192
- **Equity-Kurve** (log-Skala umschaltbar)
193193
- **Price (PBGui MarketData)**-Auswahl, um die Schlusskurse einer Result-Exchange/Coin-Kombination kontrastreich auf einer zweiten Y-Achse einzublenden; die Abdeckung wird gegen den sichtbaren Equity-Chart-Zeitraum geprüft, und partielle oder fehlende Daten werden neben der Auswahl angezeigt, während der Equity-Chart sichtbar bleibt
194-
- **PnL** über Zeit
194+
- **PnL** über Zeit, nach Symbol gruppiert, abzüglich erfasster Trading-Gebühren und auf den verbindlichen Fill-Zeitstempeln
195195
- **TWE** (Total Wallet Exposure) Chart
196196
- **Hedged PnL** falls vorhanden
197197
- Vollständige **Analyse-Metriken** Tabelle

docs/help_de/42_pbv8_backtest.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ PB8 speichert wiederverwendbare Datensaetze unter `pb8/caches/hlcvs_data` und te
4747

4848
## Results
4949

50-
Der **Version**-Filter ist standardmäßig auf PBv8 gesetzt und kann auf PBv7 oder **Both** umgeschaltet werden. Die Liste zeigt Version, Config, aktive PB8-Strategie, Exchange, Run-Verzeichnis und kompakte skalare Werte. Die sortierbare Strategy-Spalte erscheint, sobald V8-Zeilen sichtbar sind, und bleibt in einer reinen V7-Ansicht ausgeblendet. Finale Balance und Equity stammen aus den letzten Werten von PB8s `balance_and_equity.csv` beziehungsweise der komprimierten `.csv.gz`-Datei, wenn die Analyse keine expliziten Endwerte enthält. Beim Öffnen eines Results bietet **Price (PBGui MarketData)** eine Exchange/Coin-Auswahl, deren Schlusskurse kontrastreich auf einer zweiten Y-Achse eingeblendet werden; die Abdeckung wird gegen den sichtbaren Equity-Chart-Zeitraum geprüft, ohne diesen Chart auszublenden. Combined- und Suite-Results verwenden die echten Exchanges aus ihrer Config. PBv7- und PBv8-Zeilen können gemeinsam ausgewählt und über **Compare** verglichen werden; jede Equity-Datei wird aus dem passenden Resultroot geladen. **Delete Selected** unterstützt ebenfalls eine gemischte Auswahl und sendet jedes Result an sein zuständiges PBv7- oder PBv8-Backend.
50+
Der **Version**-Filter ist standardmäßig auf PBv8 gesetzt und kann auf PBv7 oder **Both** umgeschaltet werden. Die Liste zeigt Version, Config, aktive PB8-Strategie, Exchange, Run-Verzeichnis und kompakte skalare Werte. Die sortierbare Strategy-Spalte erscheint, sobald V8-Zeilen sichtbar sind, und bleibt in einer reinen V7-Ansicht ausgeblendet. Finale Balance und Equity stammen aus den letzten Werten von PB8s `balance_and_equity.csv` beziehungsweise der komprimierten `.csv.gz`-Datei, wenn die Analyse keine expliziten Endwerte enthält. Der PnL je Symbol verwendet die verbindlichen Fill-Zeitstempel und berücksichtigt erfasste Trading-Gebühren, damit Zeitverlauf und Nettosumme mit dem Balance-Chart vergleichbar bleiben. Beim Öffnen eines Results bietet **Price (PBGui MarketData)** eine Exchange/Coin-Auswahl, deren Schlusskurse kontrastreich auf einer zweiten Y-Achse eingeblendet werden; die Abdeckung wird gegen den sichtbaren Equity-Chart-Zeitraum geprüft, ohne diesen Chart auszublenden. Bei Combined-Results wählt PBGui initial eine konfigurierte Exchange mit vollständiger sichtbarer Kursabdeckung, sofern vorhanden; Suite-Results verwenden ebenfalls ihre echte konfigurierte Exchange. PBv7- und PBv8-Zeilen können gemeinsam ausgewählt und über **Compare** verglichen werden; jede Equity-Datei wird aus dem passenden Resultroot geladen. **Delete Selected** unterstützt ebenfalls eine gemischte Auswahl und sendet jedes Result an sein zuständiges PBv7- oder PBv8-Backend.
5151

5252
PB8 kann beim Backtest historische Daten herunterladen. Config, Exchanges, Coin-Auswahl, Zeitraum und Migrationsbericht sollten vor einem großen Backtest geprüft werden.
5353

frontend/v7_backtest.html

Lines changed: 85 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6688,7 +6688,34 @@ <h3>Paste config</h3>
66886688
status.style.color = warning ? 'var(--orange)' : 'var(--text-dim)';
66896689
}
66906690

6691-
function renderBEWithSelectedPrice(cd, be) {
6691+
function loadPricePayload(cd, market) {
6692+
var cacheKey = cd.path + '|' + market.exchange + '|' + market.coin;
6693+
if (_priceCache[cacheKey]) return Promise.resolve(_priceCache[cacheKey]);
6694+
return resultApiFetch(
6695+
cd.result,
6696+
'/results/price?path=' + encodeURIComponent(cd.path)
6697+
+ '&exchange=' + encodeURIComponent(market.exchange)
6698+
+ '&coin=' + encodeURIComponent(market.coin)
6699+
+ '&max_points=6000'
6700+
).then(function(payload) {
6701+
_priceCache[cacheKey] = payload;
6702+
return payload;
6703+
});
6704+
}
6705+
6706+
function pricePayloadCoversChart(payload, be) {
6707+
if (!(payload && payload.available && payload.time && payload.time.length)) return false;
6708+
var chartStart = Date.parse((be.time || [])[0] || '');
6709+
var chartEnd = Date.parse((be.time || [])[be.time.length - 1] || '');
6710+
var coverageStart = Date.parse(payload.coverage_start || '');
6711+
var coverageEnd = Date.parse(payload.coverage_end || '');
6712+
return Number.isFinite(chartStart) && Number.isFinite(chartEnd)
6713+
&& Number.isFinite(coverageStart) && Number.isFinite(coverageEnd)
6714+
? coverageStart <= chartStart && coverageEnd >= chartEnd
6715+
: !!payload.coverage_complete;
6716+
}
6717+
6718+
function renderBEWithSelectedPrice(cd, be, autoSelectCoverage) {
66926719
if (cd.type !== 'be') {
66936720
renderBEChart(cd, be, null);
66946721
return;
@@ -6698,37 +6725,43 @@ <h3>Paste config</h3>
66986725
renderBEChart(cd, be, null);
66996726
return;
67006727
}
6701-
var cacheKey = cd.path + '|' + market.exchange + '|' + market.coin;
6728+
var markets = [market];
6729+
if (autoSelectCoverage) {
6730+
resultPriceMarkets(cd.result).forEach(function(candidate) {
6731+
if (candidate.coin === market.coin && candidate.exchange !== market.exchange) markets.push(candidate);
6732+
});
6733+
}
67026734
var requestSeq = (_priceRequestSeq[cd.id] || 0) + 1;
67036735
_priceRequestSeq[cd.id] = requestSeq;
67046736
setPriceOverlayStatus(cd, 'Loading close prices...', false);
6705-
var request = _priceCache[cacheKey]
6706-
? Promise.resolve(_priceCache[cacheKey])
6707-
: resultApiFetch(
6708-
cd.result,
6709-
'/results/price?path=' + encodeURIComponent(cd.path)
6710-
+ '&exchange=' + encodeURIComponent(market.exchange)
6711-
+ '&coin=' + encodeURIComponent(market.coin)
6712-
+ '&max_points=6000'
6713-
).then(function(payload) {
6714-
_priceCache[cacheKey] = payload;
6715-
return payload;
6716-
});
6717-
request.then(function(payload) {
6737+
Promise.all(markets.map(function(candidate) {
6738+
return loadPricePayload(cd, candidate).then(function(payload) {
6739+
return { market: candidate, payload: payload };
6740+
}).catch(function(error) {
6741+
return { market: candidate, payload: null, error: error };
6742+
});
6743+
})).then(function(entries) {
67186744
if (_priceRequestSeq[cd.id] !== requestSeq) return;
6745+
var picked = entries[0];
6746+
if (autoSelectCoverage) {
6747+
var fullCoverage = entries.find(function(entry) { return pricePayloadCoversChart(entry.payload, be); });
6748+
var firstAvailable = entries.find(function(entry) {
6749+
return entry.payload && entry.payload.available && entry.payload.time && entry.payload.time.length;
6750+
});
6751+
picked = fullCoverage || firstAvailable || picked;
6752+
}
6753+
if (!picked.payload && picked.error) throw picked.error;
6754+
var payload = picked.payload;
6755+
if (picked.market.exchange !== market.exchange) {
6756+
var select = document.getElementById('price-market-' + cd.idx);
6757+
if (select) select.value = priceMarketOptionValue(picked.market);
6758+
}
67196759
if (!(payload && payload.available && payload.time && payload.time.length)) {
67206760
setPriceOverlayStatus(cd, 'No matching PBGui MarketData for this range.', true);
67216761
renderBEChart(cd, be, null);
67226762
return;
67236763
}
6724-
var chartStart = Date.parse((be.time || [])[0] || '');
6725-
var chartEnd = Date.parse((be.time || [])[be.time.length - 1] || '');
6726-
var coverageStart = Date.parse(payload.coverage_start || '');
6727-
var coverageEnd = Date.parse(payload.coverage_end || '');
6728-
var coversChart = Number.isFinite(chartStart) && Number.isFinite(chartEnd)
6729-
&& Number.isFinite(coverageStart) && Number.isFinite(coverageEnd)
6730-
? coverageStart <= chartStart && coverageEnd >= chartEnd
6731-
: !!payload.coverage_complete;
6764+
var coversChart = pricePayloadCoversChart(payload, be);
67326765
var coverage = coversChart
67336766
? 'full chart coverage'
67346767
: ('partial: ' + String(payload.coverage_start || '?').slice(0, 10) + ' to ' + String(payload.coverage_end || '?').slice(0, 10));
@@ -6744,19 +6777,19 @@ <h3>Paste config</h3>
67446777
function rerenderBEChartPrice(idx) {
67456778
var result = (_activeResultsCtx.data || [])[idx];
67466779
if (!result || !_beCache[result.path]) return;
6747-
renderBEWithSelectedPrice({ id: 'be-chart-' + idx, type: 'be', path: result.path, result: result, idx: idx }, _beCache[result.path]);
6780+
renderBEWithSelectedPrice({ id: 'be-chart-' + idx, type: 'be', path: result.path, result: result, idx: idx }, _beCache[result.path], false);
67486781
}
67496782

67506783
function loadAndRenderBEChart(cd) {
67516784
if (_beCache[cd.path]) {
67526785
var _cached = _beCache[cd.path];
6753-
requestAnimationFrame(function() { renderBEWithSelectedPrice(cd, _cached); });
6786+
requestAnimationFrame(function() { renderBEWithSelectedPrice(cd, _cached, true); });
67546787
return;
67556788
}
67566789
fetchCSV(cd.path, 'equity', cd.result).then(function(csv) {
67576790
var be = normalizeBE(csv, cd.result);
67586791
_beCache[cd.path] = be;
6759-
requestAnimationFrame(function() { renderBEWithSelectedPrice(cd, be); });
6792+
requestAnimationFrame(function() { renderBEWithSelectedPrice(cd, be, true); });
67606793
}).catch(function(e) {
67616794
document.getElementById(cd.id).innerHTML = '<div style="color:var(--red);padding:var(--sp-md)">Failed to load BE data: ' + esc(e.message) + '</div>';
67626795
});
@@ -7098,17 +7131,18 @@ <h3>Paste config</h3>
70987131
var headers = csv.headers.map(function(h) { return h.trim(); });
70997132
var coinCol = headers.indexOf('coin') >= 0 ? 'coin' : 'symbol';
71007133
var pnlCol = 'pnl';
7134+
var feeCol = headers.indexOf('fee_paid') >= 0 ? 'fee_paid' : null;
71017135

71027136
/* Resolve timestamps with the historical load_fills() priority. */
71037137
var r = cd.result;
71047138
var times = resolveFilsTimes(rows, headers, r);
71057139

7106-
/* Group PnL by coin, cumulative */
7140+
/* Group realized PnL and fees by coin, cumulative. */
71077141
var coins = {};
71087142
rows.forEach(function(row, i) {
71097143
var coin = row[coinCol] || 'unknown';
71107144
if (!coins[coin]) coins[coin] = { times: [], pnl: [], cumPnl: 0 };
7111-
coins[coin].cumPnl += parseFloat(row[pnlCol]) || 0;
7145+
coins[coin].cumPnl += (parseFloat(row[pnlCol]) || 0) + (feeCol ? (parseFloat(row[feeCol]) || 0) : 0);
71127146
coins[coin].times.push(times[i]);
71137147
coins[coin].pnl.push(coins[coin].cumPnl);
71147148
});
@@ -7123,14 +7157,14 @@ <h3>Paste config</h3>
71237157

71247158
var name = r.display_name || (r.config_name + '/' + (r.exchange_dir || '') + '/' + (r.result_name || ''));
71257159
var timeStr = fmtDate(r.modified);
7126-
var layout = _chartLayout(name + ' ' + timeStr, 'PnL');
7160+
var layout = _chartLayout(name + ' ' + timeStr, 'Net PnL');
71277161
Plotly.newPlot(cd.id, traces, layout, _plotlyConf());
71287162
}
71297163

7130-
/* resolveFillsTimes — mirrors historical load_fills() column priority:
7164+
/* resolveFillsTimes — use authoritative timestamps when present:
71317165
1. time (already a datetime string)
7132-
2. minute (offset from end_date)
7133-
3. timestamp (numeric ms/s OR datetime string like "2020-01-05 21:33:00")
7166+
2. timestamp (numeric ms/s OR datetime string like "2020-01-05 21:33:00")
7167+
3. minute (legacy offset from end_date)
71347168
*/
71357169
function resolveFilsTimes(rows, headers, result) {
71367170
var times = [];
@@ -7141,39 +7175,40 @@ <h3>Paste config</h3>
71417175
return times;
71427176
}
71437177

7144-
if (headers.indexOf('minute') >= 0) {
7145-
/* Historical behavior: start_time = end_date_ts - max_minute * 60 */
7146-
var endDate = result.end_date || '';
7147-
var endTs = endDate ? new Date(endDate).getTime() : Date.now();
7148-
var maxMin = 0;
7149-
rows.forEach(function(r) { var m = parseFloat(r['minute']) || 0; if (m > maxMin) maxMin = m; });
7150-
var startTs = endTs - (maxMin * 60000);
7151-
rows.forEach(function(r) {
7152-
var m = parseFloat(r['minute']) || 0;
7153-
times.push(new Date(startTs + m * 60000).toISOString());
7154-
});
7155-
return times;
7156-
}
7157-
71587178
if (headers.indexOf('timestamp') >= 0) {
71597179
/* Numeric (ms if > 1e11, else seconds) OR datetime string. */
71607180
var firstVal = (rows[0] || {})['timestamp'] || '';
7161-
var firstNum = parseFloat(firstVal);
7162-
var isNumeric = !isNaN(firstNum);
7181+
var isNumeric = /^[-+]?\d+(\.\d+)?$/.test(String(firstVal).trim());
71637182
rows.forEach(function(r) {
71647183
var raw = r['timestamp'] || '';
71657184
if (isNumeric) {
71667185
var n = parseFloat(raw) || 0;
71677186
var ms = n > 1e11 ? n : n * 1000;
71687187
times.push(new Date(ms).toISOString());
71697188
} else {
7170-
/* datetime string like "2020-01-05 21:33:00" — replace space with T for ISO parse */
7171-
times.push(new Date(raw.replace(' ', 'T')).toISOString());
7189+
/* PB backtest datetime strings are UTC even when they omit an explicit zone. */
7190+
var normalized = raw.replace(' ', 'T');
7191+
if (!/(Z|[+-]\d{2}:?\d{2})$/i.test(normalized)) normalized += 'Z';
7192+
times.push(new Date(normalized).toISOString());
71727193
}
71737194
});
71747195
return times;
71757196
}
71767197

7198+
if (headers.indexOf('minute') >= 0) {
7199+
/* Legacy format: start_time = end_date_ts - max_minute * 60 */
7200+
var endDate = result.end_date || '';
7201+
var endTs = endDate ? new Date(endDate).getTime() : Date.now();
7202+
var maxMin = 0;
7203+
rows.forEach(function(r) { var m = parseFloat(r['minute']) || 0; if (m > maxMin) maxMin = m; });
7204+
var startTs = endTs - (maxMin * 60000);
7205+
rows.forEach(function(r) {
7206+
var m = parseFloat(r['minute']) || 0;
7207+
times.push(new Date(startTs + m * 60000).toISOString());
7208+
});
7209+
return times;
7210+
}
7211+
71777212
/* Fallback: row index */
71787213
rows.forEach(function(r, i) { times.push(i); });
71797214
return times;

pbgui_purefunc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ def import_passivbot_rust():
609609
return pbr
610610

611611
PBGDIR = Path(__file__).resolve().parent
612-
PBGUI_VERSION = "v1.95.20"
612+
PBGUI_VERSION = "v1.95.21"
613613
_serial_path = PBGDIR / 'api' / 'serial.txt'
614614
PBGUI_SERIAL = _serial_path.read_text().strip() if _serial_path.exists() else ''
615615

releases/v1.95.21.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# v1.95.21
2+
3+
## Fixed
4+
5+
- Kept PB8 per-symbol PnL charts aligned to authoritative UTC fill timestamps, included trading fees in their net cumulative PnL, and automatically selected a configured exchange with full visible price coverage for combined results when available.

0 commit comments

Comments
 (0)