mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
261 lines
7.3 KiB
261 lines
7.3 KiB
import {
|
|
StrategyRecommendation,
|
|
StrategyStep,
|
|
StrategyStepId
|
|
} from './strategy-flow.types';
|
|
|
|
const STEP_ORDER: StrategyStepId[] = [
|
|
'goals',
|
|
'investmentAmount',
|
|
'timeHorizon',
|
|
'riskAppetite',
|
|
'experience'
|
|
];
|
|
|
|
const STEPS: Record<StrategyStepId, StrategyStep> = {
|
|
goals: {
|
|
stepId: 'goals',
|
|
question: "What's your primary investment goal?",
|
|
options: [
|
|
{
|
|
label: 'Retire Early',
|
|
value: 'retire_early',
|
|
description: 'Build enough wealth to stop working before 60'
|
|
},
|
|
{
|
|
label: 'Grow Wealth',
|
|
value: 'grow_wealth',
|
|
description: 'Maximize long-term portfolio growth'
|
|
},
|
|
{
|
|
label: 'Generate Income',
|
|
value: 'generate_income',
|
|
description: 'Create a steady stream of passive income'
|
|
},
|
|
{
|
|
label: 'Preserve Capital',
|
|
value: 'preserve_capital',
|
|
description: 'Protect what you have from inflation and loss'
|
|
}
|
|
]
|
|
},
|
|
investmentAmount: {
|
|
stepId: 'investmentAmount',
|
|
question: 'How much do you have to invest?',
|
|
options: [
|
|
{
|
|
label: 'Under $1K',
|
|
value: 'under_1k',
|
|
description: 'Just getting started with a small amount'
|
|
},
|
|
{
|
|
label: '$1K - $10K',
|
|
value: '1k_10k',
|
|
description: 'Building a meaningful starter portfolio'
|
|
},
|
|
{
|
|
label: '$10K - $50K',
|
|
value: '10k_50k',
|
|
description: 'Enough for a diversified portfolio'
|
|
},
|
|
{
|
|
label: '$50K - $100K',
|
|
value: '50k_100k',
|
|
description: 'Substantial capital for broad allocation'
|
|
},
|
|
{
|
|
label: '$100K+',
|
|
value: '100k_plus',
|
|
description: 'Significant assets requiring careful management'
|
|
}
|
|
]
|
|
},
|
|
timeHorizon: {
|
|
stepId: 'timeHorizon',
|
|
question: "What's your investment time horizon?",
|
|
options: [
|
|
{
|
|
label: 'Less than 1 year',
|
|
value: 'lt_1y',
|
|
description: 'Short-term — need access to funds soon'
|
|
},
|
|
{
|
|
label: '1 - 5 years',
|
|
value: '1_5y',
|
|
description: 'Medium-term — saving for a goal in a few years'
|
|
},
|
|
{
|
|
label: '5 - 15 years',
|
|
value: '5_15y',
|
|
description: 'Long-term — growing wealth over time'
|
|
},
|
|
{
|
|
label: '15+ years',
|
|
value: '15y_plus',
|
|
description: 'Very long-term — retirement or generational wealth'
|
|
}
|
|
]
|
|
},
|
|
riskAppetite: {
|
|
stepId: 'riskAppetite',
|
|
question: 'How would you feel if your portfolio dropped 20% in a month?',
|
|
options: [
|
|
{
|
|
label: 'Panic sell',
|
|
value: 'panic_sell',
|
|
description: "I'd sell immediately to stop the bleeding"
|
|
},
|
|
{
|
|
label: 'Worried but hold',
|
|
value: 'worried_hold',
|
|
description: "I'd be stressed but wouldn't sell"
|
|
},
|
|
{
|
|
label: 'Stay the course',
|
|
value: 'stay_course',
|
|
description: "Downturns are normal, I'd do nothing"
|
|
},
|
|
{
|
|
label: 'Buy the dip',
|
|
value: 'buy_dip',
|
|
description: "I'd see it as a buying opportunity"
|
|
}
|
|
]
|
|
},
|
|
experience: {
|
|
stepId: 'experience',
|
|
question: 'How experienced are you with investing?',
|
|
options: [
|
|
{
|
|
label: 'Beginner',
|
|
value: 'beginner',
|
|
description: 'New to investing, learning the basics'
|
|
},
|
|
{
|
|
label: 'Some experience',
|
|
value: 'some_experience',
|
|
description: "I've made a few trades or own some funds"
|
|
},
|
|
{
|
|
label: 'Intermediate',
|
|
value: 'intermediate',
|
|
description: 'Comfortable with stocks, ETFs, and diversification'
|
|
},
|
|
{
|
|
label: 'Expert',
|
|
value: 'expert',
|
|
description:
|
|
'Deep knowledge of markets, options, and advanced strategies'
|
|
}
|
|
]
|
|
},
|
|
recommendation: {
|
|
stepId: 'recommendation',
|
|
question: ''
|
|
}
|
|
};
|
|
|
|
export function getStep(stepId: StrategyStepId): StrategyStep {
|
|
return STEPS[stepId];
|
|
}
|
|
|
|
export function getFirstStep(): StrategyStep {
|
|
return STEPS[STEP_ORDER[0]];
|
|
}
|
|
|
|
export function getNextStepId(
|
|
currentStepId: StrategyStepId
|
|
): StrategyStepId | null {
|
|
const idx = STEP_ORDER.indexOf(currentStepId);
|
|
if (idx < 0 || idx >= STEP_ORDER.length - 1) {
|
|
return 'recommendation';
|
|
}
|
|
return STEP_ORDER[idx + 1];
|
|
}
|
|
|
|
export function computeRecommendation(
|
|
answers: Record<string, string>
|
|
): StrategyRecommendation {
|
|
let riskScore = 0;
|
|
|
|
// Goals scoring
|
|
const goalScores: Record<string, number> = {
|
|
retire_early: 2,
|
|
grow_wealth: 3,
|
|
generate_income: 1,
|
|
preserve_capital: 0
|
|
};
|
|
riskScore += goalScores[answers['goals']] ?? 1;
|
|
|
|
// Time horizon scoring
|
|
const horizonScores: Record<string, number> = {
|
|
lt_1y: 0,
|
|
'1_5y': 1,
|
|
'5_15y': 2,
|
|
'15y_plus': 3
|
|
};
|
|
riskScore += horizonScores[answers['timeHorizon']] ?? 1;
|
|
|
|
// Risk appetite scoring
|
|
const riskScores: Record<string, number> = {
|
|
panic_sell: 0,
|
|
worried_hold: 1,
|
|
stay_course: 2,
|
|
buy_dip: 3
|
|
};
|
|
riskScore += riskScores[answers['riskAppetite']] ?? 1;
|
|
|
|
// Experience scoring
|
|
const expScores: Record<string, number> = {
|
|
beginner: 0,
|
|
some_experience: 1,
|
|
intermediate: 2,
|
|
expert: 3
|
|
};
|
|
riskScore += expScores[answers['experience']] ?? 1;
|
|
|
|
// Determine profile (max score = 12)
|
|
if (riskScore <= 4) {
|
|
return {
|
|
title: 'Conservative: Dividend Income Portfolio',
|
|
description:
|
|
'Based on your preference for capital preservation and shorter time horizon, a conservative allocation focused on income-generating assets is recommended.',
|
|
allocations: [
|
|
{ asset: 'Bonds (BND/AGG)', percent: 40, color: '#6b8cae' },
|
|
{ asset: 'Dividend Stocks (VYM/SCHD)', percent: 25, color: '#33ff99' },
|
|
{ asset: 'REITs (VNQ)', percent: 15, color: '#ff6600' },
|
|
{ asset: 'Money Market/Cash', percent: 10, color: '#888' },
|
|
{ asset: 'International Bonds (BNDX)', percent: 10, color: '#ffb84d' }
|
|
],
|
|
riskLevel: 'Conservative'
|
|
};
|
|
} else if (riskScore <= 8) {
|
|
return {
|
|
title: 'Balanced: Core Satellite Strategy',
|
|
description:
|
|
'Your moderate risk tolerance and medium-to-long time horizon suit a balanced approach — broad index funds at the core with selective growth positions.',
|
|
allocations: [
|
|
{ asset: 'US Total Market (VTI)', percent: 35, color: '#33ff99' },
|
|
{ asset: 'International (VXUS)', percent: 20, color: '#6b8cae' },
|
|
{ asset: 'Bonds (BND)', percent: 20, color: '#ffb84d' },
|
|
{ asset: 'Growth ETFs (QQQ/VUG)', percent: 15, color: '#ff6600' },
|
|
{ asset: 'REITs/Alternatives', percent: 10, color: '#888' }
|
|
],
|
|
riskLevel: 'Moderate'
|
|
};
|
|
} else {
|
|
return {
|
|
title: 'Aggressive: Momentum Growth Portfolio',
|
|
description:
|
|
'Your high risk tolerance, long time horizon, and experience support an aggressive growth strategy focused on high-beta assets and sector concentration.',
|
|
allocations: [
|
|
{ asset: 'Growth ETFs (QQQ/VUG)', percent: 30, color: '#ff6600' },
|
|
{ asset: 'US Total Market (VTI)', percent: 25, color: '#33ff99' },
|
|
{ asset: 'Sector ETFs (XLK/XLF)', percent: 20, color: '#ffb84d' },
|
|
{ asset: 'International Growth', percent: 15, color: '#6b8cae' },
|
|
{ asset: 'Small Cap (VB/IWM)', percent: 10, color: '#888' }
|
|
],
|
|
riskLevel: 'Aggressive'
|
|
};
|
|
}
|
|
}
|
|
|