Trading journal calculations
Build custom trading journal session performance metrics that run only in your browser and power the session Report tab and custom report layouts. Manage them at Calculations — they never leave your browser when a session performance table runs.
Who this is for
Use calculations when built-in metrics are not enough — for example win rate your way, expectancy, or a custom risk score. You write a small JavaScript function; EndureTrade runs it against the trades on that session and shows the result in the performance table.
Function shape
Your source must define function calculate(params) and return a non-empty array of number or string values. Single-value table cells show the last array element.
function calculate(params) {
// params: trades for this session (already filtered by long/short/all)
console.log('trade count', params.trades.length);
return [params.trades.length];
}Data you receive
calculate receives capital and a list of trades. There is no separate side field — the performance tab filters long/short/all and passes that subset as params.trades.
interface CalculationParams {
capital: number;
trades: Trade[];
}
interface Trade {
id: string; // uuid
userId: string;
sessionId: string;
tradeNumber: number;
symbol: string;
direction: 'long' | 'short';
positionSize: number;
entryPrice: number;
openedAt: string; // ISO 8601 UTC
/** Omit while the position is still open. */
exitPrice?: number;
closedAt?: string;
profitLoss?: number;
note?: string;
tags?: string[];
entryMethod?: string;
exitMethod?: string;
stopLoss?: number;
takeProfit?: number;
favorableExcursion?: number;
adverseExcursion?: number;
timeframe?: string;
createdAt: string;
updatedAt: string;
messages?: Array<{ id: string; content?: string; createdAt: string }>;
}Built-in helpers
The calculation sandbox automatically provides these functions — you do not paste them into your source_code. They are available in the calculation tester and on session performance cards. Expand each function to see its full implementation.
Whether the trade is still open (no valid exit price and closed timestamp).
function isTradeOpen(t) {
var exitOk = t.exitPrice !== undefined && t.exitPrice !== null && t.exitPrice > 0;
var closedOk = t.closedAt !== undefined && t.closedAt !== null && String(t.closedAt).length > 0;
return !(exitOk && closedOk);
}Signed P/L from entry/exit prices and position size when profitLoss is unset.
function calculateTradeProfitLoss(t) {
var exitPrice = t.exitPrice;
if (exitPrice === undefined || exitPrice === null || !Number.isFinite(exitPrice) || exitPrice <= 0) {
return undefined;
}
var entryPrice = t.entryPrice;
var positionSize = t.positionSize;
if (!Number.isFinite(entryPrice) || !Number.isFinite(positionSize)) {
return undefined;
}
if (positionSize <= 0 || entryPrice <= 0) {
return undefined;
}
var isLong = t.direction === 'long' || t.direction === 'Long';
var diff = isLong ? exitPrice - entryPrice : entryPrice - exitPrice;
return diff * positionSize;
}Stored profitLoss when finite, otherwise calculateTradeProfitLoss(t).
function resolvePnl(t) {
var stored = t.profitLoss;
if (stored !== undefined && stored !== null && Number.isFinite(stored)) {
return stored;
}
return calculateTradeProfitLoss(t);
}Arithmetic mean; returns 0 when values is empty.
function average(values) {
return values.length > 0 ? values.reduce(function (s, v) { return s + v; }, 0) / values.length : 0;
}Sample standard deviation; returns 0 when fewer than two values.
function sampleStdDev(values) {
if (values.length < 2) {
return 0;
}
var avg = average(values);
var sumSq = values.reduce(function (s, v) { return s + Math.pow(v - avg, 2); }, 0);
return Math.sqrt(sumSq / (values.length - 1));
}Root mean square of negative returns (Sortino denominator).
function downsideRms(negativeReturns) {
if (negativeReturns.length === 0) {
return 0;
}
var sumSq = negativeReturns.reduce(function (s, r) { return s + Math.pow(r, 2); }, 0);
return Math.sqrt(sumSq / negativeReturns.length);
}Signed fractional return from entry to exit price.
function instrumentReturn(t) {
if (t.exitPrice === undefined || !(t.exitPrice > 0) || !(t.entryPrice > 0)) {
return undefined;
}
if (t.direction === 'long' || t.direction === 'Long') {
return (t.exitPrice - t.entryPrice) / t.entryPrice;
}
return (t.entryPrice - t.exitPrice) / t.entryPrice;
}Resolvable signed P/L values only (omits unresolved trades).
function finitePnls(trades) {
return (trades || []).map(resolvePnl).filter(function (p) {
return p !== undefined && Number.isFinite(p);
});
}Per-trade signed P/L for every trade (0 when unresolved).
function listProfitLoss(params) {
return (params.trades || []).map(function (t) {
var p = resolvePnl(t);
return p !== undefined && Number.isFinite(p) ? p : 0;
});
}Running total of listProfitLoss — one cumulative point per trade.
function cumulativeProfitLoss(params) {
var list = listProfitLoss(params);
var out = [];
var sum = 0;
list.forEach(function (p) {
sum += p;
out.push(sum);
});
return out;
}Per-trade MAE (non-positive; 0 when unresolved).
function listMae(params) {
return (params.trades || []).map(function (t) {
var v = normalizeAdverseExcursionValue(t.adverseExcursion);
return v !== undefined ? v : 0;
});
}Per-trade MFE (0 when unresolved).
function listMfe(params) {
return (params.trades || []).map(function (t) {
var v = t.favorableExcursion;
return v !== undefined && v !== null && Number.isFinite(v) ? v : 0;
});
}Mean signed P/L over trades with a resolvable P/L.
function averageProfitLoss(params) {
var pnls = finitePnls(params.trades);
return pnls.length > 0 ? average(pnls) : 0;
}Maximum absolute P/L among provided trades.
function largestAbsProfitLoss(params) {
var abs = finitePnls(params.trades).map(function (p) { return Math.abs(p); });
return abs.length > 0 ? Math.max.apply(null, abs) : 0;
}Maximum absolute instrument return among provided trades.
function largestAbsTradePct(params) {
var rets = (params.trades || []).map(instrumentReturn).filter(function (r) {
return r !== undefined && Number.isFinite(r);
}).map(function (r) { return Math.abs(r); });
return rets.length > 0 ? Math.max.apply(null, rets) : 0;
}max(|P/L|) / sum(|P/L|) over provided trades.
function largestAbsShareOfSum(params) {
var abs = finitePnls(params.trades).map(function (p) { return Math.abs(p); });
var sum = abs.reduce(function (s, p) { return s + p; }, 0);
if (sum <= 0) {
return 0;
}
return Math.max.apply(null, abs) / sum;
}Closed-trade analytics: percentageProfitable, profitFactor, ratioAverageWinLoss, sharpeRatio, sortinoRatio.
- percentageProfitable — wins / closed trades
- profitFactor — gross profit / gross loss
- ratioAverageWinLoss — average win / average loss
- sharpeRatio — mean return / sample stddev
- sortinoRatio — mean return / downside RMS
function computeGroupG(params) {
var trades = params.trades || [];
var capital = typeof params.capital === 'number' && params.capital > 0 ? params.capital : 100000;
var closed = trades.filter(function (t) {
return !isTradeOpen(t) && t.exitPrice !== undefined && t.exitPrice > 0;
});
var closedWith = closed.map(function (t) {
var pnl = resolvePnl(t);
return { trade: t, pnl: pnl === undefined ? 0 : pnl };
});
var grossProfit = 0, grossLoss = 0, winCount = 0, lossCount = 0, totalWins = 0, totalLosses = 0;
closedWith.forEach(function (x) {
var pnl = x.pnl;
if (pnl > 0) {
grossProfit += pnl;
winCount += 1;
totalWins += pnl;
} else if (pnl < 0) {
var mag = Math.abs(pnl);
grossLoss += mag;
lossCount += 1;
totalLosses += mag;
}
});
var sortedClosed = closedWith.slice().sort(function (a, b) {
return Date.parse(a.trade.closedAt || '') - Date.parse(b.trade.closedAt || '');
});
var equity = capital;
var returns = [];
sortedClosed.forEach(function (x) {
if (equity <= 0) {
return;
}
returns.push((x.pnl / equity) * 100);
equity += x.pnl;
});
var avgReturn = average(returns);
var sd = sampleStdDev(returns);
var downside = returns.filter(function (r) { return r < 0; });
var downDev = downsideRms(downside);
var averageWinningTrade = winCount > 0 ? totalWins / winCount : 0;
var averageLosingTrade = lossCount > 0 ? totalLosses / lossCount : 0;
return {
percentageProfitable: closed.length > 0 ? winCount / closed.length : 0,
profitFactor: grossLoss > 0 ? grossProfit / grossLoss : 0,
ratioAverageWinLoss: averageLosingTrade > 0 ? averageWinningTrade / averageLosingTrade : 0,
sharpeRatio: sd > 0 ? avgReturn / sd : 0,
sortinoRatio: downDev > 0 ? avgReturn / downDev : 0
};
}Largest peak-to-trough equity drawdown on the trade sequence.
function maxEquityDrawdown(params) {
var trades = params.trades || [];
if (trades.length === 0) {
return 0;
}
var closedTrades = trades.filter(isClosedTradeWithExit);
var tradesToAnalyze = closedTrades.length > 0
? closedTrades
: trades.filter(function (t) {
var pl = resolvePnl(t);
return pl !== undefined && Number.isFinite(pl);
});
if (tradesToAnalyze.length === 0) {
return 0;
}
var useClosedCurve = closedTrades.length > 0;
var curveSourceTrades = useClosedCurve
? closedTrades.slice().sort(function (a, b) { return closedAtMs(a) - closedAtMs(b); })
: tradesToAnalyze.slice().sort(function (a, b) { return openedAtMs(a) - openedAtMs(b); });
var cumulativeEquity = 0;
var peakEquity = 0;
var maxDd = 0;
var getPnl = function (t) {
var p = resolvePnl(t);
return p === undefined ? 0 : p;
};
curveSourceTrades.forEach(function (trade) {
maxDd = Math.max(maxDd, maxIntraTradeDrawdownFromAdverse(trade, curveSourceTrades, getPnl));
});
curveSourceTrades.forEach(function (trade) {
cumulativeEquity += getPnl(trade);
peakEquity = Math.max(peakEquity, cumulativeEquity);
maxDd = Math.max(maxDd, peakEquity - cumulativeEquity);
});
var openTrades = trades.filter(isTradeOpen);
var openUnrealizedSum = openTrades.reduce(function (sum, t) {
var pl = resolvePnl(t);
return pl !== undefined && Number.isFinite(pl) ? sum + pl : sum;
}, 0);
var hasOpenMarkToMarket = openTrades.some(function (t) {
var pl = resolvePnl(t);
return pl !== undefined && Number.isFinite(pl);
});
if (useClosedCurve && hasOpenMarkToMarket) {
var equityWithOpen = cumulativeEquity + openUnrealizedSum;
peakEquity = Math.max(peakEquity, equityWithOpen);
maxDd = Math.max(maxDd, peakEquity - equityWithOpen);
}
return maxDd;
}Testing and logs
In the calculation form tester, use console.log, console.warn, or console.error. Captured lines appear in a Logs section under the result (they are not shown on the session performance table).
Prefer console for debug output. You can also put intermediate strings in the return array — the performance table still displays only the last element:
function calculate(params) {
var closed = (params.trades || []).filter(function (t) {
return typeof t.profitLoss === 'number';
});
// last value is the cell display; earlier strings are visible in the tester Result JSON
return ['closed=' + closed.length, closed.length];
}Errors and timeout
Thrown errors become { ok: false, error: "…" }. Invalid returns use Invalid return type. Runs that exceed your timeout (Settings → Preferences, default 5000 ms) are stopped and return Calculation timed out.
Results appear on the session Report layouts tab — bind metric, graph, and table cards to these calculations. Prefer a file backup? See the Calculations import guide.