feat: risikobasiertes Position-Sizing mit Caps
This commit is contained in:
27
src/server/engine/sizing.test.ts
Normal file
27
src/server/engine/sizing.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { sizePosition, DEFAULT_RISK } from './sizing';
|
||||
|
||||
test('1% Equity-Risiko bestimmt die Größe', () => {
|
||||
// Equity 1000, Risiko 10 USDT; Entry 100, Stop 94 → 6 USDT Risiko/Einheit → qty 10/6
|
||||
const r = sizePosition(1000, 1000, 100, 94, DEFAULT_RISK);
|
||||
expect(r.qty).toBeCloseTo(10 / 6);
|
||||
expect(r.notional).toBeCloseTo((10 / 6) * 100);
|
||||
expect(r.blockedBy).toBeNull();
|
||||
});
|
||||
|
||||
test('Cap bei 30% der Equity', () => {
|
||||
// enger Stop: Entry 100, Stop 99.5 → ungecappt 2000 USDT Notional → Cap 300
|
||||
const r = sizePosition(1000, 1000, 100, 99.5, DEFAULT_RISK);
|
||||
expect(r.notional).toBeCloseTo(300);
|
||||
});
|
||||
|
||||
test('Cap durch verfügbares Cash', () => {
|
||||
const r = sizePosition(1000, 100, 100, 99.5, DEFAULT_RISK);
|
||||
expect(r.notional).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
test('blockiert unter Mindestordergröße', () => {
|
||||
const r = sizePosition(1000, 5, 100, 94, DEFAULT_RISK);
|
||||
expect(r.qty).toBe(0);
|
||||
expect(r.blockedBy).toBe('min_notional');
|
||||
});
|
||||
30
src/server/engine/sizing.ts
Normal file
30
src/server/engine/sizing.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export interface RiskConfig {
|
||||
riskPerTradePct: number; // 0.01 = 1% der Equity
|
||||
maxPositionPct: number; // 0.30
|
||||
minNotionalUsdt: number; // 10
|
||||
}
|
||||
|
||||
export const DEFAULT_RISK: RiskConfig = { riskPerTradePct: 0.01, maxPositionPct: 0.3, minNotionalUsdt: 10 };
|
||||
|
||||
export interface SizingResult {
|
||||
qty: number;
|
||||
notional: number;
|
||||
riskAmount: number;
|
||||
blockedBy: 'min_notional' | null;
|
||||
}
|
||||
|
||||
export function sizePosition(
|
||||
equity: number,
|
||||
cash: number,
|
||||
entryPrice: number,
|
||||
stopPrice: number,
|
||||
cfg: RiskConfig,
|
||||
): SizingResult {
|
||||
const riskAmount = equity * cfg.riskPerTradePct;
|
||||
const stopDist = entryPrice - stopPrice;
|
||||
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);
|
||||
if (!(notional >= cfg.minNotionalUsdt)) return { qty: 0, notional: 0, riskAmount, blockedBy: 'min_notional' };
|
||||
return { qty: notional / entryPrice, notional, riskAmount, blockedBy: null };
|
||||
}
|
||||
Reference in New Issue
Block a user