191 lines
7.5 KiB
TypeScript
191 lines
7.5 KiB
TypeScript
// Investor Flow — Derisking Strategy Library (Slice 23)
|
|||
|
|
//
|
||
|
|
// ADR-0007: "winners have flexibility; losers only cut, never average down."
|
||
|
|
// Derisking suggestions are considerations, not instructions. No trade verbs.
|
||
|
|
//
|
||
|
|
// Pure/cache-deterministic: no I/O. Pure logic operating on position + market data.
|
||
|
|
|
||
|
|
// ─── Derisking Strategy Types ───────────────────────────────────────────────
|
||
|
|
|
||
|
|
export type DeriskingStrategyId =
|
||
|
|
| 'scale_out_at_targets'
|
||
|
|
| 'stop_trail_ema21_50'
|
||
|
|
| 'thesis_based_partial'
|
||
|
|
| 'option_protected_collar'
|
||
|
|
| 'regime_cut'
|
||
|
|
| 'correlation_driven';
|
||
|
|
|
||
|
|
export interface DeriskingStrategy {
|
||
|
|
id: DeriskingStrategyId;
|
||
|
|
name: string;
|
||
|
|
description: string;
|
||
|
|
alfredFraming: string;
|
||
|
|
requiresOptionsSleeve: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const DERISKING_STRATEGIES: DeriskingStrategy[] = [
|
||
|
|
{
|
||
|
|
id: 'scale_out_at_targets',
|
||
|
|
name: 'Scale Out at Targets',
|
||
|
|
description: 'Reduce partial position size as price reaches predefined profit targets. This adds flexibility — some position remains if the thesis continues to play out.',
|
||
|
|
alfredFraming: 'Winners have flexibility. Scaling out at targets locks in some gains while leaving room for further upside.',
|
||
|
|
requiresOptionsSleeve: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'stop_trail_ema21_50',
|
||
|
|
name: 'Stop-Trail EMA 21/50',
|
||
|
|
description: 'Trail a stop above the 21-day or 50-day EMA. This adapts the stop to volatility, allowing the position to breathe while protecting gains.',
|
||
|
|
alfredFraming: 'Trailing stops adapt to changing conditions. A static stop ignores new information.',
|
||
|
|
requiresOptionsSleeve: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'thesis_based_partial',
|
||
|
|
name: 'Thesis-Based Partial Reduction',
|
||
|
|
description: 'If the thesis is weakening but not fully invalidated, consider reducing position size rather than fully exiting. This acknowledges uncertainty without abandoning the thesis entirely.',
|
||
|
|
alfredFraming: 'When the thesis weakens but is not broken, flexibility means adjusting size — not all or nothing.',
|
||
|
|
requiresOptionsSleeve: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'option_protected_collar',
|
||
|
|
name: 'Option-Protected Collar',
|
||
|
|
description: 'Buy a protective put and sell a covered call to collar the position. This defines downside risk while partially financing the protection with call premium.',
|
||
|
|
alfredFraming: 'A collar is insurance financed by someone else. You give up some upside to protect the downside.',
|
||
|
|
requiresOptionsSleeve: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'regime_cut',
|
||
|
|
name: 'Regime-Cut Reduction',
|
||
|
|
description: 'When the market regime shifts to trending-down, consider reducing exposure across the portfolio. This is a macro-level derisking response to changed conditions.',
|
||
|
|
alfredFraming: 'When the tide turns, reduce exposure. Fighting a regime shift is a common way winners become losers.',
|
||
|
|
requiresOptionsSleeve: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'correlation_driven',
|
||
|
|
name: 'Correlation-Driven Reduction',
|
||
|
|
description: 'When multiple positions become highly correlated, consider reducing the most correlated ones. Concentrated correlation creates hidden portfolio risk.',
|
||
|
|
alfredFraming: 'When everything moves together, you do not have diversification — you have one bet wearing many masks.',
|
||
|
|
requiresOptionsSleeve: false,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
// ─── Derisking Suggestion Input ─────────────────────────────────────────────
|
||
|
|
|
||
|
|
export interface DeriskingInput {
|
||
|
|
symbol: string;
|
||
|
|
currentPrice: number;
|
||
|
|
avgCost: number;
|
||
|
|
shares: number;
|
||
|
|
ema21?: number;
|
||
|
|
ema50?: number;
|
||
|
|
thesisStatus?: 'intact' | 'weakening' | 'broken';
|
||
|
|
currentRegime?: 'trending-up' | 'trending-down' | 'range-bound';
|
||
|
|
portfolioCorrelation?: number; // 0-1, how correlated this position is with others
|
||
|
|
optionsUnlockState?: number; // 0-4, from ConvexityGate
|
||
|
|
profitTargets?: number[]; // price levels
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface DeriskingSuggestion {
|
||
|
|
strategy: DeriskingStrategy;
|
||
|
|
applicable: boolean;
|
||
|
|
reason: string;
|
||
|
|
details: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Suggestion Engine ──────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
export function suggestDerisking(input: DeriskingInput): DeriskingSuggestion[] {
|
||
|
|
const suggestions: DeriskingSuggestion[] = [];
|
||
|
|
const inProfit = input.currentPrice > input.avgCost;
|
||
|
|
|
||
|
|
for (const strat of DERISKING_STRATEGIES) {
|
||
|
|
const suggestion = evaluateStrategy(strat, input, inProfit);
|
||
|
|
if (suggestion.applicable) {
|
||
|
|
suggestions.push(suggestion);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return suggestions;
|
||
|
|
}
|
||
|
|
|
||
|
|
function evaluateStrategy(
|
||
|
|
strat: DeriskingStrategy,
|
||
|
|
input: DeriskingInput,
|
||
|
|
inProfit: boolean,
|
||
|
|
): DeriskingSuggestion {
|
||
|
|
switch (strat.id) {
|
||
|
|
case 'scale_out_at_targets': {
|
||
|
|
const targets = input.profitTargets ?? [];
|
||
|
|
const hittingTarget = targets.some((t) => input.currentPrice >= t);
|
||
|
|
return {
|
||
|
|
strategy: strat,
|
||
|
|
applicable: inProfit && hittingTarget,
|
||
|
|
reason: hittingTarget
|
||
|
|
? `Price has reached a predefined target. Consider scaling out a portion.`
|
||
|
|
: 'Price has not reached any predefined target.',
|
||
|
|
details: strat.alfredFraming,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
case 'stop_trail_ema21_50': {
|
||
|
|
const hasEMA = input.ema21 !== undefined || input.ema50 !== undefined;
|
||
|
|
const belowEMA = input.ema21 !== undefined && input.currentPrice < input.ema21;
|
||
|
|
return {
|
||
|
|
strategy: strat,
|
||
|
|
applicable: hasEMA && inProfit,
|
||
|
|
reason: belowEMA
|
||
|
|
? `Price has fallen below EMA21. A trailing stop may help protect gains.`
|
||
|
|
: 'Consider trailing a stop above EMA21 or EMA50 to protect gains while allowing the position to breathe.',
|
||
|
|
details: strat.alfredFraming,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
case 'thesis_based_partial': {
|
||
|
|
const weakening = input.thesisStatus === 'weakening';
|
||
|
|
return {
|
||
|
|
strategy: strat,
|
||
|
|
applicable: weakening,
|
||
|
|
reason: weakening
|
||
|
|
? 'Thesis is weakening but not broken. Consider a partial reduction rather than a full exit.'
|
||
|
|
: 'Thesis is not weakening.',
|
||
|
|
details: strat.alfredFraming,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
case 'option_protected_collar': {
|
||
|
|
const hasOptionsSleeve = (input.optionsUnlockState ?? 0) >= 3;
|
||
|
|
return {
|
||
|
|
strategy: strat,
|
||
|
|
applicable: hasOptionsSleeve && inProfit,
|
||
|
|
reason: hasOptionsSleeve
|
||
|
|
? 'Options insurance sleeve is unlocked. A collar may define downside risk while partially financing protection.'
|
||
|
|
: 'Options insurance sleeve is not unlocked. Collar strategy requires the insurance sleeve tier.',
|
||
|
|
details: strat.alfredFraming,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
case 'regime_cut': {
|
||
|
|
const regimeDown = input.currentRegime === 'trending-down';
|
||
|
|
return {
|
||
|
|
strategy: strat,
|
||
|
|
applicable: regimeDown,
|
||
|
|
reason: regimeDown
|
||
|
|
? 'Market regime has shifted to trending-down. Consider reducing exposure across the portfolio.'
|
||
|
|
: 'Market regime is not trending-down.',
|
||
|
|
details: strat.alfredFraming,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
case 'correlation_driven': {
|
||
|
|
const highCorr = (input.portfolioCorrelation ?? 0) > 0.7;
|
||
|
|
return {
|
||
|
|
strategy: strat,
|
||
|
|
applicable: highCorr,
|
||
|
|
reason: highCorr
|
||
|
|
? `Position correlation is ${(input.portfolioCorrelation! * 100).toFixed(0)}%. Consider reducing the most correlated positions.`
|
||
|
|
: 'Position correlation is within acceptable range.',
|
||
|
|
details: strat.alfredFraming,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|