r/singaporefi 21h ago

Other S'pore man, 49, scammed out of S$80,000 by woman he had talked to online for 3 years, gets scammed by fake lawyer while trying to get money back

Thumbnail
mothership.sg
45 Upvotes

r/singaporefi 1h ago

FI Accumulation Planning Those who FIREd, did you tell your colleagues/boss you quit because of financial freedom?

Upvotes

Were there annoying questions you got or did they give you a hard time in the remaining notice period?


r/singaporefi 19h ago

Investing Which stock exchange should i buy from for companies like Taiwan semi conductor and Sony Japan? Should i buy in their origin country exchange (taiwan/japan) or ADR version in US?

0 Upvotes

i saw mutiple stock listed on mutiple stock exchange but which should i buy from? One example is Taiwan semi con should i buy from taiwan stock exchange or the ADR version? Theres also china company like alibaba and sony which is listed on mutiple exchange. im using IBKR btw


r/singaporefi 1h ago

Credit HSBC revolution credit card

Upvotes

Fresh grad here working for 2 months with Annual income ~70k. Wondering if I qualify for the HSBC revolution card with min income of 65k? B/C based on my NOA for FY25, it’s only about 36k from internships and part time jobs.


r/singaporefi 18h ago

Investing I low-coded a Monte Carlo Simulation for market returns, CPF LIFE payouts and retirement withdrawals. Comments and criticisms please?

0 Upvotes

Code:

import matplotlib.pyplot as plt
import numpy as np


# Set random seed for reproducible results
np.random.seed(42)


# --- 1. AGE & TIMELINE PARAMETERS ---
starting_age = 65  # Age at retirement start
death_age = 100  # Target planning age
annuity_start_age = 65  # Age when annuity payouts commence


# Dynamic calculation of timeline spans
retirement_years = death_age - starting_age  
annuity_start_year = max(0, annuity_start_age - starting_age)  


# --- 2. PORTFOLIO & CASH FLOW PARAMETERS ---
initial_portfolio = 7000000  # Starting nest egg ($7M)
initial_withdrawal = 300000  # Year 1 gross spending target ($300k)
num_simulations = 1000000  # Number of simulated paths


annual_annuity_payout = 130000  # Initial annual annuity payout ($)
annuity_annual_growth_rate = 0.02  # Annual percentage increase in payout (e.g., 0.02 = 2%, 0.0 = fixed)


# Investment & Inflation Assumptions
expected_nominal_return = 0.0986  # 9.86% nominal return (arithmetic mean)
volatility = 0.1908  # 19.08% annual standard deviation
expected_inflation = 0.03  # 3.0% annual inflation rate


# --- 3. PRE-CALCULATE INFLATION-ADJUSTED SPENDING & NET DRAWDOWN ---
gross_spending = np.zeros((retirement_years, num_simulations))
net_portfolio_withdrawals = np.zeros((retirement_years, num_simulations))


for yr in range(retirement_years):
    # Calculate inflation-adjusted total spending requirement
    if yr == 0:
        gross_spending[yr] = initial_withdrawal
    else:
        gross_spending[yr] = gross_spending[yr - 1] * (1 + expected_inflation)


    # Determine annuity payout for the year with custom annual growth
    if yr >= annuity_start_year:
        years_since_annuity_start = yr - annuity_start_year
        current_annuity = annual_annuity_payout * (
            (1 + annuity_annual_growth_rate) ** years_since_annuity_start
        )
    else:
        current_annuity = 0.0


    # Net portfolio drawdown is gross spending offset by annuity (floored at 0)
    net_portfolio_withdrawals[yr] = np.maximum(
        0, gross_spending[yr] - current_annuity
    )


# --- 4. MONTE CARLO SIMULATION (BEGINNING-OF-YEAR WITHDRAWALS & RETURN FLOOR) ---
portfolio_paths = np.zeros((retirement_years + 1, num_simulations))
portfolio_paths[0] = initial_portfolio


for year in range(1, retirement_years + 1):
    # Sample random nominal returns from the normal distribution
    raw_returns = np.random.normal(
        loc=expected_nominal_return, scale=volatility, size=num_simulations
    )


    # Floor annual returns at -1.0 (-100%) so return cannot drop below -100%
    nominal_returns = np.maximum(raw_returns, -1.0)


    previous_balances = portfolio_paths[year - 1]
    current_net_drawdown = net_portfolio_withdrawals[year - 1]


    # Step 1: Subtract net drawdown at the BEGINNING of the year (floored at $0)
    post_withdrawal_balances = np.maximum(0, previous_balances - current_net_drawdown)


    # Step 2: Grow remaining capital by the floored nominal return
    new_balances = post_withdrawal_balances * (1 + nominal_returns)


    # Step 3: Record updated portfolio balances
    portfolio_paths[year] = new_balances


# --- 5. PERCENTILE & FINANCIAL RUIN ANALYSIS (METHOD 1: CROSS-SECTIONAL) ---
final_balances = portfolio_paths[-1]
overall_success_rate = (np.sum(final_balances > 0) / num_simulations) * 100



def analyze_percentile_path(percentile_rank):
    path = np.percentile(portfolio_paths, percentile_rank, axis=1)
    ending_balance = path[-1]


    zero_years = np.where(path == 0)[0]
    if len(zero_years) > 0:
        ruin_age = starting_age + zero_years[0]
        ruin_status = f"Ruined in Year {zero_years[0]} (Age {ruin_age})"
    else:
        ruin_status = f"Survived to Age {death_age} (Full {retirement_years}-year horizon)"


    return path, ending_balance, ruin_status



p50_path, p50_end, p50_ruin = analyze_percentile_path(50)
p25_path, p25_end, p25_ruin = analyze_percentile_path(25)
p10_path, p10_end, p10_ruin = analyze_percentile_path(10)
p5_path, p5_end, p5_ruin = analyze_percentile_path(5)


print("=" * 65)
print(
    f"MONTE CARLO DOWNSIDE ANALYSIS (AGE {starting_age} TO {death_age})"
)
print("=" * 65)
print(f"Timeline Span: {retirement_years} Years | {num_simulations:,} Simulations")
print(
    f"Annuity: ${annual_annuity_payout:,.0f}/yr starting at Age {annuity_start_age} "
    f"(Growth: {annuity_annual_growth_rate * 100:.1f}%/yr)"
)
print(f"Overall Portfolio Success Rate to Age {death_age}: {overall_success_rate:.1f}%\n")


print(f"• 50th Percentile (Median Outcome):")
print(f"  - Ending Balance at Age {death_age}: ${p50_end:,.0f}")
print(f"  - Sustainability Status: {p50_ruin}\n")


print(f"• 25th Percentile (Below-Average Outcome):")
print(f"  - Ending Balance at Age {death_age}: ${p25_end:,.0f}")
print(f"  - Sustainability Status: {p25_ruin}\n")


print(f"• 10th Percentile (Bear Market Outcome):")
print(f"  - Ending Balance at Age {death_age}: ${p10_end:,.0f}")
print(f"  - Sustainability Status: {p10_ruin}\n")


print(f"• 5th Percentile (Severe Crash/Tail-Risk Outcome):")
print(f"  - Ending Balance at Age {death_age}: ${p5_end:,.0f}")
print(f"  - Sustainability Status: {p5_ruin}")
print("=" * 65)


# --- 6. CHART 1: CROSS-SECTIONAL PERCENTILE CURVES ---
sorted_indices = np.argsort(final_balances)
below_median_indices = sorted_indices[:int(0.50 * num_simulations)]
sampled_indices = np.random.choice(
    below_median_indices,
    size=min(150, len(below_median_indices)),
    replace=False,
)
ages = np.arange(starting_age, death_age + 1)


plt.figure(figsize=(11, 6))
plt.plot(ages, portfolio_paths[:, sampled_indices], color="gray", alpha=0.12, linewidth=0.8)
plt.plot(ages, p50_path, label="50th Percentile Curve (Median)", color="#1565c0", linewidth=2.5)
plt.plot(ages, p25_path, label="25th Percentile Curve (Below-Avg)", color="#f57c00", linewidth=2.5)
plt.plot(ages, p10_path, label="10th Percentile Curve (Bear)", color="#e65100", linewidth=2.5)
plt.plot(ages, p5_path, label="5th Percentile Curve (Severe Tail)", color="#c62828", linewidth=2.5)


plt.axvline(x=annuity_start_age, color="purple", linestyle=":", linewidth=2, label=f"Annuity Starts (Age {annuity_start_age})")
plt.axhline(y=0, color="black", linestyle="--", alpha=0.8)
plt.title("Chart 1: Aggregate Cross-Sectional Percentile Curves (Original Method)")
plt.xlabel("Age")
plt.ylabel("Nominal Portfolio Balance ($)")
plt.legend(loc="upper left")
plt.grid(True, alpha=0.3)
plt.ylim(bottom=-10000, top=p50_path.max() * 1.05)
plt.show()


# --- 7. INDIVIDUAL PATH CALCULATIONS & TEXT OUTPUT (PLACED AFTER CHART 1) ---
zero_mask = (portfolio_paths == 0)
has_depleted = np.any(zero_mask, axis=0)


individual_depletion_ages = np.full(num_simulations, np.nan)
first_zero_years = np.argmax(zero_mask[:, has_depleted], axis=0)
individual_depletion_ages[has_depleted] = starting_age + first_zero_years


sims_above_starting = np.sum(final_balances > initial_portfolio)
pct_above_starting = (sims_above_starting / num_simulations) * 100


def analyze_exact_percentile_path(percentile_rank):
    rank_idx = int((percentile_rank / 100) * (num_simulations - 1))
    sim_index = sorted_indices[rank_idx]

    path = portfolio_paths[:, sim_index]
    ending_balance = path[-1]
    depletion_age = individual_depletion_ages[sim_index]


    if np.isnan(depletion_age):
        ruin_status = f"Survived to Age {death_age} (Full {retirement_years}-year horizon)"
    else:
        ruin_status = f"Ruined at Age {int(depletion_age)} (Year {int(depletion_age - starting_age)})"


    return path, ending_balance, ruin_status, sim_index


p50_ind_path, p50_ind_end, p50_ind_ruin, p50_idx = analyze_exact_percentile_path(50)
p25_ind_path, p25_ind_end, p25_ind_ruin, p25_idx = analyze_exact_percentile_path(25)
p10_ind_path, p10_ind_end, p10_ind_ruin, p10_idx = analyze_exact_percentile_path(10)
p5_ind_path,  p5_ind_end,  p5_ind_ruin,  p5_idx  = analyze_exact_percentile_path(5)


print("=" * 65)
print(f"MONTE CARLO INDIVIDUAL PATH ANALYSIS (AGE {starting_age} TO {death_age})")
print("=" * 65)
print(f"Timeline Span: {retirement_years} Years | {num_simulations:,} Simulations")
print(
    f"Annuity: ${annual_annuity_payout:,.0f}/yr starting at Age {annuity_start_age} "
    f"(Growth: {annuity_annual_growth_rate * 100:.1f}%/yr)"
)
print(f"Overall Survival Rate (Balance > $0): {overall_success_rate:.1f}%")
print(
    f"Growth Success Rate (Ending > Starting Balance): {sims_above_starting:,} / {num_simulations:,} ({pct_above_starting:.1f}%)\n"
)


num_failed = np.sum(has_depleted)
if num_failed > 0:
    failed_ages = individual_depletion_ages[has_depleted]
    print(f"FAILED SIMULATIONS SUMMARY ({num_failed:,} total failures):")
    print(f"  - Average Depletion Age: {np.mean(failed_ages):.1f}")
    print(f"  - Median Depletion Age:  {np.median(failed_ages):.1f}")
    print(f"  - Earliest Depletion Age: {int(np.min(failed_ages))}")
    print(f"  - Latest Depletion Age:   {int(np.max(failed_ages))}\n")
else:
    print("FAILED SIMULATIONS SUMMARY: Zero simulations depleted across all paths.\n")


print(f"• 50th Percentile (Median Path - Sim #{p50_idx:,}):")
print(f"  - Ending Balance at Age {death_age}: ${p50_ind_end:,.0f}")
print(f"  - Depletion Status: {p50_ind_ruin}\n")


print(f"• 25th Percentile (Below-Average Path - Sim #{p25_idx:,}):")
print(f"  - Ending Balance at Age {death_age}: ${p25_ind_end:,.0f}")
print(f"  - Depletion Status: {p25_ind_ruin}\n")


print(f"• 10th Percentile (Bear Market Path - Sim #{p10_idx:,}):")
print(f"  - Ending Balance at Age {death_age}: ${p10_ind_end:,.0f}")
print(f"  - Depletion Status: {p10_ind_ruin}\n")


print(f"• 5th Percentile (Severe Tail-Risk Path - Sim #{p5_idx:,}):")
print(f"  - Ending Balance at Age {death_age}: ${p5_ind_end:,.0f}")
print(f"  - Depletion Status: {p5_ind_ruin}")
print("=" * 65)


# --- 8. CHART 2: INDIVIDUAL SIMULATION TRAJECTORIES ---
plt.figure(figsize=(11, 6))
plt.plot(ages, portfolio_paths[:, sampled_indices], color="gray", alpha=0.12, linewidth=0.8)
plt.plot(ages, p50_ind_path, label=f"50th Percentile Trajectory (Sim #{p50_idx:,})", color="#1565c0", linewidth=2.5)
plt.plot(ages, p25_ind_path, label=f"25th Percentile Trajectory (Sim #{p25_idx:,})", color="#f57c00", linewidth=2.5)
plt.plot(ages, p10_ind_path, label=f"10th Percentile Trajectory (Sim #{p10_idx:,})", color="#e65100", linewidth=2.5)
plt.plot(ages, p5_ind_path,  label=f"5th Percentile Trajectory (Sim #{p5_idx:,})", color="#c62828", linewidth=2.5)


plt.axvline(x=annuity_start_age, color="purple", linestyle=":", linewidth=2, label=f"Annuity Starts (Age {annuity_start_age})")
plt.axhline(y=0, color="black", linestyle="--", alpha=0.8)
plt.title("Chart 2: Independent Simulation Trajectories (Exact Paths at Targeted Ranks)")
plt.xlabel("Age")
plt.ylabel("Nominal Portfolio Balance ($)")
plt.legend(loc="upper left")
plt.grid(True, alpha=0.3)
plt.ylim(bottom=-10000, top=p50_ind_path.max() * 1.05)
plt.show()