fix: Review-Fixes Sizing/Portfolio (invalid_stop-Guard, Doppel-Open-Schutz, Tests)
This commit is contained in:
@@ -29,7 +29,7 @@ export function computeMetrics(trades: ClosedTrade[], curve: EquityPoint[], star
|
|||||||
trades: trades.length,
|
trades: trades.length,
|
||||||
wins: wins.length,
|
wins: wins.length,
|
||||||
winRate: trades.length ? wins.length / trades.length : 0,
|
winRate: trades.length ? wins.length / trades.length : 0,
|
||||||
profitFactor: grossLoss > 0 ? grossWin / grossLoss : grossWin > 0 ? Infinity : 0,
|
profitFactor: grossLoss > 0 ? grossWin / grossLoss : grossWin > 0 ? Infinity : 0, // keine Trades → 0 (Spec-Konvention, nicht zu NaN/1 "fixen")
|
||||||
totalPnl: trades.reduce((s, t) => s + t.pnl, 0),
|
totalPnl: trades.reduce((s, t) => s + t.pnl, 0),
|
||||||
maxDrawdownPct: maxDd,
|
maxDrawdownPct: maxDd,
|
||||||
avgR: trades.length ? trades.reduce((s, t) => s + t.r, 0) / trades.length : 0,
|
avgR: trades.length ? trades.reduce((s, t) => s + t.r, 0) / trades.length : 0,
|
||||||
|
|||||||
@@ -27,3 +27,14 @@ test('Equity = Cash + Marktwert offener Positionen', () => {
|
|||||||
p.open('BTC_USDT', 0, 100, 94, 2, 10);
|
p.open('BTC_USDT', 0, 100, 94, 2, 10);
|
||||||
expect(p.equity(new Map([['BTC_USDT', 105]]))).toBeCloseTo(p.cash + 2 * 105);
|
expect(p.equity(new Map([['BTC_USDT', 105]]))).toBeCloseTo(p.cash + 2 * 105);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('open auf bestehende Position wirft', () => {
|
||||||
|
const p = new Portfolio(1000, DEFAULT_EXEC);
|
||||||
|
p.open('BTC_USDT', 0, 100, 94, 1, 10);
|
||||||
|
expect(() => p.open('BTC_USDT', 1, 101, 95, 1, 10)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('close ohne Position wirft', () => {
|
||||||
|
const p = new Portfolio(1000, DEFAULT_EXEC);
|
||||||
|
expect(() => p.close('BTC_USDT', 0, 100, 'trailing_stop')).toThrow();
|
||||||
|
});
|
||||||
|
|||||||
@@ -41,13 +41,14 @@ export class Portfolio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open(pair: Pair, ts: number, signalPrice: number, initialStop: number, qty: number, riskAmount: number): void {
|
open(pair: Pair, ts: number, signalPrice: number, initialStop: number, qty: number, riskAmount: number): void {
|
||||||
|
if (this.positions.has(pair)) throw new Error(`open auf bestehende Position: ${pair}`);
|
||||||
const fill = signalPrice * (1 + this.exec.slippage);
|
const fill = signalPrice * (1 + this.exec.slippage);
|
||||||
const cost = qty * fill;
|
const cost = qty * fill;
|
||||||
const fee = cost * this.exec.feeRate;
|
const fee = cost * this.exec.feeRate;
|
||||||
this.cash -= cost + fee;
|
this.cash -= cost + fee;
|
||||||
this.positions.set(pair, {
|
this.positions.set(pair, {
|
||||||
pair, qty, entryTs: ts, entryPrice: fill, entryCost: cost + fee,
|
pair, qty, entryTs: ts, entryPrice: fill, entryCost: cost + fee,
|
||||||
initialStop, stop: initialStop, highestHigh: signalPrice, riskAmount,
|
initialStop, stop: initialStop, highestHigh: signalPrice, riskAmount, // Markt-Preis, nicht Fill: der Chandelier trailt Markt-Hochs, Slippage ist Ausführungsartefakt
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,3 +25,8 @@ test('blockiert unter Mindestordergröße', () => {
|
|||||||
expect(r.qty).toBe(0);
|
expect(r.qty).toBe(0);
|
||||||
expect(r.blockedBy).toBe('min_notional');
|
expect(r.blockedBy).toBe('min_notional');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('blockiert bei Stop >= Entry (invalid_stop)', () => {
|
||||||
|
expect(sizePosition(1000, 1000, 100, 100, DEFAULT_RISK).blockedBy).toBe('invalid_stop');
|
||||||
|
expect(sizePosition(1000, 1000, 100, 101, DEFAULT_RISK).blockedBy).toBe('invalid_stop');
|
||||||
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface SizingResult {
|
|||||||
qty: number;
|
qty: number;
|
||||||
notional: number;
|
notional: number;
|
||||||
riskAmount: number;
|
riskAmount: number;
|
||||||
blockedBy: 'min_notional' | null;
|
blockedBy: 'min_notional' | 'invalid_stop' | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sizePosition(
|
export function sizePosition(
|
||||||
@@ -22,6 +22,7 @@ export function sizePosition(
|
|||||||
): SizingResult {
|
): SizingResult {
|
||||||
const riskAmount = equity * cfg.riskPerTradePct;
|
const riskAmount = equity * cfg.riskPerTradePct;
|
||||||
const stopDist = entryPrice - stopPrice;
|
const stopDist = entryPrice - stopPrice;
|
||||||
|
if (stopDist <= 0) return { qty: 0, notional: 0, riskAmount, blockedBy: 'invalid_stop' };
|
||||||
let notional = (riskAmount / stopDist) * entryPrice;
|
let notional = (riskAmount / stopDist) * entryPrice;
|
||||||
// 0.997: Puffer für Fee (0.1%) + Slippage (0.05%) auf der Entry-Seite
|
// 0.997: Puffer für Fee (0.1%) + Slippage (0.05%) auf der Entry-Seite
|
||||||
notional = Math.min(notional, equity * cfg.maxPositionPct, cash * 0.997);
|
notional = Math.min(notional, equity * cfg.maxPositionPct, cash * 0.997);
|
||||||
|
|||||||
Reference in New Issue
Block a user