judging / finance_core.py
Corin1998's picture
Upload 7 files
21ff730 verified
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import Dict, Any, Optional, List, Tuple
from schemas import FinancialExtract, ExtractedPeriod, MultipleSuggestion, MarketOutlook
def _fmt_currency(x, currency: str) -> str:
if x is None:
return "—"
try:
absx = abs(x)
if absx >= 1e12: return f"{x/1e12:.2f}{currency}"
if absx >= 1e8: return f"{x/1e8:.2f}{currency}"
if absx >= 1e6: return f"{x/1e6:.2f}百万{currency}"
return f"{x:,.0f}{currency}"
except Exception:
return f"{x} {currency}"
def compute_ratios(extract: FinancialExtract) -> Dict[str, Any]:
latest: Optional[ExtractedPeriod] = extract.latest()
prev: Optional[ExtractedPeriod] = extract.previous()
currency = extract.currency or ""
ratios: Dict[str, Any] = {}
if latest:
revenue = latest.revenue
cogs = latest.cogs
ebit = latest.ebit
ebitda = latest.ebitda if latest.ebitda is not None else (latest.ebit + (latest.depreciation or 0.0) if latest.ebit is not None else None)
net_income = latest.net_income
cash = latest.cash_and_equivalents
ar = latest.accounts_receivable
inv = latest.inventory
ap = latest.accounts_payable
ca = latest.current_assets
cl = latest.current_liabilities
te = latest.total_equity
td = latest.total_debt
interest = latest.interest_expense
ta = latest.total_assets
gross_profit = (revenue - cogs) if revenue is not None and cogs is not None else None
gross_margin = (gross_profit / revenue) if revenue and gross_profit is not None else None
ebit_margin = (ebit / revenue) if revenue and ebit is not None else None
ebitda_margin = (ebitda / revenue) if revenue and ebitda is not None else None
current_ratio = (ca / cl) if ca and cl else None
quick_assets = (cash or 0.0) + (ar or 0.0)
quick_ratio = (quick_assets / cl) if quick_assets and cl else None
debt_to_equity = (td / te) if td is not None and te else None
interest_coverage = (ebit / interest) if ebit is not None and interest else None
# 追加KPI
roe = (net_income / te) if net_income is not None and te else None
roa = (net_income / ta) if net_income is not None and ta else None
cash_ratio = (cash / cl) if cash is not None and cl else None
net_debt = (td - (cash or 0.0)) if td is not None else None
dso = ( (ar / (revenue/365.0)) if ar is not None and revenue else None )
dio = ( (inv / ( (cogs or 0.0)/365.0)) if inv is not None and cogs else None )
dpo = ( (ap / ( (cogs or 0.0)/365.0)) if ap is not None and cogs else None )
ccc = ( (dso or 0) + (dio or 0) - (dpo or 0) ) if (dso is not None and dio is not None and dpo is not None) else None
ratios.update({
"revenue": _fmt_currency(revenue, currency),
"ebit": _fmt_currency(ebit, currency),
"ebitda": _fmt_currency(ebitda, currency),
"net_income": _fmt_currency(net_income, currency),
"gross_margin_pct": f"{gross_margin*100:.1f}%" if gross_margin is not None else "—",
"ebit_margin_pct": f"{ebit_margin*100:.1f}%" if ebit_margin is not None else "—",
"ebitda_margin_pct": f"{ebitda_margin*100:.1f}%" if ebitda_margin is not None else "—",
"current_ratio": f"{current_ratio:.2f}" if current_ratio is not None else "—",
"quick_ratio": f"{quick_ratio:.2f}" if quick_ratio is not None else "—",
"debt_to_equity": f"{debt_to_equity:.2f}" if debt_to_equity is not None else "—",
"interest_coverage": f"{interest_coverage:.2f}" if interest_coverage is not None else "—",
# 追加表示
"roe_pct": f"{roe*100:.1f}%" if roe is not None else "—",
"roa_pct": f"{roa*100:.1f}%" if roa is not None else "—",
"cash_ratio": f"{cash_ratio:.2f}" if cash_ratio is not None else "—",
"net_debt": _fmt_currency(net_debt, currency) if net_debt is not None else "—",
"dso_days": f"{dso:.0f}日" if dso is not None else "—",
"dio_days": f"{dio:.0f}日" if dio is not None else "—",
"dpo_days": f"{dpo:.0f}日" if dpo is not None else "—",
"ccc_days": f"{ccc:.0f}日" if ccc is not None else "—",
})
if latest and prev and latest.revenue and prev.revenue and prev.revenue != 0:
growth = (latest.revenue - prev.revenue) / abs(prev.revenue)
ratios["revenue_growth_pct"] = f"{growth*100:.1f}%"
ratios["_revenue_growth_float"] = growth
else:
ratios["revenue_growth_pct"] = "—"
ratios["_revenue_growth_float"] = None
ratios["_latest_currency"] = extract.currency or "JPY"
ratios["_latest_ebitda"] = (latest.ebitda if latest and latest.ebitda is not None else
(latest.ebit + (latest.depreciation or 0.0) if latest and latest.ebit is not None else None))
ratios["_latest"] = latest.dict() if latest else {}
return ratios
def _risk_score(latest: ExtractedPeriod, ratios: Dict[str, Any], policies: Dict[str, Any]) -> float:
score = 50.0
ca, cl = latest.current_assets, latest.current_liabilities
if ca and cl:
cr = ca / cl
if cr >= 2.0: score += 10
elif cr >= 1.5: score += 6
elif cr >= 1.2: score += 3
elif cr < 1.0: score -= 10
td, te = latest.total_debt, latest.total_equity
if td is not None and te:
de = td / te
if de <= 0.5: score += 8
elif de <= 1.0: score += 4
elif de > 2.0: score -= 8
ebit, interest = latest.ebit, latest.interest_expense
if ebit is not None and interest:
ic = ebit / interest
if ic >= 6: score += 10
elif ic >= 3: score += 6
elif ic >= 1.5: score += 3
elif ic < 1.0: score -= 12
revenue = latest.revenue
net = latest.net_income
if revenue and net is not None:
nm = net / revenue
if nm >= 0.1: score += 6
elif nm >= 0.03: score += 3
elif nm < 0: score -= 8
g = ratios.get("_revenue_growth_float")
if g is not None:
if g >= 0.2: score += 6
elif g >= 0.05: score += 3
elif g < 0: score -= 5
return max(0.0, min(100.0, score))
def _rating(score: float) -> str:
if score >= 85: return "A"
if score >= 70: return "B"
if score >= 55: return "C"
if score >= 40: return "D"
return "E"
def credit_decision(extract: FinancialExtract, ratios: Dict[str, Any], policies: Dict[str, Any]) -> Dict[str, Any]:
latest = extract.latest()
currency = extract.currency or "JPY"
if not latest:
return {"rating": "E", "risk_score": 0, "site_days": 0,
"transaction_limit": 0.0, "transaction_limit_display": _fmt_currency(0.0, currency),
"review_cycle": "取引前入金(与信不可)"}
score = _risk_score(latest, ratios, policies)
rating = _rating(score)
site_map = {"A": 90, "B": 60, "C": 45, "D": 30, "E": 0}
review_map = {"A": "年1回", "B": "半年毎", "C": "四半期毎", "D": "月次", "E": "取引ごと"}
revenue = latest.revenue or 0.0
working_capital = (latest.current_assets or 0.0) - (latest.current_liabilities or 0.0)
try:
cr_val = float(ratios.get("current_ratio", "0").replace("—","0"))
except Exception:
cr_val = 0.0
liquidity_factor = 0.4 + 0.6 * (min(1.0, max(0.0, cr_val / 2.0)))
risk_factor = 0.5 + 0.5 * (score / 100.0)
site_days = site_map[rating]
base_limit = (revenue / 12.0) * (site_days / 30.0) * 0.6
cap = max(0.0, working_capital) * 0.4
tx_limit = min(cap, base_limit * liquidity_factor * risk_factor)
tx_limit = max(0.0, tx_limit)
return {"rating": rating, "risk_score": round(score, 1), "site_days": site_days,
"transaction_limit": tx_limit, "transaction_limit_display": _fmt_currency(tx_limit, currency),
"review_cycle": review_map[rating]}
def _annuity_factor(rate: float, years: int) -> float:
r = rate / 100.0
n = max(1, years)
if r <= 0: return 1.0 / n
return r / (1 - (1 + r) ** (-n))
def loan_decision(extract: FinancialExtract, ratios: Dict[str, Any], base_rate_pct: float, policies: Dict[str, Any]) -> Dict[str, Any]:
latest = extract.latest()
currency = extract.currency or "JPY"
if not latest:
return {"max_principal": 0.0, "max_principal_display": _fmt_currency(0.0, currency),
"term_years": 0, "interest_rate_pct": base_rate_pct, "target_dscr": 0.0}
score = _risk_score(latest, ratios, policies)
rating = _rating(score)
rp = {"A": 0.5, "B": 1.0, "C": 2.0, "D": 3.5, "E": 0.0}[rating]
if rating == "E":
return {"max_principal": 0.0, "max_principal_display": _fmt_currency(0.0, currency),
"term_years": 0, "interest_rate_pct": 0.0, "target_dscr": 0.0}
interest_rate_pct = base_rate_pct + rp
term_years = {"A": 5, "B": 4, "C": 3, "D": 1}[rating]
target_dscr = {"A": 1.6, "B": 1.4, "C": 1.3, "D": 1.2}[rating]
ebitda = ratios.get("_latest_ebitda")
if not ebitda or ebitda <= 0:
return {"max_principal": 0.0, "max_principal_display": _fmt_currency(0.0, currency),
"term_years": term_years, "interest_rate_pct": interest_rate_pct, "target_dscr": target_dscr}
ann = _annuity_factor(interest_rate_pct, term_years)
annual_debt_service_cap = ebitda / target_dscr
max_principal = max(0.0, annual_debt_service_cap / ann)
ta = latest.total_assets or 0.0
cash = latest.cash_and_equivalents or 0.0
nta_cap = max(0.0, (ta - cash) * 0.5)
max_principal = min(max_principal, nta_cap)
return {"max_principal": max_principal, "max_principal_display": _fmt_currency(max_principal, currency),
"term_years": term_years, "interest_rate_pct": round(interest_rate_pct, 2), "target_dscr": target_dscr}
def _market_factor_from_score(score: int) -> Tuple[float, List[float]]:
# score別の投資シナリオ倍率(保守/基準/強気)
table = {
1: (0.85, [0.75, 0.90, 1.05]),
2: (0.93, [0.85, 0.95, 1.10]),
3: (1.00, [0.90, 1.00, 1.15]),
4: (1.07, [0.95, 1.05, 1.25]),
5: (1.15, [1.00, 1.10, 1.35]),
}
return table.get(int(score) if score else 3, (1.0, [0.9, 1.0, 1.15]))
def investment_decision(
extract: FinancialExtract,
ratios: Dict[str, Any],
policies: Dict[str, Any],
multiples: Optional[MultipleSuggestion],
market_outlook: Optional[MarketOutlook] = None,
) -> Dict[str, Any]:
latest = extract.latest()
currency = extract.currency or "JPY"
if not latest:
return {"ev": 0.0, "ev_display": _fmt_currency(0.0, currency),
"market_cap": 0.0, "market_cap_display": _fmt_currency(0.0, currency),
"recommended_check_size": 0.0, "recommended_check_size_display": _fmt_currency(0.0, currency),
"attractiveness": 1, "growth_label": "Low"}
g = ratios.get("_revenue_growth_float")
if g is None: glabel = "Unknown"
elif g >= 0.25: glabel = "High"
elif g >= 0.05: glabel = "Medium"
else: glabel = "Low"
ebitda = latest.ebitda if latest.ebitda is not None else (latest.ebit + (latest.depreciation or 0.0) if latest.ebit is not None else None)
revenue = latest.revenue or 0.0
total_debt = latest.total_debt or 0.0
cash = latest.cash_and_equivalents or 0.0
net_debt = max(0.0, total_debt - cash)
if multiples and ebitda and ebitda > 0:
ev = ebitda * multiples.ebitda_multiple
elif ebitda and ebitda > 0:
ev = ebitda * 8.0
else:
rev_mult = multiples.revenue_multiple if multiples else 1.5
ev = revenue * rev_mult
market_cap = max(0.0, ev - net_debt)
score = _risk_score(latest, ratios, policies)
rating = _rating(score)
pct = {"A": 0.15, "B": 0.12, "C": 0.08, "D": 0.04, "E": 0.0}[rating]
base_check = market_cap * pct
attractiveness = 1
if rating == "A": attractiveness = 5 if glabel == "High" else 4
elif rating == "B": attractiveness = 4 if glabel != "Low" else 3
elif rating == "C": attractiveness = 3 if glabel == "High" else 2
elif rating == "D": attractiveness = 1
# 市場期待の反映(基本倍率+シナリオ)
market_factor, scenarios = (1.0, [0.9, 1.0, 1.15])
if market_outlook:
market_factor, scenarios = _market_factor_from_score(market_outlook.expectation_score)
if market_outlook.expectation_score >= 4:
attractiveness = min(5, attractiveness + 1)
elif market_outlook.expectation_score <= 2:
attractiveness = max(1, attractiveness - 1)
check = base_check * market_factor
# シナリオテーブル
scenario_rows = []
for label, fac in zip(["保守", "基準", "強気"], scenarios):
scenario_rows.append({
"label": label,
"factor": fac,
"check_size": base_check * fac,
"check_size_display": _fmt_currency(base_check * fac, currency),
})
return {
"ev": ev, "ev_display": _fmt_currency(ev, currency),
"market_cap": market_cap, "market_cap_display": _fmt_currency(market_cap, currency),
"recommended_check_size": check, "recommended_check_size_display": _fmt_currency(check, currency),
"attractiveness": attractiveness, "growth_label": glabel,
"market_factor": round(market_factor, 2),
"scenarios": scenario_rows,
}
def build_report_dict(
extract: FinancialExtract,
ratios: Dict[str, Any],
decisions: Dict[str, Any],
unit_info: Optional[Dict[str, Any]] = None,
market_outlook: Optional[MarketOutlook] = None,
) -> Dict[str, Any]:
out = {
"metadata": {
"company_name": extract.company_name,
"industry": extract.industry,
"currency": extract.currency,
"fiscal_year_end": extract.fiscal_year_end
},
"extracted": extract.dict(),
"ratios": ratios,
"decisions": decisions,
"disclaimer": "本ツールはAIによる推定・一般的な計算式に基づく参考提案であり、投資勧誘・融資約定・与信保証を目的としたものではありません。最終判断は自己責任で、必要に応じて専門家の確認を行ってください。",
}
if unit_info: out["unit_detection"] = unit_info
if market_outlook: out["market_outlook"] = market_outlook.model_dump()
return out