- 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>
118 lines
3.8 KiB
TypeScript
118 lines
3.8 KiB
TypeScript
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;
|
||
}
|
||
}
|