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>
This commit is contained in:
2026-06-09 21:29:30 +00:00
parent 26166c5f3c
commit 0e1b477e27
11 changed files with 289 additions and 30 deletions

View File

@@ -12,11 +12,14 @@ export interface Position {
qty: number;
entryTs: number;
entryPrice: number; // Fill inkl. Slippage
entryCost: number; // qty*fill + Entry-Fee
/** Long: Kosten (qty×fill + Entry-Fee); Short: Netto-Erlös (proceeds Entry-Fee). */
entryCost: number;
initialStop: number;
stop: number;
highestHigh: number;
/** Long: höchstes Hoch seit Entry; Short: tiefstes Tief seit Entry. */
trailExtreme: number;
riskAmount: number;
side: 'long' | 'short';
}
export interface ClosedTrade {
@@ -29,6 +32,7 @@ export interface ClosedTrade {
pnl: number;
r: number;
exitReason: 'trailing_stop' | 'end_of_data';
side: 'long' | 'short';
}
export class Portfolio {
@@ -40,29 +44,57 @@ export class Portfolio {
this.cash = startCapital;
}
open(pair: Pair, ts: number, signalPrice: number, initialStop: number, qty: number, riskAmount: number): void {
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}`);
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, // Markt-Preis, nicht Fill: der Chandelier trailt Markt-Hochs, Slippage ist Ausführungsartefakt
});
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}`);
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;
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,
exitPrice: fill, qty: pos.qty, pnl, r: pnl / pos.riskAmount, exitReason, side: pos.side,
};
this.trades.push(trade);
this.positions.delete(pair);
@@ -72,7 +104,13 @@ export class Portfolio {
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);
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;
}