feat: Paper-Portfolio mit Fees, Slippage, R-Multiples
This commit is contained in:
78
src/server/engine/portfolio.ts
Normal file
78
src/server/engine/portfolio.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Pair } from '../types';
|
||||
|
||||
export interface ExecConfig {
|
||||
feeRate: number; // 0.001 pro Seite
|
||||
slippage: number; // 0.0005 pro Seite
|
||||
}
|
||||
|
||||
export const DEFAULT_EXEC: ExecConfig = { feeRate: 0.001, slippage: 0.0005 };
|
||||
|
||||
export interface Position {
|
||||
pair: Pair;
|
||||
qty: number;
|
||||
entryTs: number;
|
||||
entryPrice: number; // Fill inkl. Slippage
|
||||
entryCost: number; // qty*fill + Entry-Fee
|
||||
initialStop: number;
|
||||
stop: number;
|
||||
highestHigh: number;
|
||||
riskAmount: number;
|
||||
}
|
||||
|
||||
export interface ClosedTrade {
|
||||
pair: Pair;
|
||||
entryTs: number;
|
||||
entryPrice: number;
|
||||
exitTs: number;
|
||||
exitPrice: number;
|
||||
qty: number;
|
||||
pnl: number;
|
||||
r: number;
|
||||
exitReason: 'trailing_stop' | 'end_of_data';
|
||||
}
|
||||
|
||||
export class Portfolio {
|
||||
cash: number;
|
||||
positions = new Map<Pair, Position>();
|
||||
trades: ClosedTrade[] = [];
|
||||
|
||||
constructor(startCapital: number, private exec: ExecConfig) {
|
||||
this.cash = startCapital;
|
||||
}
|
||||
|
||||
open(pair: Pair, ts: number, signalPrice: number, initialStop: number, qty: number, riskAmount: number): void {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
close(pair: Pair, ts: number, exitPrice: number, exitReason: ClosedTrade['exitReason']): ClosedTrade {
|
||||
const pos = this.positions.get(pair);
|
||||
if (!pos) throw new Error(`close ohne Position: ${pair}`);
|
||||
const fill = exitPrice * (1 - this.exec.slippage);
|
||||
const proceeds = pos.qty * fill;
|
||||
const fee = proceeds * this.exec.feeRate;
|
||||
this.cash += proceeds - fee;
|
||||
const pnl = proceeds - fee - pos.entryCost;
|
||||
const trade: ClosedTrade = {
|
||||
pair, entryTs: pos.entryTs, entryPrice: pos.entryPrice, exitTs: ts,
|
||||
exitPrice: fill, qty: pos.qty, pnl, r: pnl / pos.riskAmount, exitReason,
|
||||
};
|
||||
this.trades.push(trade);
|
||||
this.positions.delete(pair);
|
||||
return trade;
|
||||
}
|
||||
|
||||
equity(lastClose: Map<Pair, number>): number {
|
||||
let eq = this.cash;
|
||||
for (const pos of this.positions.values()) {
|
||||
eq += pos.qty * (lastClose.get(pos.pair) ?? pos.entryPrice);
|
||||
}
|
||||
return eq;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user