fix: Review-Fixes Sizing/Portfolio (invalid_stop-Guard, Doppel-Open-Schutz, Tests)

This commit is contained in:
2026-06-09 20:35:04 +00:00
parent d738a2c9d0
commit 2fa4695f1b
5 changed files with 21 additions and 3 deletions

View File

@@ -27,3 +27,14 @@ test('Equity = Cash + Marktwert offener Positionen', () => {
p.open('BTC_USDT', 0, 100, 94, 2, 10);
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();
});

View File

@@ -41,13 +41,14 @@ export class Portfolio {
}
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 cost = qty * fill;
const fee = cost * this.exec.feeRate;
this.cash -= cost + fee;
this.positions.set(pair, {
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
});
}

View File

@@ -25,3 +25,8 @@ test('blockiert unter Mindestordergröße', () => {
expect(r.qty).toBe(0);
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');
});

View File

@@ -10,7 +10,7 @@ export interface SizingResult {
qty: number;
notional: number;
riskAmount: number;
blockedBy: 'min_notional' | null;
blockedBy: 'min_notional' | 'invalid_stop' | null;
}
export function sizePosition(
@@ -22,6 +22,7 @@ export function sizePosition(
): SizingResult {
const riskAmount = equity * cfg.riskPerTradePct;
const stopDist = entryPrice - stopPrice;
if (stopDist <= 0) return { qty: 0, notional: 0, riskAmount, blockedBy: 'invalid_stop' };
let notional = (riskAmount / stopDist) * entryPrice;
// 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);