mirror of
https://github.com/furyhawk/agent_alpha.git
synced 2026-07-20 10:15:33 +00:00
Refactor code structure for improved readability and maintainability
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
@@ -0,0 +1 @@
|
||||
3.12
|
||||
+106
@@ -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())
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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"]])
|
||||
Reference in New Issue
Block a user