finance-planning / src /scenarios.py
HsiehMinChieh
Fix Python 3.9 type hint compatibility (remove | None syntax)
1c4669e
"""情境模擬模組 - Scenario Simulation Module.
This module handles financial scenario simulations for the urban renewal
project, including delay impacts, cost changes, and interest rate adjustments.
"""
from dataclasses import dataclass
from typing import Any, Optional
import pandas as pd
@dataclass
class ScenarioParams:
"""Parameters for scenario simulation."""
name: str
delay_months: int = 0
cost_increase_pct: float = 0.0
rate_adjustment: float = 0.0
market_price_change: float = 0.0
@dataclass
class ScenarioResult:
"""Results from scenario simulation."""
scenario_name: str
base_profit: float
adjusted_profit: float
profit_change: float
profit_change_pct: float
delay_cost: float
cost_increase: float
interest_impact: float
market_impact: float
monthly_cashflow: list[float]
irr: float
break_even_month: Optional[int]
def simulate_scenario(
base_income: float,
base_expense: float,
loan_amount: float,
base_rate: float,
monthly_cashflow: list[float],
params: ScenarioParams,
) -> ScenarioResult:
"""Simulate the impact of a scenario on project finances.
Args:
base_income: Base total income.
base_expense: Base total expense.
loan_amount: Total loan amount.
base_rate: Base annual interest rate (%).
monthly_cashflow: List of monthly net cashflow values.
params: Scenario parameters.
Returns:
ScenarioResult: Simulation results.
"""
base_profit = base_income - base_expense
# 1. 延遲成本:每延遲一個月的利息支出
monthly_interest = loan_amount * (base_rate / 100) / 12
delay_cost = params.delay_months * monthly_interest
# 2. 成本上漲影響
cost_increase = base_expense * (params.cost_increase_pct / 100)
# 3. 利率變動影響 (假設貸款期間 36 個月)
new_rate = base_rate + params.rate_adjustment
old_total_interest = loan_amount * (base_rate / 100) * 3 # 3 年
new_total_interest = loan_amount * (new_rate / 100) * 3
interest_impact = new_total_interest - old_total_interest
# 4. 市場價格變動
market_impact = base_income * (params.market_price_change / 100)
# 計算調整後淨利
adjusted_profit = (
base_profit
- delay_cost
- cost_increase
- interest_impact
+ market_impact
)
profit_change = adjusted_profit - base_profit
profit_change_pct = (profit_change / base_profit * 100) if base_profit != 0 else 0
# 調整月度現金流 (模擬延遲影響)
adjusted_cashflow = monthly_cashflow.copy()
if params.delay_months > 0 and len(adjusted_cashflow) > 0:
# 延遲期間仍有利息支出
for i in range(min(params.delay_months, len(adjusted_cashflow))):
if i < len(adjusted_cashflow):
adjusted_cashflow[i] -= monthly_interest
# 計算損益平衡月份
cumulative = 0.0
break_even_month = None
for i, cf in enumerate(adjusted_cashflow):
cumulative += cf
if cumulative > 0 and break_even_month is None:
break_even_month = i + 1
# Placeholder for IRR calculation
irr_value = 0.0 # This should be calculated based on adjusted_cashflow and initial investment
return ScenarioResult(
scenario_name=params.name,
base_profit=base_profit,
adjusted_profit=adjusted_profit,
profit_change=profit_change,
profit_change_pct=profit_change_pct,
delay_cost=delay_cost,
cost_increase=cost_increase,
interest_impact=interest_impact,
market_impact=market_impact,
monthly_cashflow=adjusted_cashflow,
irr=irr_value,
break_even_month=break_even_month,
)
def run_sensitivity_analysis(
base_income: float,
base_expense: float,
loan_amount: float,
base_rate: float,
) -> pd.DataFrame:
"""Run sensitivity analysis across multiple scenarios.
Args:
base_income: Base total income.
base_expense: Base total expense.
loan_amount: Total loan amount.
base_rate: Base annual interest rate (%).
Returns:
pd.DataFrame: Sensitivity analysis results.
"""
base_profit = base_income - base_expense
scenarios = [
# 延遲情境
("銷售延遲 3 個月", 3, 0, 0, 0),
("銷售延遲 6 個月", 6, 0, 0, 0),
("銷售延遲 12 個月", 12, 0, 0, 0),
# 成本情境
("成本上漲 5%", 0, 5, 0, 0),
("成本上漲 10%", 0, 10, 0, 0),
("成本上漲 15%", 0, 15, 0, 0),
# 利率情境
("利率上升 0.5%", 0, 0, 0.5, 0),
("利率上升 1.0%", 0, 0, 1.0, 0),
("利率上升 1.5%", 0, 0, 1.5, 0),
# 市場情境
("房價下跌 5%", 0, 0, 0, -5),
("房價下跌 10%", 0, 0, 0, -10),
# 綜合情境
("悲觀情境", 6, 10, 1.0, -5),
("最悲觀情境", 12, 15, 1.5, -10),
]
results = []
for name, delay, cost, rate, market in scenarios:
params = ScenarioParams(
name=name,
delay_months=delay,
cost_increase_pct=cost,
rate_adjustment=rate,
market_price_change=market,
)
result = simulate_scenario(
base_income, base_expense, loan_amount, base_rate, [], params
)
results.append({
"情境": name,
"調整後淨利 (萬)": round(result.adjusted_profit, 0),
"淨利變動 (萬)": round(result.profit_change, 0),
"變動幅度 (%)": round(result.profit_change_pct, 1),
})
return pd.DataFrame(results)
def calculate_break_even_analysis(
fixed_cost: float,
variable_cost_per_unit: float,
selling_price_per_unit: float,
total_units: int,
) -> dict[str, Any]:
"""Calculate break-even analysis.
Args:
fixed_cost: Total fixed costs.
variable_cost_per_unit: Variable cost per unit.
selling_price_per_unit: Selling price per unit.
total_units: Total sellable units.
Returns:
dict: Break-even analysis results.
"""
contribution_margin = selling_price_per_unit - variable_cost_per_unit
if contribution_margin <= 0:
return {
"break_even_units": None,
"break_even_revenue": None,
"margin_of_safety": 0,
"error": "貢獻邊際為負,無法達到損益平衡",
}
break_even_units = fixed_cost / contribution_margin
break_even_revenue = break_even_units * selling_price_per_unit
total_revenue = total_units * selling_price_per_unit
margin_of_safety = ((total_revenue - break_even_revenue) / total_revenue * 100) if total_revenue > 0 else 0
return {
"break_even_units": round(break_even_units, 1),
"break_even_revenue": round(break_even_revenue, 0),
"margin_of_safety": round(margin_of_safety, 1),
"contribution_margin": contribution_margin,
}
def calculate_irr_approximation(
initial_investment: float,
monthly_cashflows: list[float],
) -> Optional[float]:
"""Calculate approximate IRR using Newton-Raphson method.
Args:
initial_investment: Initial investment (negative value).
monthly_cashflows: List of monthly cash flows.
Returns:
float: Approximate IRR as annual percentage, or None if not converging.
"""
if not monthly_cashflows:
return None
# 將月度現金流轉換為年度
annual_cashflows = []
for i in range(0, len(monthly_cashflows), 12):
annual_cashflows.append(sum(monthly_cashflows[i : i + 12]))
# 加入初始投資
cashflows = [-initial_investment] + annual_cashflows
# Newton-Raphson 迭代
rate = 0.1 # 初始猜測 10%
for _ in range(100): # 最多 100 次迭代
npv = sum(cf / (1 + rate) ** i for i, cf in enumerate(cashflows))
npv_derivative = sum(
-i * cf / (1 + rate) ** (i + 1) for i, cf in enumerate(cashflows)
)
if abs(npv_derivative) < 1e-10:
break
new_rate = rate - npv / npv_derivative
if abs(new_rate - rate) < 1e-6:
return round(new_rate * 100, 2)
rate = new_rate
return round(rate * 100, 2) if -1 < rate < 10 else None