Files
trade-kuns/src/server/engine/portfolio.ts
Claude 0e1b477e27 feat: Short-Seite — Indikator, Strategie, Chandelier, Sizing, Portfolio
- donchianLow: Spiegelbild von donchianHigh (tiefstes Tief der N Candles vor i)
- evaluateAt(allowShort=false): Short-Signal wenn close < donchianLow && close < trendEma
- IndicatorSet + Evaluation erweitert um donchianLow; signal: 'long'|'short'|null
- updateChandelierShort: ll + mult×ATR, wandert nur abwärts
- sizePosition(side): Short ignoriert Cash-Cap, stopDist = stop − entry
- Portfolio: highestHigh → trailExtreme; open/close/equity für Shorts; side auf Position + ClosedTrade

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 21:29:30 +00:00

118 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
/** Long: Kosten (qty×fill + Entry-Fee); Short: Netto-Erlös (proceeds Entry-Fee). */
entryCost: number;
initialStop: number;
stop: number;
/** Long: höchstes Hoch seit Entry; Short: tiefstes Tief seit Entry. */
trailExtreme: number;
riskAmount: number;
side: 'long' | 'short';
}
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';
side: 'long' | 'short';
}
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, side: 'long' | 'short' = 'long'): void {
if (this.positions.has(pair)) throw new Error(`open auf bestehende Position: ${pair}`);
if (side === 'short') {
const fill = signalPrice * (1 - this.exec.slippage);
const proceeds = qty * fill;
const fee = proceeds * this.exec.feeRate;
this.cash += proceeds - fee;
this.positions.set(pair, {
pair, qty, entryTs: ts, entryPrice: fill,
entryCost: proceeds - fee, // Netto-Erlös des Leerverkaufs
initialStop, stop: initialStop,
trailExtreme: signalPrice, // Markt-Preis, nicht Fill
riskAmount, side,
});
} else {
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, // Kosten des Kaufs
initialStop, stop: initialStop,
trailExtreme: signalPrice, // Markt-Preis, nicht Fill: der Chandelier trailt Markt-Hochs, Slippage ist Ausführungsartefakt
riskAmount, side,
});
}
}
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}`);
let pnl: number;
let fill: number;
if (pos.side === 'short') {
// Deckungskauf: schlechterer Fill = höherer Preis
fill = exitPrice * (1 + this.exec.slippage);
const cost = pos.qty * fill;
const fee = cost * this.exec.feeRate;
this.cash -= cost + fee;
pnl = pos.entryCost - (cost + fee);
} else {
fill = exitPrice * (1 - this.exec.slippage);
const proceeds = pos.qty * fill;
const fee = proceeds * this.exec.feeRate;
this.cash += proceeds - fee;
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, side: pos.side,
};
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()) {
const last = lastClose.get(pos.pair) ?? pos.entryPrice;
if (pos.side === 'short') {
// Proceeds already in cash; owe qty×last to close
eq -= pos.qty * last;
} else {
eq += pos.qty * last;
}
}
return eq;
}
}