Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-06-12 16:39:48 +08:00
commit 3d67e8d1e5
16 changed files with 5482 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
+1
View File
@@ -0,0 +1 @@
3.12
View File
+106
View File
@@ -0,0 +1,106 @@
import logfire
from pydantic_ai.models.openai import OpenAIResponsesModel
from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai import Agent
from pydantic_ai.capabilities import MCP, Thinking, ToolSearch, WebSearch
from pydantic_ai_harness import CodeMode
# Community packages, alphabetical:
from pydantic_ai_backends import ConsoleCapability
from pydantic_ai_shields import CostTracking, InputGuard, SecretRedaction, ToolGuard
from pydantic_ai_skills import SkillsCapability
from pydantic_ai_summarization import ContextManagerCapability
from pydantic_ai_todo import TodoCapability
from pydantic_deep import MemoryCapability, StuckLoopDetection
from pydantic_deep.deps import DeepAgentDeps
from subagents_pydantic_ai import SubAgentCapability, SubAgentConfig
# See https://ai.pydantic.dev/logfire/ for setup details.
logfire.configure()
logfire.instrument_pydantic_ai()
model = OpenAIResponsesModel(
"llama",
provider=OpenAIProvider(base_url="http://localhost:8011/v1"),
)
agent = Agent(
model,
capabilities=[
# --- Tool execution & discovery ---
# Wraps every tool into a single run_code, sandboxed by Monty.
CodeMode(),
# Progressive tool discovery for large tool sets; discovered tools fold into run_code.
ToolSearch(),
# --- Reasoning ---
# Provider-adaptive thinking; uses native extended thinking on supporting models.
Thinking(effort="xhigh"),
# --- Context management ---
# Sliding window + LLM compaction. By @vstorm-co:
# https://github.com/vstorm-co/summarization-pydantic-ai
# Pydantic AI also ships `AnthropicCompaction` and `OpenAICompaction` for
# provider-native compaction.
ContextManagerCapability(max_tokens=100_000),
# --- Tools ---
# Connect to any MCP server -- here, the open-source Hacker News server
# (https://github.com/cyanheads/hn-mcp-server).
MCP("https://hn.caseyjhand.com/mcp"),
# Provider-adaptive web search; falls back to a local DuckDuckGo implementation.
WebSearch(),
# Filesystem + shell. By @vstorm-co: https://github.com/vstorm-co/pydantic-ai-backend
ConsoleCapability(),
# --- Memory & persistence ---
# Persistent ./MEMORY.md per agent name. By @vstorm-co:
# https://github.com/vstorm-co/pydantic-deepagents
MemoryCapability(agent_name="harness-example"),
# --- Orchestration ---
# Agent skills (Anthropic's spec) by @DougTrajano:
# https://github.com/DougTrajano/pydantic-ai-skills
# @vstorm-co's pydantic-deep also offers skills loading; the two have different
# spec footprints (Doug's is closer to programmatic skills).
SkillsCapability(directories=["./skills"]),
# Spawn sub-agents with their own toolsets and instructions. By @vstorm-co:
# https://github.com/vstorm-co/subagents-pydantic-ai
SubAgentCapability(
subagents=[
SubAgentConfig(
name="researcher",
description="Deep research on a topic",
instructions="You are a thorough research assistant.",
),
]
),
# Track tasks and subtasks; in-memory by default, AsyncPostgresStorage available.
# By @vstorm-co: https://github.com/vstorm-co/pydantic-ai-todo
TodoCapability(enable_subtasks=True),
# --- Safety & reliability ---
# The next four are by @vstorm-co: https://github.com/vstorm-co/pydantic-ai-shields
# Per-run cost cap with a callback hook.
CostTracking(budget_usd=5.0),
# Reject prompts that look like prompt-injection attempts.
InputGuard(guard=lambda p: "ignore previous instructions" not in p.lower()),
# Block or require approval per tool name.
ToolGuard(blocked=["rm"], require_approval=["write_file"]),
# Detect API keys/tokens in tool I/O and redact before they reach the model.
SecretRedaction(),
# Bail out if the agent gets stuck calling the same tools in a loop.
# By @vstorm-co: https://github.com/vstorm-co/pydantic-deepagents
StuckLoopDetection(),
],
)
async def main() -> None:
deps = DeepAgentDeps()
result = await agent.run(
"What are your skills.",
# "Find the latest HN stories about AI, pull their comment threads, and summarize the discussions.",
deps=deps,
)
print(result.output)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
+16
View File
@@ -0,0 +1,16 @@
[project]
name = "agent-alpha"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"logfire[asyncpg,fastapi,httpx,sqlite3]>=4.36.0",
"pydantic-ai-backend>=0.2.11",
"pydantic-ai-harness[code-mode]>=0.3.0",
"pydantic-ai-shields>=0.3.4",
"pydantic-ai-skills>=0.11.0",
"pydantic-ai-slim[duckduckgo,mcp,openai]>=1.107.0",
"pydantic-ai-todo>=0.2.4",
"pydantic-deep>=0.3.28",
]
@@ -0,0 +1,69 @@
---
name: analyzing-financial-statements
description: This skill calculates key financial ratios and metrics from financial statement data for investment analysis
---
# Financial Ratio Calculator Skill
This skill provides comprehensive financial ratio analysis for evaluating company performance, profitability, liquidity, and valuation.
## Capabilities
Calculate and interpret:
- **Profitability Ratios**: ROE, ROA, Gross Margin, Operating Margin, Net Margin
- **Liquidity Ratios**: Current Ratio, Quick Ratio, Cash Ratio
- **Leverage Ratios**: Debt-to-Equity, Interest Coverage, Debt Service Coverage
- **Efficiency Ratios**: Asset Turnover, Inventory Turnover, Receivables Turnover
- **Valuation Ratios**: P/E, P/B, P/S, EV/EBITDA, PEG
- **Per-Share Metrics**: EPS, Book Value per Share, Dividend per Share
## How to Use
1. **Input Data**: Provide financial statement data (income statement, balance sheet, cash flow)
2. **Select Ratios**: Specify which ratios to calculate or use "all" for comprehensive analysis
3. **Interpretation**: The skill will calculate ratios and provide industry-standard interpretations
## Input Format
Financial data can be provided as:
- CSV with financial line items
- JSON with structured financial statements
- Text description of key financial figures
- Excel files with financial statements
## Output Format
Results include:
- Calculated ratios with values
- Industry benchmark comparisons (when available)
- Trend analysis (if multiple periods provided)
- Interpretation and insights
- Excel report with formatted results
## Example Usage
"Calculate key financial ratios for this company based on the attached financial statements"
"What's the P/E ratio if the stock price is $50 and annual earnings are $2.50 per share?"
"Analyze the liquidity position using the balance sheet data"
## Scripts
- `calculate_ratios.py`: Main calculation engine for all financial ratios
- `interpret_ratios.py`: Provides interpretation and benchmarking
## Best Practices
1. Always validate data completeness before calculations
2. Handle missing values appropriately (use industry averages or exclude)
3. Consider industry context when interpreting ratios
4. Include period comparisons for trend analysis
5. Flag unusual or concerning ratios
## Limitations
- Requires accurate financial data
- Industry benchmarks are general guidelines
- Some ratios may not apply to all industries
- Historical data doesn't guarantee future performance
@@ -0,0 +1,346 @@
"""
Financial ratio calculation module.
Provides functions to calculate key financial metrics and ratios.
"""
import json
from typing import Any
class FinancialRatioCalculator:
"""Calculate financial ratios from financial statement data."""
def __init__(self, financial_data: dict[str, Any]):
"""
Initialize with financial statement data.
Args:
financial_data: Dictionary containing income_statement, balance_sheet,
cash_flow, and market_data
"""
self.income_statement = financial_data.get("income_statement", {})
self.balance_sheet = financial_data.get("balance_sheet", {})
self.cash_flow = financial_data.get("cash_flow", {})
self.market_data = financial_data.get("market_data", {})
self.ratios = {}
def safe_divide(self, numerator: float, denominator: float, default: float = 0.0) -> float:
"""Safely divide two numbers, returning default if denominator is zero."""
if denominator == 0:
return default
return numerator / denominator
def calculate_profitability_ratios(self) -> dict[str, float]:
"""Calculate profitability ratios."""
ratios = {}
# ROE (Return on Equity)
net_income = self.income_statement.get("net_income", 0)
shareholders_equity = self.balance_sheet.get("shareholders_equity", 0)
ratios["roe"] = self.safe_divide(net_income, shareholders_equity)
# ROA (Return on Assets)
total_assets = self.balance_sheet.get("total_assets", 0)
ratios["roa"] = self.safe_divide(net_income, total_assets)
# Gross Margin
revenue = self.income_statement.get("revenue", 0)
cogs = self.income_statement.get("cost_of_goods_sold", 0)
gross_profit = revenue - cogs
ratios["gross_margin"] = self.safe_divide(gross_profit, revenue)
# Operating Margin
operating_income = self.income_statement.get("operating_income", 0)
ratios["operating_margin"] = self.safe_divide(operating_income, revenue)
# Net Margin
ratios["net_margin"] = self.safe_divide(net_income, revenue)
return ratios
def calculate_liquidity_ratios(self) -> dict[str, float]:
"""Calculate liquidity ratios."""
ratios = {}
current_assets = self.balance_sheet.get("current_assets", 0)
current_liabilities = self.balance_sheet.get("current_liabilities", 0)
# Current Ratio
ratios["current_ratio"] = self.safe_divide(current_assets, current_liabilities)
# Quick Ratio (Acid Test)
inventory = self.balance_sheet.get("inventory", 0)
quick_assets = current_assets - inventory
ratios["quick_ratio"] = self.safe_divide(quick_assets, current_liabilities)
# Cash Ratio
cash = self.balance_sheet.get("cash_and_equivalents", 0)
ratios["cash_ratio"] = self.safe_divide(cash, current_liabilities)
return ratios
def calculate_leverage_ratios(self) -> dict[str, float]:
"""Calculate leverage/solvency ratios."""
ratios = {}
total_debt = self.balance_sheet.get("total_debt", 0)
shareholders_equity = self.balance_sheet.get("shareholders_equity", 0)
# Debt-to-Equity Ratio
ratios["debt_to_equity"] = self.safe_divide(total_debt, shareholders_equity)
# Interest Coverage Ratio
ebit = self.income_statement.get("ebit", 0)
interest_expense = self.income_statement.get("interest_expense", 0)
ratios["interest_coverage"] = self.safe_divide(ebit, interest_expense)
# Debt Service Coverage Ratio
net_operating_income = self.income_statement.get("operating_income", 0)
total_debt_service = interest_expense + self.balance_sheet.get(
"current_portion_long_term_debt", 0
)
ratios["debt_service_coverage"] = self.safe_divide(net_operating_income, total_debt_service)
return ratios
def calculate_efficiency_ratios(self) -> dict[str, float]:
"""Calculate efficiency/activity ratios."""
ratios = {}
revenue = self.income_statement.get("revenue", 0)
total_assets = self.balance_sheet.get("total_assets", 0)
# Asset Turnover
ratios["asset_turnover"] = self.safe_divide(revenue, total_assets)
# Inventory Turnover
cogs = self.income_statement.get("cost_of_goods_sold", 0)
inventory = self.balance_sheet.get("inventory", 0)
ratios["inventory_turnover"] = self.safe_divide(cogs, inventory)
# Receivables Turnover
accounts_receivable = self.balance_sheet.get("accounts_receivable", 0)
ratios["receivables_turnover"] = self.safe_divide(revenue, accounts_receivable)
# Days Sales Outstanding
ratios["days_sales_outstanding"] = self.safe_divide(365, ratios["receivables_turnover"])
return ratios
def calculate_valuation_ratios(self) -> dict[str, float]:
"""Calculate valuation ratios."""
ratios = {}
share_price = self.market_data.get("share_price", 0)
shares_outstanding = self.market_data.get("shares_outstanding", 0)
market_cap = share_price * shares_outstanding
# P/E Ratio
net_income = self.income_statement.get("net_income", 0)
eps = self.safe_divide(net_income, shares_outstanding)
ratios["pe_ratio"] = self.safe_divide(share_price, eps)
ratios["eps"] = eps
# P/B Ratio
book_value = self.balance_sheet.get("shareholders_equity", 0)
book_value_per_share = self.safe_divide(book_value, shares_outstanding)
ratios["pb_ratio"] = self.safe_divide(share_price, book_value_per_share)
ratios["book_value_per_share"] = book_value_per_share
# P/S Ratio
revenue = self.income_statement.get("revenue", 0)
ratios["ps_ratio"] = self.safe_divide(market_cap, revenue)
# EV/EBITDA
ebitda = self.income_statement.get("ebitda", 0)
total_debt = self.balance_sheet.get("total_debt", 0)
cash = self.balance_sheet.get("cash_and_equivalents", 0)
enterprise_value = market_cap + total_debt - cash
ratios["ev_to_ebitda"] = self.safe_divide(enterprise_value, ebitda)
# PEG Ratio (if growth rate available)
earnings_growth = self.market_data.get("earnings_growth_rate", 0)
if earnings_growth > 0:
ratios["peg_ratio"] = self.safe_divide(ratios["pe_ratio"], earnings_growth * 100)
return ratios
def calculate_all_ratios(self) -> dict[str, Any]:
"""Calculate all financial ratios."""
return {
"profitability": self.calculate_profitability_ratios(),
"liquidity": self.calculate_liquidity_ratios(),
"leverage": self.calculate_leverage_ratios(),
"efficiency": self.calculate_efficiency_ratios(),
"valuation": self.calculate_valuation_ratios(),
}
def interpret_ratio(self, ratio_name: str, value: float) -> str:
"""Provide interpretation for a specific ratio."""
interpretations = {
"current_ratio": lambda v: (
"Strong liquidity"
if v > 2
else "Adequate liquidity"
if v > 1.5
else "Potential liquidity concerns"
if v > 1
else "Liquidity issues"
),
"debt_to_equity": lambda v: (
"Low leverage"
if v < 0.5
else "Moderate leverage"
if v < 1
else "High leverage"
if v < 2
else "Very high leverage"
),
"roe": lambda v: (
"Excellent returns"
if v > 0.20
else "Good returns"
if v > 0.15
else "Average returns"
if v > 0.10
else "Below average returns"
if v > 0
else "Negative returns"
),
"pe_ratio": lambda v: (
"Potentially undervalued"
if 0 < v < 15
else "Fair value"
if 15 <= v < 25
else "Growth premium"
if 25 <= v < 40
else "High valuation"
if v >= 40
else "N/A (negative earnings)"
if v <= 0
else "N/A"
),
}
if ratio_name in interpretations:
return interpretations[ratio_name](value)
return "No interpretation available"
def format_ratio(self, name: str, value: float, format_type: str = "ratio") -> str:
"""Format ratio value for display."""
if format_type == "percentage":
return f"{value * 100:.2f}%"
elif format_type == "times":
return f"{value:.2f}x"
elif format_type == "days":
return f"{value:.1f} days"
elif format_type == "currency":
return f"${value:.2f}"
else:
return f"{value:.2f}"
def calculate_ratios_from_data(financial_data: dict[str, Any]) -> dict[str, Any]:
"""
Main function to calculate all ratios from financial data.
Args:
financial_data: Dictionary with financial statement data
Returns:
Dictionary with calculated ratios and interpretations
"""
calculator = FinancialRatioCalculator(financial_data)
ratios = calculator.calculate_all_ratios()
# Add interpretations
interpretations = {}
for category, category_ratios in ratios.items():
interpretations[category] = {}
for ratio_name, value in category_ratios.items():
interpretations[category][ratio_name] = {
"value": value,
"formatted": calculator.format_ratio(ratio_name, value),
"interpretation": calculator.interpret_ratio(ratio_name, value),
}
return {
"ratios": ratios,
"interpretations": interpretations,
"summary": generate_summary(ratios),
}
def generate_summary(ratios: dict[str, Any]) -> str:
"""Generate a text summary of the financial analysis."""
summary_parts = []
# Profitability summary
prof = ratios.get("profitability", {})
if prof.get("roe", 0) > 0:
summary_parts.append(
f"ROE of {prof['roe'] * 100:.1f}% indicates {'strong' if prof['roe'] > 0.15 else 'moderate'} shareholder returns."
)
# Liquidity summary
liq = ratios.get("liquidity", {})
if liq.get("current_ratio", 0) > 0:
summary_parts.append(
f"Current ratio of {liq['current_ratio']:.2f} suggests {'good' if liq['current_ratio'] > 1.5 else 'potential'} liquidity {'position' if liq['current_ratio'] > 1.5 else 'concerns'}."
)
# Leverage summary
lev = ratios.get("leverage", {})
if lev.get("debt_to_equity", 0) >= 0:
summary_parts.append(
f"Debt-to-equity of {lev['debt_to_equity']:.2f} indicates {'conservative' if lev['debt_to_equity'] < 0.5 else 'moderate' if lev['debt_to_equity'] < 1 else 'high'} leverage."
)
# Valuation summary
val = ratios.get("valuation", {})
if val.get("pe_ratio", 0) > 0:
summary_parts.append(
f"P/E ratio of {val['pe_ratio']:.1f} suggests the stock is trading at {'a discount' if val['pe_ratio'] < 15 else 'fair value' if val['pe_ratio'] < 25 else 'a premium'}."
)
return " ".join(summary_parts) if summary_parts else "Insufficient data for summary."
# Example usage
if __name__ == "__main__":
# Sample financial data
sample_data = {
"income_statement": {
"revenue": 1000000,
"cost_of_goods_sold": 600000,
"operating_income": 200000,
"ebit": 180000,
"ebitda": 250000,
"interest_expense": 20000,
"net_income": 150000,
},
"balance_sheet": {
"total_assets": 2000000,
"current_assets": 800000,
"cash_and_equivalents": 200000,
"accounts_receivable": 150000,
"inventory": 250000,
"current_liabilities": 400000,
"total_debt": 500000,
"current_portion_long_term_debt": 50000,
"shareholders_equity": 1500000,
},
"cash_flow": {
"operating_cash_flow": 180000,
"investing_cash_flow": -100000,
"financing_cash_flow": -50000,
},
"market_data": {
"share_price": 50,
"shares_outstanding": 100000,
"earnings_growth_rate": 0.10,
},
}
results = calculate_ratios_from_data(sample_data)
print(json.dumps(results, indent=2))
@@ -0,0 +1,377 @@
"""
Financial ratio interpretation module.
Provides industry benchmarks and contextual analysis.
"""
from typing import Any
class RatioInterpreter:
"""Interpret financial ratios with industry context."""
# Industry benchmark ranges (simplified for demonstration)
BENCHMARKS = {
"technology": {
"current_ratio": {"excellent": 2.5, "good": 1.8, "acceptable": 1.2, "poor": 1.0},
"debt_to_equity": {"excellent": 0.3, "good": 0.5, "acceptable": 1.0, "poor": 2.0},
"roe": {"excellent": 0.25, "good": 0.18, "acceptable": 0.12, "poor": 0.08},
"gross_margin": {"excellent": 0.70, "good": 0.50, "acceptable": 0.35, "poor": 0.20},
"pe_ratio": {"undervalued": 15, "fair": 25, "growth": 35, "expensive": 50},
},
"retail": {
"current_ratio": {"excellent": 2.0, "good": 1.5, "acceptable": 1.0, "poor": 0.8},
"debt_to_equity": {"excellent": 0.5, "good": 0.8, "acceptable": 1.5, "poor": 2.5},
"roe": {"excellent": 0.20, "good": 0.15, "acceptable": 0.10, "poor": 0.05},
"gross_margin": {"excellent": 0.40, "good": 0.30, "acceptable": 0.20, "poor": 0.10},
"pe_ratio": {"undervalued": 12, "fair": 18, "growth": 25, "expensive": 35},
},
"financial": {
"current_ratio": {"excellent": 1.5, "good": 1.2, "acceptable": 1.0, "poor": 0.8},
"debt_to_equity": {"excellent": 1.0, "good": 2.0, "acceptable": 4.0, "poor": 6.0},
"roe": {"excellent": 0.15, "good": 0.12, "acceptable": 0.08, "poor": 0.05},
"pe_ratio": {"undervalued": 10, "fair": 15, "growth": 20, "expensive": 30},
},
"manufacturing": {
"current_ratio": {"excellent": 2.2, "good": 1.7, "acceptable": 1.3, "poor": 1.0},
"debt_to_equity": {"excellent": 0.4, "good": 0.7, "acceptable": 1.2, "poor": 2.0},
"roe": {"excellent": 0.18, "good": 0.14, "acceptable": 0.10, "poor": 0.06},
"gross_margin": {"excellent": 0.35, "good": 0.25, "acceptable": 0.18, "poor": 0.12},
"pe_ratio": {"undervalued": 14, "fair": 20, "growth": 28, "expensive": 40},
},
"healthcare": {
"current_ratio": {"excellent": 2.3, "good": 1.8, "acceptable": 1.4, "poor": 1.0},
"debt_to_equity": {"excellent": 0.3, "good": 0.6, "acceptable": 1.0, "poor": 1.8},
"roe": {"excellent": 0.22, "good": 0.16, "acceptable": 0.11, "poor": 0.07},
"gross_margin": {"excellent": 0.65, "good": 0.45, "acceptable": 0.30, "poor": 0.20},
"pe_ratio": {"undervalued": 18, "fair": 28, "growth": 40, "expensive": 55},
},
}
def __init__(self, industry: str = "general"):
"""
Initialize interpreter with industry context.
Args:
industry: Industry sector for benchmarking
"""
self.industry = industry.lower()
self.benchmarks = self.BENCHMARKS.get(self.industry, self._get_general_benchmarks())
def _get_general_benchmarks(self) -> dict[str, Any]:
"""Get general industry-agnostic benchmarks."""
return {
"current_ratio": {"excellent": 2.0, "good": 1.5, "acceptable": 1.0, "poor": 0.8},
"debt_to_equity": {"excellent": 0.5, "good": 1.0, "acceptable": 1.5, "poor": 2.5},
"roe": {"excellent": 0.20, "good": 0.15, "acceptable": 0.10, "poor": 0.05},
"gross_margin": {"excellent": 0.40, "good": 0.30, "acceptable": 0.20, "poor": 0.10},
"pe_ratio": {"undervalued": 15, "fair": 22, "growth": 30, "expensive": 45},
}
def interpret_ratio(self, ratio_name: str, value: float) -> dict[str, Any]:
"""
Interpret a single ratio with context.
Args:
ratio_name: Name of the ratio
value: Calculated ratio value
Returns:
Dictionary with interpretation details
"""
interpretation = {
"value": value,
"rating": "N/A",
"message": "",
"recommendation": "",
"benchmark_comparison": {},
}
if ratio_name in self.benchmarks:
benchmark = self.benchmarks[ratio_name]
interpretation["benchmark_comparison"] = benchmark
# Determine rating based on benchmarks
if ratio_name in ["current_ratio", "roe", "gross_margin"]:
# Higher is better
if value >= benchmark["excellent"]:
interpretation["rating"] = "Excellent"
interpretation["message"] = (
"Performance significantly exceeds industry standards"
)
elif value >= benchmark["good"]:
interpretation["rating"] = "Good"
interpretation["message"] = (
f"Above average performance for {self.industry} industry"
)
elif value >= benchmark["acceptable"]:
interpretation["rating"] = "Acceptable"
interpretation["message"] = "Meets industry standards"
else:
interpretation["rating"] = "Poor"
interpretation["message"] = "Below industry standards - attention needed"
elif ratio_name == "debt_to_equity":
# Lower is better
if value <= benchmark["excellent"]:
interpretation["rating"] = "Excellent"
interpretation["message"] = "Very conservative capital structure"
elif value <= benchmark["good"]:
interpretation["rating"] = "Good"
interpretation["message"] = "Healthy leverage level"
elif value <= benchmark["acceptable"]:
interpretation["rating"] = "Acceptable"
interpretation["message"] = "Moderate leverage"
else:
interpretation["rating"] = "Poor"
interpretation["message"] = "High leverage - potential risk"
elif ratio_name == "pe_ratio":
# Context-dependent
if value > 0:
if value < benchmark["undervalued"]:
interpretation["rating"] = "Potentially Undervalued"
interpretation["message"] = (
f"Trading below typical {self.industry} multiples"
)
elif value < benchmark["fair"]:
interpretation["rating"] = "Fair Value"
interpretation["message"] = "In line with industry averages"
elif value < benchmark["growth"]:
interpretation["rating"] = "Growth Premium"
interpretation["message"] = "Market pricing in growth expectations"
else:
interpretation["rating"] = "Expensive"
interpretation["message"] = "High valuation relative to industry"
# Add specific recommendations
interpretation["recommendation"] = self._get_recommendation(
ratio_name, interpretation["rating"]
)
return interpretation
def _get_recommendation(self, ratio_name: str, rating: str) -> str:
"""Generate actionable recommendations based on ratio and rating."""
recommendations = {
"current_ratio": {
"Poor": "Consider improving working capital management, reducing short-term debt, or increasing liquid assets",
"Acceptable": "Monitor liquidity closely and consider building additional cash reserves",
"Good": "Maintain current liquidity management practices",
"Excellent": "Strong liquidity position - consider productive use of excess cash",
},
"debt_to_equity": {
"Poor": "High leverage increases financial risk - consider debt reduction strategies",
"Acceptable": "Monitor debt levels and ensure adequate interest coverage",
"Good": "Balanced capital structure - maintain current approach",
"Excellent": "Conservative leverage - may consider strategic use of debt for growth",
},
"roe": {
"Poor": "Focus on improving operational efficiency and profitability",
"Acceptable": "Explore opportunities to enhance returns through operational improvements",
"Good": "Solid returns - continue current strategies",
"Excellent": "Outstanding performance - ensure sustainability of high returns",
},
"pe_ratio": {
"Potentially Undervalued": "May present buying opportunity if fundamentals are solid",
"Fair Value": "Reasonably priced relative to industry peers",
"Growth Premium": "Ensure growth prospects justify premium valuation",
"Expensive": "Consider valuation risk - ensure fundamentals support high multiple",
},
}
if ratio_name in recommendations and rating in recommendations[ratio_name]:
return recommendations[ratio_name][rating]
return "Continue monitoring this metric"
def analyze_trend(
self, ratio_name: str, values: list[float], periods: list[str]
) -> dict[str, Any]:
"""
Analyze trend in a ratio over time.
Args:
ratio_name: Name of the ratio
values: List of ratio values
periods: List of period labels
Returns:
Trend analysis dictionary
"""
if len(values) < 2:
return {
"trend": "Insufficient data",
"message": "Need at least 2 periods for trend analysis",
}
# Calculate trend
first_value = values[0]
last_value = values[-1]
change = last_value - first_value
pct_change = (change / abs(first_value)) * 100 if first_value != 0 else 0
# Determine trend direction
if abs(pct_change) < 5:
trend = "Stable"
elif pct_change > 0:
trend = "Improving" if ratio_name != "debt_to_equity" else "Deteriorating"
else:
trend = "Deteriorating" if ratio_name != "debt_to_equity" else "Improving"
return {
"trend": trend,
"change": change,
"pct_change": pct_change,
"message": f"{ratio_name} has {'increased' if change > 0 else 'decreased'} by {abs(pct_change):.1f}% from {periods[0]} to {periods[-1]}",
"values": list(zip(periods, values, strict=False)),
}
def generate_report(self, ratios: dict[str, Any]) -> str:
"""
Generate a comprehensive interpretation report.
Args:
ratios: Dictionary of calculated ratios
Returns:
Formatted report string
"""
report_lines = [
f"Financial Analysis Report - {self.industry.title()} Industry Context",
"=" * 70,
"",
]
for category, category_ratios in ratios.items():
report_lines.append(f"\n{category.upper()} ANALYSIS")
report_lines.append("-" * 40)
for ratio_name, value in category_ratios.items():
if isinstance(value, (int, float)):
interpretation = self.interpret_ratio(ratio_name, value)
report_lines.append(f"\n{ratio_name.replace('_', ' ').title()}:")
report_lines.append(f" Value: {value:.2f}")
report_lines.append(f" Rating: {interpretation['rating']}")
report_lines.append(f" Analysis: {interpretation['message']}")
report_lines.append(f" Action: {interpretation['recommendation']}")
return "\n".join(report_lines)
def perform_comprehensive_analysis(
ratios: dict[str, Any],
industry: str = "general",
historical_data: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""
Perform comprehensive ratio analysis with interpretations.
Args:
ratios: Calculated financial ratios
industry: Industry sector for benchmarking
historical_data: Optional historical ratio data for trend analysis
Returns:
Complete analysis with interpretations and recommendations
"""
interpreter = RatioInterpreter(industry)
analysis = {
"current_analysis": {},
"trend_analysis": {},
"overall_health": {},
"recommendations": [],
}
# Analyze current ratios
for category, category_ratios in ratios.items():
analysis["current_analysis"][category] = {}
for ratio_name, value in category_ratios.items():
if isinstance(value, (int, float)):
analysis["current_analysis"][category][ratio_name] = interpreter.interpret_ratio(
ratio_name, value
)
# Perform trend analysis if historical data provided
if historical_data:
for ratio_name, historical_values in historical_data.items():
if "values" in historical_values and "periods" in historical_values:
analysis["trend_analysis"][ratio_name] = interpreter.analyze_trend(
ratio_name, historical_values["values"], historical_values["periods"]
)
# Generate overall health assessment
analysis["overall_health"] = _assess_overall_health(analysis["current_analysis"])
# Generate key recommendations
analysis["recommendations"] = _generate_key_recommendations(analysis)
# Add formatted report
analysis["report"] = interpreter.generate_report(ratios)
return analysis
def _assess_overall_health(current_analysis: dict[str, Any]) -> dict[str, str]:
"""Assess overall financial health based on ratio analysis."""
ratings = []
for _category, category_analysis in current_analysis.items():
for _ratio_name, ratio_analysis in category_analysis.items():
if "rating" in ratio_analysis:
ratings.append(ratio_analysis["rating"])
# Simple scoring system
score_map = {
"Excellent": 4,
"Good": 3,
"Acceptable": 2,
"Poor": 1,
"Fair Value": 3,
"Potentially Undervalued": 3,
"Growth Premium": 2,
"Expensive": 1,
}
scores = [score_map.get(rating, 2) for rating in ratings]
avg_score = sum(scores) / len(scores) if scores else 0
if avg_score >= 3.5:
health = "Excellent"
message = "Company shows strong financial health across most metrics"
elif avg_score >= 2.5:
health = "Good"
message = "Overall healthy financial position with some areas for improvement"
elif avg_score >= 1.5:
health = "Fair"
message = "Mixed financial indicators - attention needed in several areas"
else:
health = "Poor"
message = "Significant financial challenges requiring immediate attention"
return {"status": health, "message": message, "score": f"{avg_score:.1f}/4.0"}
def _generate_key_recommendations(analysis: dict[str, Any]) -> list[str]:
"""Generate prioritized recommendations based on analysis."""
recommendations = []
# Check for critical issues
for _category, category_analysis in analysis["current_analysis"].items():
for ratio_name, ratio_analysis in category_analysis.items():
if ratio_analysis.get("rating") == "Poor":
recommendations.append(
f"Priority: Address {ratio_name.replace('_', ' ')} - {ratio_analysis.get('recommendation', '')}"
)
# Add trend-based recommendations
for ratio_name, trend in analysis.get("trend_analysis", {}).items():
if trend.get("trend") == "Deteriorating":
recommendations.append(
f"Monitor: {ratio_name.replace('_', ' ')} showing negative trend"
)
# Add general recommendations if healthy
if not recommendations:
recommendations.append("Continue current financial management practices")
recommendations.append("Consider strategic growth opportunities")
return recommendations[:5] # Return top 5 recommendations
@@ -0,0 +1,137 @@
# Brand Guidelines Reference
## Quick Reference Card
### Must-Have Elements
✅ Company logo on first page/slide
✅ Correct brand colors (no variations)
✅ Approved fonts only
✅ Consistent formatting throughout
✅ Professional tone of voice
### Never Use
❌ Competitor logos or references
❌ Unapproved colors or gradients
❌ Decorative or script fonts
❌ Pixelated or stretched logos
❌ Informal language or slang
## Color Codes Reference
### For Digital (RGB/Hex)
| Color Name | Hex Code | RGB | Usage |
|------------|----------|-----|-------|
| Acme Blue | #0066CC | 0, 102, 204 | Primary headers, CTAs |
| Acme Navy | #003366 | 0, 51, 102 | Body text, secondary |
| Success Green | #28A745 | 40, 167, 69 | Positive values |
| Warning Amber | #FFC107 | 255, 193, 7 | Warnings, attention |
| Error Red | #DC3545 | 220, 53, 69 | Errors, negative |
| Neutral Gray | #6C757D | 108, 117, 125 | Muted text |
| Light Gray | #F8F9FA | 248, 249, 250 | Backgrounds |
### For Print (CMYK)
| Color Name | CMYK | Pantone |
|------------|------|---------|
| Acme Blue | 100, 50, 0, 20 | 2935 C |
| Acme Navy | 100, 50, 0, 60 | 2965 C |
## Document Templates
### Email Signature
```
[Name]
[Title]
Acme Corporation | Innovation Through Excellence
[Phone] | [Email]
www.acmecorp.example
```
### Slide Footer
```
© 2025 Acme Corporation | Confidential | Page [X]
```
### Report Header
```
[Logo] [Document Title] Page [X] of [Y]
```
## Accessibility Standards
### Color Contrast
- Text on white background: Use Acme Navy (#003366)
- Text on blue background: Use white (#FFFFFF)
- Minimum contrast ratio: 4.5:1 for body text
- Minimum contrast ratio: 3:1 for large text
### Font Sizes
- Minimum body text: 11pt (print), 14px (digital)
- Minimum caption text: 9pt (print), 12px (digital)
## File Naming Conventions
### Standard Format
```
YYYY-MM-DD_DocumentType_Version_Status.ext
```
### Examples
- `2025-01-15_QuarterlyReport_v2_FINAL.pptx`
- `2025-01-15_BudgetAnalysis_v1_DRAFT.xlsx`
- `2025-01-15_Proposal_v3_APPROVED.pdf`
## Common Mistakes to Avoid
1. **Wrong Blue**: Using generic blue instead of Acme Blue #0066CC
2. **Stretched Logo**: Always maintain aspect ratio
3. **Too Many Colors**: Stick to the approved palette
4. **Inconsistent Fonts**: Don't mix font families
5. **Missing Logo**: Always include on first page
6. **Wrong Date Format**: Use "Month DD, YYYY"
7. **Decimal Places**: Be consistent (currency: 2, percentage: 1)
## Department-Specific Guidelines
### Finance
- Always right-align numbers in tables
- Use parentheses for negative values: ($1,234)
- Include data source citations
### Marketing
- Can use full secondary color palette
- May include approved imagery
- Follow social media specific guidelines when applicable
### Legal
- Use numbered sections (1.0, 1.1, 1.2)
- Include document control information
- Apply "Confidential" watermark when needed
## International Considerations
### Date Formats by Region
- **US**: Month DD, YYYY (January 15, 2025)
- **UK**: DD Month YYYY (15 January 2025)
- **ISO**: YYYY-MM-DD (2025-01-15)
### Currency Display
- **USD**: $1,234.56
- **EUR**: €1.234,56
- **GBP**: £1,234.56
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 2.0 | Jan 2025 | Added digital color codes |
| 1.5 | Oct 2024 | Updated font guidelines |
| 1.0 | Jan 2024 | Initial brand guidelines |
## Contact for Questions
**Brand Team**
Email: brand@acmecorp.example
Slack: #brand-guidelines
**For Exceptions**
Submit request to brand team with business justification
+171
View File
@@ -0,0 +1,171 @@
---
name: applying-brand-guidelines
description: This skill applies consistent corporate branding and styling to all generated documents including colors, fonts, layouts, and messaging
---
# Corporate Brand Guidelines Skill
This skill ensures all generated documents adhere to corporate brand standards for consistent, professional communication.
## Brand Identity
### Company: Acme Corporation
**Tagline**: "Innovation Through Excellence"
**Industry**: Technology Solutions
## Visual Standards
### Color Palette
**Primary Colors**:
- **Acme Blue**: #0066CC (RGB: 0, 102, 204) - Headers, primary buttons
- **Acme Navy**: #003366 (RGB: 0, 51, 102) - Text, accents
- **White**: #FFFFFF - Backgrounds, reverse text
**Secondary Colors**:
- **Success Green**: #28A745 (RGB: 40, 167, 69) - Positive metrics
- **Warning Amber**: #FFC107 (RGB: 255, 193, 7) - Cautions
- **Error Red**: #DC3545 (RGB: 220, 53, 69) - Negative values
- **Neutral Gray**: #6C757D (RGB: 108, 117, 125) - Secondary text
### Typography
**Primary Font Family**: Segoe UI, system-ui, -apple-system, sans-serif
**Font Hierarchy**:
- **H1**: 32pt, Bold, Acme Blue
- **H2**: 24pt, Semibold, Acme Navy
- **H3**: 18pt, Semibold, Acme Navy
- **Body**: 11pt, Regular, Acme Navy
- **Caption**: 9pt, Regular, Neutral Gray
### Logo Usage
- Position: Top-left corner on first page/slide
- Size: 120px width (maintain aspect ratio)
- Clear space: Minimum 20px padding on all sides
- Never distort, rotate, or apply effects
## Document Standards
### PowerPoint Presentations
**Slide Templates**:
1. **Title Slide**: Company logo, presentation title, date, presenter
2. **Section Divider**: Section title with blue background
3. **Content Slide**: Title bar with blue background, white content area
4. **Data Slide**: For charts/graphs, maintain color palette
**Layout Rules**:
- Margins: 0.5 inches all sides
- Title position: Top 15% of slide
- Bullet indentation: 0.25 inches per level
- Maximum 6 bullet points per slide
- Charts use brand colors exclusively
### Excel Spreadsheets
**Formatting Standards**:
- **Headers**: Row 1, Bold, White text on Acme Blue background
- **Subheaders**: Bold, Acme Navy text
- **Data cells**: Regular, Acme Navy text
- **Borders**: Thin, Neutral Gray
- **Alternating rows**: Light gray (#F8F9FA) for readability
**Chart Defaults**:
- Primary series: Acme Blue
- Secondary series: Success Green
- Gridlines: Neutral Gray, 0.5pt
- No 3D effects or gradients
### PDF Documents
**Page Layout**:
- **Header**: Company logo left, document title center, page number right
- **Footer**: Copyright notice left, date center, classification right
- **Margins**: 1 inch all sides
- **Line spacing**: 1.15
- **Paragraph spacing**: 12pt after
**Section Formatting**:
- Main headings: Acme Blue, 16pt, bold
- Subheadings: Acme Navy, 14pt, semibold
- Body text: Acme Navy, 11pt, regular
## Content Guidelines
### Tone of Voice
- **Professional**: Formal but approachable
- **Clear**: Avoid jargon, use simple language
- **Active**: Use active voice, action-oriented
- **Positive**: Focus on solutions and benefits
### Standard Phrases
**Opening Statements**:
- "At Acme Corporation, we..."
- "Our commitment to innovation..."
- "Delivering excellence through..."
**Closing Statements**:
- "Thank you for your continued partnership."
- "We look forward to serving your needs."
- "Together, we achieve excellence."
### Data Presentation
**Numbers**:
- Use comma separators for thousands
- Currency: $X,XXX.XX format
- Percentages: XX.X% (one decimal)
- Dates: Month DD, YYYY
**Tables**:
- Headers in brand blue
- Alternating row colors
- Right-align numbers
- Left-align text
## Quality Standards
### Before Finalizing
Always ensure:
1. Logo is properly placed and sized
2. All colors match brand palette exactly
3. Fonts are consistent throughout
4. No typos or grammatical errors
5. Data is accurately presented
6. Professional tone maintained
### Prohibited Elements
Never use:
- Clip art or stock photos without approval
- Comic Sans, Papyrus, or decorative fonts
- Rainbow colors or gradients
- Animations or transitions (unless specified)
- Competitor branding or references
## Application Instructions
When creating any document:
1. Start with brand colors and fonts
2. Apply appropriate template structure
3. Include logo on first page/slide
4. Use consistent formatting throughout
5. Review against brand standards
6. Ensure professional appearance
## Scripts
- `apply_brand.py`: Automatically applies brand formatting to documents
- `validate_brand.py`: Checks documents for brand compliance
## Notes
- These guidelines apply to all external communications
- Internal documents may use simplified formatting
- Special projects may have exceptions (request approval)
- Brand guidelines updated quarterly - check for latest version
@@ -0,0 +1,432 @@
"""
Brand application module for corporate document styling.
Applies consistent branding to Excel, PowerPoint, and PDF documents.
"""
from typing import Any
class BrandFormatter:
"""Apply corporate brand guidelines to documents."""
# Brand color definitions
COLORS = {
"primary": {
"acme_blue": {"hex": "#0066CC", "rgb": (0, 102, 204)},
"acme_navy": {"hex": "#003366", "rgb": (0, 51, 102)},
"white": {"hex": "#FFFFFF", "rgb": (255, 255, 255)},
},
"secondary": {
"success_green": {"hex": "#28A745", "rgb": (40, 167, 69)},
"warning_amber": {"hex": "#FFC107", "rgb": (255, 193, 7)},
"error_red": {"hex": "#DC3545", "rgb": (220, 53, 69)},
"neutral_gray": {"hex": "#6C757D", "rgb": (108, 117, 125)},
"light_gray": {"hex": "#F8F9FA", "rgb": (248, 249, 250)},
},
}
# Font definitions
FONTS = {
"primary": "Segoe UI",
"fallback": ["system-ui", "-apple-system", "sans-serif"],
"sizes": {"h1": 32, "h2": 24, "h3": 18, "body": 11, "caption": 9},
"weights": {"regular": 400, "semibold": 600, "bold": 700},
}
# Company information
COMPANY = {
"name": "Acme Corporation",
"tagline": "Innovation Through Excellence",
"copyright": "© 2025 Acme Corporation. All rights reserved.",
"website": "www.acmecorp.example",
"logo_path": "assets/acme_logo.png",
}
def __init__(self):
"""Initialize brand formatter with standard settings."""
self.colors = self.COLORS
self.fonts = self.FONTS
self.company = self.COMPANY
def format_excel(self, workbook_config: dict[str, Any]) -> dict[str, Any]:
"""
Apply brand formatting to Excel workbook configuration.
Args:
workbook_config: Excel workbook configuration dictionary
Returns:
Branded workbook configuration
"""
branded_config = workbook_config.copy()
# Apply header formatting
branded_config["header_style"] = {
"font": {
"name": self.fonts["primary"],
"size": self.fonts["sizes"]["body"],
"bold": True,
"color": self.colors["primary"]["white"]["hex"],
},
"fill": {"type": "solid", "color": self.colors["primary"]["acme_blue"]["hex"]},
"alignment": {"horizontal": "center", "vertical": "center"},
"border": {"style": "thin", "color": self.colors["secondary"]["neutral_gray"]["hex"]},
}
# Apply data cell formatting
branded_config["cell_style"] = {
"font": {
"name": self.fonts["primary"],
"size": self.fonts["sizes"]["body"],
"color": self.colors["primary"]["acme_navy"]["hex"],
},
"alignment": {"horizontal": "left", "vertical": "center"},
}
# Apply alternating row colors
branded_config["alternating_rows"] = {
"enabled": True,
"color": self.colors["secondary"]["light_gray"]["hex"],
}
# Chart color scheme
branded_config["chart_colors"] = [
self.colors["primary"]["acme_blue"]["hex"],
self.colors["secondary"]["success_green"]["hex"],
self.colors["secondary"]["warning_amber"]["hex"],
self.colors["secondary"]["neutral_gray"]["hex"],
]
return branded_config
def format_powerpoint(self, presentation_config: dict[str, Any]) -> dict[str, Any]:
"""
Apply brand formatting to PowerPoint presentation configuration.
Args:
presentation_config: PowerPoint configuration dictionary
Returns:
Branded presentation configuration
"""
branded_config = presentation_config.copy()
# Slide master settings
branded_config["master"] = {
"background_color": self.colors["primary"]["white"]["hex"],
"title_area": {
"font": self.fonts["primary"],
"size": self.fonts["sizes"]["h1"],
"color": self.colors["primary"]["acme_blue"]["hex"],
"bold": True,
"position": {"x": 0.5, "y": 0.15, "width": 9, "height": 1},
},
"content_area": {
"font": self.fonts["primary"],
"size": self.fonts["sizes"]["body"],
"color": self.colors["primary"]["acme_navy"]["hex"],
"position": {"x": 0.5, "y": 2, "width": 9, "height": 5},
},
"footer": {
"show_slide_number": True,
"show_date": True,
"company_name": self.company["name"],
},
}
# Title slide template
branded_config["title_slide"] = {
"background": self.colors["primary"]["acme_blue"]["hex"],
"title_color": self.colors["primary"]["white"]["hex"],
"subtitle_color": self.colors["primary"]["white"]["hex"],
"include_logo": True,
"logo_position": {"x": 0.5, "y": 0.5, "width": 2},
}
# Content slide template
branded_config["content_slide"] = {
"title_bar": {
"background": self.colors["primary"]["acme_blue"]["hex"],
"text_color": self.colors["primary"]["white"]["hex"],
"height": 1,
},
"bullet_style": {"level1": "", "level2": "", "level3": "", "indent": 0.25},
}
# Chart defaults
branded_config["charts"] = {
"color_scheme": [
self.colors["primary"]["acme_blue"]["hex"],
self.colors["secondary"]["success_green"]["hex"],
self.colors["secondary"]["warning_amber"]["hex"],
self.colors["secondary"]["neutral_gray"]["hex"],
],
"gridlines": {"color": self.colors["secondary"]["neutral_gray"]["hex"], "width": 0.5},
"font": {"name": self.fonts["primary"], "size": self.fonts["sizes"]["caption"]},
}
return branded_config
def format_pdf(self, document_config: dict[str, Any]) -> dict[str, Any]:
"""
Apply brand formatting to PDF document configuration.
Args:
document_config: PDF document configuration dictionary
Returns:
Branded document configuration
"""
branded_config = document_config.copy()
# Page layout
branded_config["page"] = {
"margins": {"top": 1, "bottom": 1, "left": 1, "right": 1},
"size": "letter",
"orientation": "portrait",
}
# Header configuration
branded_config["header"] = {
"height": 0.75,
"content": {
"left": {"type": "logo", "width": 1.5},
"center": {
"type": "text",
"content": document_config.get("title", "Document"),
"font": self.fonts["primary"],
"size": self.fonts["sizes"]["body"],
"color": self.colors["primary"]["acme_navy"]["hex"],
},
"right": {"type": "page_number", "format": "Page {page} of {total}"},
},
}
# Footer configuration
branded_config["footer"] = {
"height": 0.5,
"content": {
"left": {
"type": "text",
"content": self.company["copyright"],
"font": self.fonts["primary"],
"size": self.fonts["sizes"]["caption"],
"color": self.colors["secondary"]["neutral_gray"]["hex"],
},
"center": {"type": "date", "format": "%B %d, %Y"},
"right": {"type": "text", "content": "Confidential"},
},
}
# Text styles
branded_config["styles"] = {
"heading1": {
"font": self.fonts["primary"],
"size": self.fonts["sizes"]["h1"],
"color": self.colors["primary"]["acme_blue"]["hex"],
"bold": True,
"spacing_after": 12,
},
"heading2": {
"font": self.fonts["primary"],
"size": self.fonts["sizes"]["h2"],
"color": self.colors["primary"]["acme_navy"]["hex"],
"bold": True,
"spacing_after": 10,
},
"heading3": {
"font": self.fonts["primary"],
"size": self.fonts["sizes"]["h3"],
"color": self.colors["primary"]["acme_navy"]["hex"],
"bold": False,
"spacing_after": 8,
},
"body": {
"font": self.fonts["primary"],
"size": self.fonts["sizes"]["body"],
"color": self.colors["primary"]["acme_navy"]["hex"],
"line_spacing": 1.15,
"paragraph_spacing": 12,
},
"caption": {
"font": self.fonts["primary"],
"size": self.fonts["sizes"]["caption"],
"color": self.colors["secondary"]["neutral_gray"]["hex"],
"italic": True,
},
}
# Table formatting
branded_config["table_style"] = {
"header": {
"background": self.colors["primary"]["acme_blue"]["hex"],
"text_color": self.colors["primary"]["white"]["hex"],
"bold": True,
},
"rows": {
"alternating_color": self.colors["secondary"]["light_gray"]["hex"],
"border_color": self.colors["secondary"]["neutral_gray"]["hex"],
},
}
return branded_config
def validate_colors(self, colors_used: list[str]) -> dict[str, Any]:
"""
Validate that colors match brand guidelines.
Args:
colors_used: List of color codes used in document
Returns:
Validation results with corrections if needed
"""
results = {"valid": True, "corrections": [], "warnings": []}
approved_colors = []
for category in self.colors.values():
for color in category.values():
approved_colors.append(color["hex"].upper())
for color in colors_used:
color_upper = color.upper()
if color_upper not in approved_colors:
results["valid"] = False
# Find closest brand color
closest = self._find_closest_brand_color(color)
results["corrections"].append(
{
"original": color,
"suggested": closest,
"message": f"Non-brand color {color} should be replaced with {closest}",
}
)
return results
def _find_closest_brand_color(self, color: str) -> str:
"""Find the closest brand color to a given color."""
# Simplified - in reality would calculate color distance
return self.colors["primary"]["acme_blue"]["hex"]
def apply_watermark(self, document_type: str) -> dict[str, Any]:
"""
Generate watermark configuration for documents.
Args:
document_type: Type of document (draft, confidential, etc.)
Returns:
Watermark configuration
"""
watermarks = {
"draft": {
"text": "DRAFT",
"color": self.colors["secondary"]["neutral_gray"]["hex"],
"opacity": 0.1,
"angle": 45,
"font_size": 72,
},
"confidential": {
"text": "CONFIDENTIAL",
"color": self.colors["secondary"]["error_red"]["hex"],
"opacity": 0.1,
"angle": 45,
"font_size": 60,
},
"sample": {
"text": "SAMPLE",
"color": self.colors["secondary"]["warning_amber"]["hex"],
"opacity": 0.15,
"angle": 45,
"font_size": 72,
},
}
return watermarks.get(document_type, watermarks["draft"])
def get_chart_palette(self, num_series: int = 4) -> list[str]:
"""
Get color palette for charts.
Args:
num_series: Number of data series
Returns:
List of hex color codes
"""
palette = [
self.colors["primary"]["acme_blue"]["hex"],
self.colors["secondary"]["success_green"]["hex"],
self.colors["secondary"]["warning_amber"]["hex"],
self.colors["secondary"]["neutral_gray"]["hex"],
self.colors["primary"]["acme_navy"]["hex"],
self.colors["secondary"]["error_red"]["hex"],
]
return palette[:num_series]
def format_number(self, value: float, format_type: str = "general") -> str:
"""
Format numbers according to brand standards.
Args:
value: Numeric value to format
format_type: Type of formatting (currency, percentage, general)
Returns:
Formatted string
"""
if format_type == "currency":
return f"${value:,.2f}"
elif format_type == "percentage":
return f"{value:.1f}%"
elif format_type == "large_number":
if value >= 1_000_000:
return f"{value / 1_000_000:.1f}M"
elif value >= 1_000:
return f"{value / 1_000:.1f}K"
else:
return f"{value:.0f}"
else:
return f"{value:,.0f}" if value >= 1000 else f"{value:.2f}"
def apply_brand_to_document(document_type: str, config: dict[str, Any]) -> dict[str, Any]:
"""
Main function to apply branding to any document type.
Args:
document_type: Type of document ('excel', 'powerpoint', 'pdf')
config: Document configuration
Returns:
Branded configuration
"""
formatter = BrandFormatter()
if document_type.lower() == "excel":
return formatter.format_excel(config)
elif document_type.lower() in ["powerpoint", "pptx"]:
return formatter.format_powerpoint(config)
elif document_type.lower() == "pdf":
return formatter.format_pdf(config)
else:
raise ValueError(f"Unsupported document type: {document_type}")
# Example usage
if __name__ == "__main__":
# Example Excel configuration
excel_config = {"title": "Quarterly Report", "sheets": ["Summary", "Details"]}
branded_excel = apply_brand_to_document("excel", excel_config)
print("Branded Excel Configuration:")
print(branded_excel)
# Example PowerPoint configuration
ppt_config = {"title": "Business Review", "num_slides": 10}
branded_ppt = apply_brand_to_document("powerpoint", ppt_config)
print("\nBranded PowerPoint Configuration:")
print(branded_ppt)
@@ -0,0 +1,325 @@
#!/usr/bin/env python3
"""
Brand Validation Script
Validates content against brand guidelines including colors, fonts, tone, and messaging.
"""
import json
import re
from dataclasses import asdict, dataclass
@dataclass
class BrandGuidelines:
"""Brand guidelines configuration"""
brand_name: str
primary_colors: list[str]
secondary_colors: list[str]
fonts: list[str]
tone_keywords: list[str]
prohibited_words: list[str]
tagline: str | None = None
logo_usage_rules: dict | None = None
@dataclass
class ValidationResult:
"""Result of brand validation"""
passed: bool
score: float
violations: list[str]
warnings: list[str]
suggestions: list[str]
class BrandValidator:
"""Validates content against brand guidelines"""
def __init__(self, guidelines: BrandGuidelines):
self.guidelines = guidelines
def validate_colors(self, content: str) -> tuple[list[str], list[str]]:
"""
Validate color usage in content (hex codes, RGB, color names)
Returns: (violations, warnings)
"""
violations = []
warnings = []
# Find hex colors
hex_pattern = r"#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}"
found_colors = re.findall(hex_pattern, content)
# Find RGB colors
rgb_pattern = r"rgb\s*\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)"
found_colors.extend(re.findall(rgb_pattern, content, re.IGNORECASE))
approved_colors = self.guidelines.primary_colors + self.guidelines.secondary_colors
for color in found_colors:
if color.upper() not in [c.upper() for c in approved_colors]:
violations.append(f"Unapproved color used: {color}")
return violations, warnings
def validate_fonts(self, content: str) -> tuple[list[str], list[str]]:
"""
Validate font usage in content
Returns: (violations, warnings)
"""
violations = []
warnings = []
# Common font specification patterns
font_patterns = [
r'font-family\s*:\s*["\']?([^;"\']+)["\']?',
r"font:\s*[^;]*\s+([A-Za-z][A-Za-z\s]+)(?:,|;|\s+\d)",
]
found_fonts = []
for pattern in font_patterns:
matches = re.findall(pattern, content, re.IGNORECASE)
found_fonts.extend(matches)
for font in found_fonts:
font_clean = font.strip().lower()
# Check if any approved font is in the found font string
if not any(approved.lower() in font_clean for approved in self.guidelines.fonts):
violations.append(f"Unapproved font used: {font}")
return violations, warnings
def validate_tone(self, content: str) -> tuple[list[str], list[str]]:
"""
Validate tone and messaging
Returns: (violations, warnings)
"""
violations = []
warnings = []
# Check for prohibited words
content_lower = content.lower()
for word in self.guidelines.prohibited_words:
if word.lower() in content_lower:
violations.append(f"Prohibited word/phrase used: '{word}'")
# Check for tone keywords (should have at least some)
tone_matches = sum(
1 for keyword in self.guidelines.tone_keywords if keyword.lower() in content_lower
)
if tone_matches == 0 and len(content) > 100:
warnings.append(
f"Content may not align with brand tone. "
f"Consider using terms like: {', '.join(self.guidelines.tone_keywords[:5])}"
)
return violations, warnings
def validate_brand_name(self, content: str) -> tuple[list[str], list[str]]:
"""
Validate brand name usage and capitalization
Returns: (violations, warnings)
"""
violations = []
warnings = []
# Find all variations of the brand name
brand_pattern = re.compile(re.escape(self.guidelines.brand_name), re.IGNORECASE)
matches = brand_pattern.findall(content)
for match in matches:
if match != self.guidelines.brand_name:
violations.append(
f"Incorrect brand name capitalization: '{match}' "
f"should be '{self.guidelines.brand_name}'"
)
return violations, warnings
def calculate_score(self, violations: list[str], warnings: list[str]) -> float:
"""Calculate compliance score (0-100)"""
violation_penalty = len(violations) * 10
warning_penalty = len(warnings) * 3
score = max(0, 100 - violation_penalty - warning_penalty)
return round(score, 2)
def generate_suggestions(self, violations: list[str], warnings: list[str]) -> list[str]:
"""Generate helpful suggestions based on violations and warnings"""
suggestions = []
if any("color" in v.lower() for v in violations):
suggestions.append(
f"Use approved colors: Primary: {', '.join(self.guidelines.primary_colors[:3])}"
)
if any("font" in v.lower() for v in violations):
suggestions.append(f"Use approved fonts: {', '.join(self.guidelines.fonts)}")
if any("tone" in w.lower() for w in warnings):
suggestions.append(
f"Incorporate brand tone keywords: {', '.join(self.guidelines.tone_keywords[:5])}"
)
if any("brand name" in v.lower() for v in violations):
suggestions.append(f"Always capitalize brand name as: {self.guidelines.brand_name}")
return suggestions
def validate(self, content: str) -> ValidationResult:
"""
Perform complete brand validation
Returns: ValidationResult
"""
all_violations = []
all_warnings = []
# Run all validation checks
color_v, color_w = self.validate_colors(content)
all_violations.extend(color_v)
all_warnings.extend(color_w)
font_v, font_w = self.validate_fonts(content)
all_violations.extend(font_v)
all_warnings.extend(font_w)
tone_v, tone_w = self.validate_tone(content)
all_violations.extend(tone_v)
all_warnings.extend(tone_w)
brand_v, brand_w = self.validate_brand_name(content)
all_violations.extend(brand_v)
all_warnings.extend(brand_w)
# Calculate score and generate suggestions
score = self.calculate_score(all_violations, all_warnings)
suggestions = self.generate_suggestions(all_violations, all_warnings)
return ValidationResult(
passed=len(all_violations) == 0,
score=score,
violations=all_violations,
warnings=all_warnings,
suggestions=suggestions,
)
def load_guidelines_from_json(filepath: str) -> BrandGuidelines:
"""
Load brand guidelines from JSON file
Args:
filepath: Path to JSON file containing brand guidelines
Returns:
BrandGuidelines object
Raises:
FileNotFoundError: If the file doesn't exist
json.JSONDecodeError: If the file contains invalid JSON
TypeError: If required fields are missing
"""
try:
with open(filepath) as f:
data = json.load(f)
return BrandGuidelines(**data)
except FileNotFoundError as e:
raise FileNotFoundError(f"Brand guidelines file not found: {filepath}") from e
except json.JSONDecodeError as e:
raise json.JSONDecodeError(
f"Invalid JSON in brand guidelines file: {e.msg}", e.doc, e.pos
) from e
except TypeError as e:
raise TypeError(f"Missing required fields in brand guidelines: {e}") from e
def get_acme_corporation_guidelines() -> BrandGuidelines:
"""
Get default Acme Corporation brand guidelines.
These guidelines match the standards defined in the SKILL.md reference.
Users should customize these for their own organization.
Returns:
BrandGuidelines object with Acme Corporation standards
"""
return BrandGuidelines(
brand_name="Acme Corporation",
primary_colors=["#0066CC", "#003366", "#FFFFFF"], # Acme Blue, Acme Navy, White
secondary_colors=[
"#28A745",
"#FFC107",
"#DC3545",
"#6C757D",
"#F8F9FA",
], # Success Green, Warning Amber, Error Red, Neutral Gray, Light Gray
fonts=["Segoe UI", "system-ui", "-apple-system", "sans-serif"],
tone_keywords=[
"innovation",
"excellence",
"professional",
"solutions",
"trusted",
"reliable",
],
prohibited_words=["cheap", "outdated", "inferior", "unprofessional", "sloppy"],
tagline="Innovation Through Excellence",
)
def main():
"""Example usage demonstrating brand validation"""
# Load Acme Corporation brand guidelines
# Users should customize this for their own organization
guidelines = get_acme_corporation_guidelines()
# Example content to validate (intentionally contains violations for demonstration)
test_content = """
Welcome to acme corporation!
We are a cheap solution provider with outdated technology.
Our innovation and excellence in professional solutions are trusted by many.
Contact us at: font-family: 'Comic Sans MS'
Color scheme: #FF0000
Background: rgb(255, 0, 0)
"""
# Validate
validator = BrandValidator(guidelines)
result = validator.validate(test_content)
# Print results
print("=" * 60)
print("BRAND VALIDATION REPORT")
print("=" * 60)
print(f"\nOverall Status: {'✓ PASSED' if result.passed else '✗ FAILED'}")
print(f"Compliance Score: {result.score}/100")
if result.violations:
print(f"\n❌ VIOLATIONS ({len(result.violations)}):")
for i, violation in enumerate(result.violations, 1):
print(f" {i}. {violation}")
if result.warnings:
print(f"\n⚠️ WARNINGS ({len(result.warnings)}):")
for i, warning in enumerate(result.warnings, 1):
print(f" {i}. {warning}")
if result.suggestions:
print("\n💡 SUGGESTIONS:")
for i, suggestion in enumerate(result.suggestions, 1):
print(f" {i}. {suggestion}")
print("\n" + "=" * 60)
# Return JSON for programmatic use
return asdict(result)
if __name__ == "__main__":
main()
+173
View File
@@ -0,0 +1,173 @@
---
name: creating-financial-models
description: This skill provides an advanced financial modeling suite with DCF analysis, sensitivity testing, Monte Carlo simulations, and scenario planning for investment decisions
---
# Financial Modeling Suite
A comprehensive financial modeling toolkit for investment analysis, valuation, and risk assessment using industry-standard methodologies.
## Core Capabilities
### 1. Discounted Cash Flow (DCF) Analysis
- Build complete DCF models with multiple growth scenarios
- Calculate terminal values using perpetuity growth and exit multiple methods
- Determine weighted average cost of capital (WACC)
- Generate enterprise and equity valuations
### 2. Sensitivity Analysis
- Test key assumptions impact on valuation
- Create data tables for multiple variables
- Generate tornado charts for sensitivity ranking
- Identify critical value drivers
### 3. Monte Carlo Simulation
- Run thousands of scenarios with probability distributions
- Model uncertainty in key inputs
- Generate confidence intervals for valuations
- Calculate probability of achieving targets
### 4. Scenario Planning
- Build best/base/worst case scenarios
- Model different economic environments
- Test strategic alternatives
- Compare outcome probabilities
## Input Requirements
### For DCF Analysis
- Historical financial statements (3-5 years)
- Revenue growth assumptions
- Operating margin projections
- Capital expenditure forecasts
- Working capital requirements
- Terminal growth rate or exit multiple
- Discount rate components (risk-free rate, beta, market premium)
### For Sensitivity Analysis
- Base case model
- Variable ranges to test
- Key metrics to track
### For Monte Carlo Simulation
- Probability distributions for uncertain variables
- Correlation assumptions between variables
- Number of iterations (typically 1,000-10,000)
### For Scenario Planning
- Scenario definitions and assumptions
- Probability weights for scenarios
- Key performance indicators to track
## Output Formats
### DCF Model Output
- Complete financial projections
- Free cash flow calculations
- Terminal value computation
- Enterprise and equity value summary
- Valuation multiples implied
- Excel workbook with full model
### Sensitivity Analysis Output
- Sensitivity tables showing value ranges
- Tornado chart of key drivers
- Break-even analysis
- Charts showing relationships
### Monte Carlo Output
- Probability distribution of valuations
- Confidence intervals (e.g., 90%, 95%)
- Statistical summary (mean, median, std dev)
- Risk metrics (VaR, probability of loss)
### Scenario Planning Output
- Scenario comparison table
- Probability-weighted expected values
- Decision tree visualization
- Risk-return profiles
## Model Types Supported
1. **Corporate Valuation**
- Mature companies with stable cash flows
- Growth companies with J-curve projections
- Turnaround situations
2. **Project Finance**
- Infrastructure projects
- Real estate developments
- Energy projects
3. **M&A Analysis**
- Acquisition valuations
- Synergy modeling
- Accretion/dilution analysis
4. **LBO Models**
- Leveraged buyout analysis
- Returns analysis (IRR, MOIC)
- Debt capacity assessment
## Best Practices Applied
### Modeling Standards
- Consistent formatting and structure
- Clear assumption documentation
- Separation of inputs, calculations, outputs
- Error checking and validation
- Version control and change tracking
### Valuation Principles
- Use multiple valuation methods for triangulation
- Apply appropriate risk adjustments
- Consider market comparables
- Validate against trading multiples
- Document key assumptions clearly
### Risk Management
- Identify and quantify key risks
- Use probability-weighted scenarios
- Stress test extreme cases
- Consider correlation effects
- Provide confidence intervals
## Example Usage
"Build a DCF model for this technology company using the attached financials"
"Run a Monte Carlo simulation on this acquisition model with 5,000 iterations"
"Create sensitivity analysis showing impact of growth rate and WACC on valuation"
"Develop three scenarios for this expansion project with probability weights"
## Scripts Included
- `dcf_model.py`: Complete DCF valuation engine
- `sensitivity_analysis.py`: Sensitivity testing framework
## Limitations and Disclaimers
- Models are only as good as their assumptions
- Past performance doesn't guarantee future results
- Market conditions can change rapidly
- Regulatory and tax changes may impact results
- Professional judgment required for interpretation
- Not a substitute for professional financial advice
## Quality Checks
The model automatically performs:
1. Balance sheet balancing checks
2. Cash flow reconciliation
3. Circular reference resolution
4. Sensitivity bound checking
5. Statistical validation of Monte Carlo results
## Updates and Maintenance
- Models use latest financial theory and practices
- Regular updates for market parameter defaults
- Incorporation of regulatory changes
- Continuous improvement based on usage patterns
@@ -0,0 +1,530 @@
"""
Discounted Cash Flow (DCF) valuation model.
Implements enterprise valuation using free cash flow projections.
"""
from typing import Any
import numpy as np
class DCFModel:
"""Build and calculate DCF valuation models."""
def __init__(self, company_name: str = "Company"):
"""
Initialize DCF model.
Args:
company_name: Name of the company being valued
"""
self.company_name = company_name
self.historical_financials = {}
self.projections = {}
self.assumptions = {}
self.wacc_components = {}
self.valuation_results = {}
def set_historical_financials(
self,
revenue: list[float],
ebitda: list[float],
capex: list[float],
nwc: list[float],
years: list[int],
):
"""
Set historical financial data.
Args:
revenue: Historical revenue
ebitda: Historical EBITDA
capex: Historical capital expenditure
nwc: Historical net working capital
years: Historical years
"""
self.historical_financials = {
"years": years,
"revenue": revenue,
"ebitda": ebitda,
"capex": capex,
"nwc": nwc,
"ebitda_margin": [ebitda[i] / revenue[i] for i in range(len(revenue))],
"capex_percent": [capex[i] / revenue[i] for i in range(len(revenue))],
}
def set_assumptions(
self,
projection_years: int = 5,
revenue_growth: list[float] = None,
ebitda_margin: list[float] = None,
tax_rate: float = 0.25,
capex_percent: list[float] = None,
nwc_percent: list[float] = None,
terminal_growth: float = 0.03,
):
"""
Set projection assumptions.
Args:
projection_years: Number of years to project
revenue_growth: Annual revenue growth rates
ebitda_margin: EBITDA margins by year
tax_rate: Corporate tax rate
capex_percent: Capex as % of revenue
nwc_percent: NWC as % of revenue
terminal_growth: Terminal growth rate
"""
if revenue_growth is None:
revenue_growth = [0.10] * projection_years # Default 10% growth
if ebitda_margin is None:
# Use historical average if available
if self.historical_financials:
avg_margin = np.mean(self.historical_financials["ebitda_margin"])
ebitda_margin = [avg_margin] * projection_years
else:
ebitda_margin = [0.20] * projection_years # Default 20% margin
if capex_percent is None:
capex_percent = [0.05] * projection_years # Default 5% of revenue
if nwc_percent is None:
nwc_percent = [0.10] * projection_years # Default 10% of revenue
self.assumptions = {
"projection_years": projection_years,
"revenue_growth": revenue_growth,
"ebitda_margin": ebitda_margin,
"tax_rate": tax_rate,
"capex_percent": capex_percent,
"nwc_percent": nwc_percent,
"terminal_growth": terminal_growth,
}
def calculate_wacc(
self,
risk_free_rate: float,
beta: float,
market_premium: float,
cost_of_debt: float,
debt_to_equity: float,
tax_rate: float | None = None,
) -> float:
"""
Calculate Weighted Average Cost of Capital (WACC).
Args:
risk_free_rate: Risk-free rate (e.g., 10-year treasury)
beta: Equity beta
market_premium: Equity market risk premium
cost_of_debt: Pre-tax cost of debt
debt_to_equity: Debt-to-equity ratio
tax_rate: Tax rate (uses assumption if not provided)
Returns:
WACC as decimal
"""
if tax_rate is None:
tax_rate = self.assumptions.get("tax_rate", 0.25)
# Calculate cost of equity using CAPM
cost_of_equity = risk_free_rate + beta * market_premium
# Calculate weights
equity_weight = 1 / (1 + debt_to_equity)
debt_weight = debt_to_equity / (1 + debt_to_equity)
# Calculate WACC
wacc = equity_weight * cost_of_equity + debt_weight * cost_of_debt * (1 - tax_rate)
self.wacc_components = {
"risk_free_rate": risk_free_rate,
"beta": beta,
"market_premium": market_premium,
"cost_of_equity": cost_of_equity,
"cost_of_debt": cost_of_debt,
"debt_to_equity": debt_to_equity,
"equity_weight": equity_weight,
"debt_weight": debt_weight,
"tax_rate": tax_rate,
"wacc": wacc,
}
return wacc
def project_cash_flows(self) -> dict[str, list[float]]:
"""
Project future cash flows based on assumptions.
Returns:
Dictionary with projected financials
"""
years = self.assumptions["projection_years"]
# Start with last historical revenue if available
if self.historical_financials and "revenue" in self.historical_financials:
base_revenue = self.historical_financials["revenue"][-1]
else:
base_revenue = 1000 # Default base
projections = {
"year": list(range(1, years + 1)),
"revenue": [],
"ebitda": [],
"ebit": [],
"tax": [],
"nopat": [],
"capex": [],
"nwc_change": [],
"fcf": [],
}
prev_revenue = base_revenue
prev_nwc = base_revenue * 0.10 # Initial NWC assumption
for i in range(years):
# Revenue
revenue = prev_revenue * (1 + self.assumptions["revenue_growth"][i])
projections["revenue"].append(revenue)
# EBITDA
ebitda = revenue * self.assumptions["ebitda_margin"][i]
projections["ebitda"].append(ebitda)
# EBIT (assuming depreciation = capex for simplicity)
depreciation = revenue * self.assumptions["capex_percent"][i]
ebit = ebitda - depreciation
projections["ebit"].append(ebit)
# Tax
tax = ebit * self.assumptions["tax_rate"]
projections["tax"].append(tax)
# NOPAT
nopat = ebit - tax
projections["nopat"].append(nopat)
# Capex
capex = revenue * self.assumptions["capex_percent"][i]
projections["capex"].append(capex)
# NWC change
nwc = revenue * self.assumptions["nwc_percent"][i]
nwc_change = nwc - prev_nwc
projections["nwc_change"].append(nwc_change)
# Free Cash Flow
fcf = nopat + depreciation - capex - nwc_change
projections["fcf"].append(fcf)
prev_revenue = revenue
prev_nwc = nwc
self.projections = projections
return projections
def calculate_terminal_value(
self, method: str = "growth", exit_multiple: float | None = None
) -> float:
"""
Calculate terminal value using perpetuity growth or exit multiple.
Args:
method: 'growth' for perpetuity growth, 'multiple' for exit multiple
exit_multiple: EV/EBITDA exit multiple (if using multiple method)
Returns:
Terminal value
"""
if not self.projections:
raise ValueError("Must project cash flows first")
if method == "growth":
# Gordon growth model
final_fcf = self.projections["fcf"][-1]
terminal_growth = self.assumptions["terminal_growth"]
wacc = self.wacc_components["wacc"]
# FCF in terminal year
terminal_fcf = final_fcf * (1 + terminal_growth)
# Terminal value
terminal_value = terminal_fcf / (wacc - terminal_growth)
elif method == "multiple":
if exit_multiple is None:
exit_multiple = 10 # Default EV/EBITDA multiple
final_ebitda = self.projections["ebitda"][-1]
terminal_value = final_ebitda * exit_multiple
else:
raise ValueError("Method must be 'growth' or 'multiple'")
return terminal_value
def calculate_enterprise_value(
self, terminal_method: str = "growth", exit_multiple: float | None = None
) -> dict[str, Any]:
"""
Calculate enterprise value by discounting cash flows.
Args:
terminal_method: Method for terminal value calculation
exit_multiple: Exit multiple if using multiple method
Returns:
Valuation results dictionary
"""
if not self.projections:
self.project_cash_flows()
if "wacc" not in self.wacc_components:
raise ValueError("Must calculate WACC first")
wacc = self.wacc_components["wacc"]
years = self.assumptions["projection_years"]
# Calculate PV of projected cash flows
pv_fcf = []
for i, fcf in enumerate(self.projections["fcf"]):
discount_factor = (1 + wacc) ** (i + 1)
pv = fcf / discount_factor
pv_fcf.append(pv)
total_pv_fcf = sum(pv_fcf)
# Calculate terminal value
terminal_value = self.calculate_terminal_value(terminal_method, exit_multiple)
# Discount terminal value
terminal_discount = (1 + wacc) ** years
pv_terminal = terminal_value / terminal_discount
# Enterprise value
enterprise_value = total_pv_fcf + pv_terminal
self.valuation_results = {
"enterprise_value": enterprise_value,
"pv_fcf": total_pv_fcf,
"pv_terminal": pv_terminal,
"terminal_value": terminal_value,
"terminal_method": terminal_method,
"pv_fcf_detail": pv_fcf,
"terminal_percent": pv_terminal / enterprise_value * 100,
}
return self.valuation_results
def calculate_equity_value(
self, net_debt: float, cash: float = 0, shares_outstanding: float = 100
) -> dict[str, Any]:
"""
Calculate equity value from enterprise value.
Args:
net_debt: Total debt minus cash
cash: Cash and equivalents (if not netted)
shares_outstanding: Number of shares (millions)
Returns:
Equity valuation metrics
"""
if "enterprise_value" not in self.valuation_results:
raise ValueError("Must calculate enterprise value first")
ev = self.valuation_results["enterprise_value"]
# Equity value = EV - Net Debt
equity_value = ev - net_debt + cash
# Per share value
value_per_share = equity_value / shares_outstanding if shares_outstanding > 0 else 0
equity_results = {
"equity_value": equity_value,
"shares_outstanding": shares_outstanding,
"value_per_share": value_per_share,
"net_debt": net_debt,
"cash": cash,
}
self.valuation_results.update(equity_results)
return equity_results
def sensitivity_analysis(
self, variable1: str, range1: list[float], variable2: str, range2: list[float]
) -> np.ndarray:
"""
Perform two-way sensitivity analysis on valuation.
Args:
variable1: First variable to test ('wacc', 'growth', 'margin')
range1: Range of values for variable1
variable2: Second variable to test
range2: Range of values for variable2
Returns:
2D array of valuations
"""
results = np.zeros((len(range1), len(range2)))
# Store original values
orig_wacc = self.wacc_components.get("wacc", 0.10)
orig_growth = self.assumptions.get("terminal_growth", 0.03)
orig_margin = self.assumptions.get("ebitda_margin", [0.20] * 5)
for i, val1 in enumerate(range1):
for j, val2 in enumerate(range2):
# Update first variable
if variable1 == "wacc":
self.wacc_components["wacc"] = val1
elif variable1 == "growth":
self.assumptions["terminal_growth"] = val1
elif variable1 == "margin":
self.assumptions["ebitda_margin"] = [val1] * len(orig_margin)
# Update second variable
if variable2 == "wacc":
self.wacc_components["wacc"] = val2
elif variable2 == "growth":
self.assumptions["terminal_growth"] = val2
elif variable2 == "margin":
self.assumptions["ebitda_margin"] = [val2] * len(orig_margin)
# Recalculate
self.project_cash_flows()
valuation = self.calculate_enterprise_value()
results[i, j] = valuation["enterprise_value"]
# Restore original values
self.wacc_components["wacc"] = orig_wacc
self.assumptions["terminal_growth"] = orig_growth
self.assumptions["ebitda_margin"] = orig_margin
return results
def generate_summary(self) -> str:
"""
Generate text summary of valuation results.
Returns:
Formatted summary string
"""
if not self.valuation_results:
return "No valuation results available. Run valuation first."
summary = [
f"DCF Valuation Summary - {self.company_name}",
"=" * 50,
"",
"Key Assumptions:",
f" Projection Period: {self.assumptions['projection_years']} years",
f" Revenue Growth: {np.mean(self.assumptions['revenue_growth']) * 100:.1f}% avg",
f" EBITDA Margin: {np.mean(self.assumptions['ebitda_margin']) * 100:.1f}% avg",
f" Terminal Growth: {self.assumptions['terminal_growth'] * 100:.1f}%",
f" WACC: {self.wacc_components['wacc'] * 100:.1f}%",
"",
"Valuation Results:",
f" Enterprise Value: ${self.valuation_results['enterprise_value']:,.0f}M",
f" PV of FCF: ${self.valuation_results['pv_fcf']:,.0f}M",
f" PV of Terminal: ${self.valuation_results['pv_terminal']:,.0f}M",
f" Terminal % of Value: {self.valuation_results['terminal_percent']:.1f}%",
"",
]
if "equity_value" in self.valuation_results:
summary.extend(
[
"Equity Valuation:",
f" Equity Value: ${self.valuation_results['equity_value']:,.0f}M",
f" Shares Outstanding: {self.valuation_results['shares_outstanding']:.0f}M",
f" Value per Share: ${self.valuation_results['value_per_share']:.2f}",
"",
]
)
return "\n".join(summary)
# Helper functions for common calculations
def calculate_beta(stock_returns: list[float], market_returns: list[float]) -> float:
"""
Calculate beta from return series.
Args:
stock_returns: Historical stock returns
market_returns: Historical market returns
Returns:
Beta coefficient
"""
covariance = np.cov(stock_returns, market_returns)[0, 1]
market_variance = np.var(market_returns)
beta = covariance / market_variance if market_variance != 0 else 1.0
return beta
def calculate_fcf_cagr(fcf_series: list[float]) -> float:
"""
Calculate compound annual growth rate of FCF.
Args:
fcf_series: Free cash flow time series
Returns:
CAGR as decimal
"""
if len(fcf_series) < 2:
return 0
years = len(fcf_series) - 1
if fcf_series[0] <= 0 or fcf_series[-1] <= 0:
return 0
cagr = (fcf_series[-1] / fcf_series[0]) ** (1 / years) - 1
return cagr
# Example usage
if __name__ == "__main__":
# Create model
model = DCFModel("TechCorp")
# Set historical data
model.set_historical_financials(
revenue=[800, 900, 1000],
ebitda=[160, 189, 220],
capex=[40, 45, 50],
nwc=[80, 90, 100],
years=[2022, 2023, 2024],
)
# Set assumptions
model.set_assumptions(
projection_years=5,
revenue_growth=[0.15, 0.12, 0.10, 0.08, 0.06],
ebitda_margin=[0.23, 0.24, 0.25, 0.25, 0.25],
tax_rate=0.25,
terminal_growth=0.03,
)
# Calculate WACC
model.calculate_wacc(
risk_free_rate=0.04, beta=1.2, market_premium=0.07, cost_of_debt=0.05, debt_to_equity=0.5
)
# Project cash flows
model.project_cash_flows()
# Calculate valuation
model.calculate_enterprise_value()
# Calculate equity value
model.calculate_equity_value(net_debt=200, shares_outstanding=50)
# Print summary
print(model.generate_summary())
@@ -0,0 +1,389 @@
"""
Sensitivity analysis module for financial models.
Tests impact of variable changes on key outputs.
"""
from collections.abc import Callable
from typing import Any
import numpy as np
import pandas as pd
class SensitivityAnalyzer:
"""Perform sensitivity analysis on financial models."""
def __init__(self, base_model: Any):
"""
Initialize sensitivity analyzer.
Args:
base_model: Base financial model to analyze
"""
self.base_model = base_model
self.base_output = None
self.sensitivity_results = {}
def one_way_sensitivity(
self,
variable_name: str,
base_value: float,
range_pct: float,
steps: int,
output_func: Callable,
model_update_func: Callable,
) -> pd.DataFrame:
"""
Perform one-way sensitivity analysis.
Args:
variable_name: Name of variable to test
base_value: Base case value
range_pct: +/- percentage range to test
steps: Number of steps in range
output_func: Function to calculate output metric
model_update_func: Function to update model with new value
Returns:
DataFrame with sensitivity results
"""
# Calculate range
min_val = base_value * (1 - range_pct)
max_val = base_value * (1 + range_pct)
test_values = np.linspace(min_val, max_val, steps)
results = []
for value in test_values:
# Update model
model_update_func(value)
# Calculate output
output = output_func()
results.append(
{
"variable": variable_name,
"value": value,
"pct_change": (value - base_value) / base_value * 100,
"output": output,
"output_change": output - self.base_output if self.base_output else 0,
}
)
# Reset to base
model_update_func(base_value)
return pd.DataFrame(results)
def two_way_sensitivity(
self,
var1_name: str,
var1_base: float,
var1_range: list[float],
var2_name: str,
var2_base: float,
var2_range: list[float],
output_func: Callable,
model_update_func: Callable,
) -> pd.DataFrame:
"""
Perform two-way sensitivity analysis.
Args:
var1_name: First variable name
var1_base: First variable base value
var1_range: Range of values for first variable
var2_name: Second variable name
var2_base: Second variable base value
var2_range: Range of values for second variable
output_func: Function to calculate output
model_update_func: Function to update model (takes var1, var2)
Returns:
DataFrame with two-way sensitivity table
"""
results = np.zeros((len(var1_range), len(var2_range)))
for i, val1 in enumerate(var1_range):
for j, val2 in enumerate(var2_range):
# Update both variables
model_update_func(val1, val2)
# Calculate output
results[i, j] = output_func()
# Reset to base
model_update_func(var1_base, var2_base)
# Create DataFrame
df = pd.DataFrame(
results,
index=[f"{var1_name}={v:.2%}" if v < 1 else f"{var1_name}={v:.1f}" for v in var1_range],
columns=[
f"{var2_name}={v:.2%}" if v < 1 else f"{var2_name}={v:.1f}" for v in var2_range
],
)
return df
def tornado_analysis(
self, variables: dict[str, dict[str, Any]], output_func: Callable
) -> pd.DataFrame:
"""
Create tornado diagram data showing relative impact of variables.
Args:
variables: Dictionary of variables with base, low, high values
output_func: Function to calculate output
Returns:
DataFrame sorted by impact magnitude
"""
# Store base output
self.base_output = output_func()
tornado_data = []
for var_name, var_info in variables.items():
# Test low value
var_info["update_func"](var_info["low"])
low_output = output_func()
# Test high value
var_info["update_func"](var_info["high"])
high_output = output_func()
# Reset to base
var_info["update_func"](var_info["base"])
# Calculate impact
impact = high_output - low_output
low_delta = low_output - self.base_output
high_delta = high_output - self.base_output
tornado_data.append(
{
"variable": var_name,
"base_value": var_info["base"],
"low_value": var_info["low"],
"high_value": var_info["high"],
"low_output": low_output,
"high_output": high_output,
"low_delta": low_delta,
"high_delta": high_delta,
"impact": abs(impact),
"impact_pct": abs(impact) / self.base_output * 100,
}
)
# Sort by impact
df = pd.DataFrame(tornado_data)
df = df.sort_values("impact", ascending=False)
return df
def scenario_analysis(
self,
scenarios: dict[str, dict[str, float]],
variable_updates: dict[str, Callable],
output_func: Callable,
probability_weights: dict[str, float] | None = None,
) -> pd.DataFrame:
"""
Analyze multiple scenarios with different variable combinations.
Args:
scenarios: Dictionary of scenarios with variable values
variable_updates: Functions to update each variable
output_func: Function to calculate output
probability_weights: Optional probability for each scenario
Returns:
DataFrame with scenario results
"""
results = []
for scenario_name, variables in scenarios.items():
# Update all variables for this scenario
for var_name, value in variables.items():
if var_name in variable_updates:
variable_updates[var_name](value)
# Calculate output
output = output_func()
# Get probability if provided
prob = (
probability_weights.get(scenario_name, 1 / len(scenarios))
if probability_weights
else 1 / len(scenarios)
)
results.append(
{
"scenario": scenario_name,
"probability": prob,
"output": output,
**variables, # Include all variable values
}
)
# Reset model (simplified - should restore all base values)
df = pd.DataFrame(results)
# Calculate expected value
df["weighted_output"] = df["output"] * df["probability"]
expected_value = df["weighted_output"].sum()
# Add summary row
summary = pd.DataFrame(
[
{
"scenario": "Expected Value",
"probability": 1.0,
"output": expected_value,
"weighted_output": expected_value,
}
]
)
df = pd.concat([df, summary], ignore_index=True)
return df
def breakeven_analysis(
self,
variable_name: str,
variable_update: Callable,
output_func: Callable,
target_value: float,
min_search: float,
max_search: float,
tolerance: float = 0.01,
) -> float:
"""
Find breakeven point where output equals target.
Args:
variable_name: Variable to adjust
variable_update: Function to update variable
output_func: Function to calculate output
target_value: Target output value
min_search: Minimum search range
max_search: Maximum search range
tolerance: Convergence tolerance
Returns:
Breakeven value of variable
"""
# Binary search for breakeven
low = min_search
high = max_search
while (high - low) > tolerance:
mid = (low + high) / 2
variable_update(mid)
output = output_func()
if abs(output - target_value) < tolerance:
return mid
elif output < target_value:
low = mid
else:
high = mid
return (low + high) / 2
def create_data_table(
row_variable: tuple[str, list[float], Callable],
col_variable: tuple[str, list[float], Callable],
output_func: Callable,
) -> pd.DataFrame:
"""
Create Excel-style data table for two variables.
Args:
row_variable: (name, values, update_function)
col_variable: (name, values, update_function)
output_func: Function to calculate output
Returns:
DataFrame formatted as data table
"""
row_name, row_values, row_update = row_variable
col_name, col_values, col_update = col_variable
results = np.zeros((len(row_values), len(col_values)))
for i, row_val in enumerate(row_values):
for j, col_val in enumerate(col_values):
row_update(row_val)
col_update(col_val)
results[i, j] = output_func()
df = pd.DataFrame(
results,
index=pd.Index(row_values, name=row_name),
columns=pd.Index(col_values, name=col_name),
)
return df
# Example usage
if __name__ == "__main__":
# Mock model for demonstration
class SimpleModel:
def __init__(self):
self.revenue = 1000
self.margin = 0.20
self.multiple = 10
def calculate_value(self):
ebitda = self.revenue * self.margin
return ebitda * self.multiple
# Create model and analyzer
model = SimpleModel()
analyzer = SensitivityAnalyzer(model)
# One-way sensitivity
results = analyzer.one_way_sensitivity(
variable_name="Revenue",
base_value=model.revenue,
range_pct=0.20,
steps=5,
output_func=model.calculate_value,
model_update_func=lambda x: setattr(model, "revenue", x),
)
print("One-Way Sensitivity Analysis:")
print(results)
# Tornado analysis
variables = {
"Revenue": {
"base": 1000,
"low": 800,
"high": 1200,
"update_func": lambda x: setattr(model, "revenue", x),
},
"Margin": {
"base": 0.20,
"low": 0.15,
"high": 0.25,
"update_func": lambda x: setattr(model, "margin", x),
},
"Multiple": {
"base": 10,
"low": 8,
"high": 12,
"update_func": lambda x: setattr(model, "multiple", x),
},
}
tornado = analyzer.tornado_analysis(variables, model.calculate_value)
print("\nTornado Analysis:")
print(tornado[["variable", "impact", "impact_pct"]])
Generated
+2400
View File
File diff suppressed because it is too large Load Diff