Update core/ai_judgement.py
Browse files- core/ai_judgement.py +84 -141
core/ai_judgement.py
CHANGED
|
@@ -1,174 +1,117 @@
|
|
| 1 |
# core/ai_judgement.py
|
| 2 |
from __future__ import annotations
|
|
|
|
| 3 |
import os, json
|
| 4 |
-
from typing import Dict, Any, List
|
| 5 |
from openai import OpenAI
|
| 6 |
|
| 7 |
OPENAI_MODEL_TEXT = os.environ.get("OPENAI_TEXT_MODEL", "gpt-4o-mini")
|
| 8 |
|
| 9 |
-
def
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
def suggest_external_with_llm(fin: Dict[str, Any], company: str = "") -> Dict[str, Any]:
|
| 16 |
"""
|
| 17 |
-
|
| 18 |
-
|
| 19 |
"""
|
|
|
|
| 20 |
try:
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
[財務データ]
|
| 31 |
-
{json.dumps(fin, ensure_ascii=False)}
|
| 32 |
-
"""
|
| 33 |
-
cli = _client()
|
| 34 |
-
resp = cli.chat.completions.create(
|
| 35 |
-
model=OPENAI_MODEL_TEXT,
|
| 36 |
-
messages=[
|
| 37 |
-
{"role": "system", "content": "出力は厳密なJSON。説明不要。"},
|
| 38 |
-
{"role": "user", "content": prompt},
|
| 39 |
-
],
|
| 40 |
-
response_format={"type": "json_object"},
|
| 41 |
-
temperature=0.2,
|
| 42 |
-
)
|
| 43 |
-
data = json.loads(resp.choices[0].message.content)
|
| 44 |
-
# 型の安全化
|
| 45 |
-
out = {}
|
| 46 |
-
for k in ["市場の年成長率(%)", "主力商品数", "成長中主力商品数"]:
|
| 47 |
-
v = data.get(k, None)
|
| 48 |
-
if v is None: continue
|
| 49 |
-
try:
|
| 50 |
-
out[k] = float(str(v).replace(",",""))
|
| 51 |
-
except Exception:
|
| 52 |
-
continue
|
| 53 |
-
# 整合性
|
| 54 |
-
if ("主力商品数" in out) and ("成長中主力商品数" in out):
|
| 55 |
-
if out["成長中主力商品数"] > out["主力商品数"]:
|
| 56 |
-
out["成長中主力商品数"] = out["主力商品数"]
|
| 57 |
-
return out
|
| 58 |
except Exception:
|
| 59 |
-
|
|
|
|
| 60 |
|
| 61 |
-
def ai_evaluate(fin: Dict[str, Any],
|
| 62 |
"""
|
| 63 |
-
AI
|
| 64 |
-
|
| 65 |
"""
|
| 66 |
bs = fin.get("balance_sheet", {}) or {}
|
| 67 |
is_ = fin.get("income_statement", {}) or {}
|
| 68 |
cf = fin.get("cash_flows", {}) or {}
|
| 69 |
|
| 70 |
-
def _to_f(x):
|
| 71 |
-
if x in (None, "", "null"): return None
|
| 72 |
-
try: return float(str(x).replace(",","").replace("▲","-").replace("△","-"))
|
| 73 |
-
except Exception: return None
|
| 74 |
-
|
| 75 |
sales = _to_f(is_.get("sales"))
|
| 76 |
op = _to_f(is_.get("operating_income"))
|
| 77 |
-
|
| 78 |
-
assets = _to_f(bs.get("total_assets"))
|
| 79 |
-
liab = _to_f(bs.get("total_liabilities"))
|
| 80 |
-
equity = _to_f(bs.get("total_equity"))
|
| 81 |
ca = _to_f(bs.get("current_assets"))
|
| 82 |
cl = _to_f(bs.get("current_liabilities"))
|
|
|
|
|
|
|
|
|
|
| 83 |
ocf = _to_f(cf.get("operating_cash_flow"))
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
#
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
# ④成長余地: 市場成長+現在の収益性レベル
|
| 116 |
-
market_g = None
|
| 117 |
-
prod_factor = 5.0
|
| 118 |
-
if ext_df_like:
|
| 119 |
-
market_g = ext_df_like.get("市場の年成長率(%)")
|
| 120 |
-
grow_items = ext_df_like.get("成長中主力商品数")
|
| 121 |
-
prod_total = ext_df_like.get("主力商品数")
|
| 122 |
-
if prod_total:
|
| 123 |
-
r = (grow_items or 0)/prod_total
|
| 124 |
-
prod_factor = clamp01(10.0*r) # 成長中比率を0-10に
|
| 125 |
|
| 126 |
-
growth = (ramp(None if market_g is None else market_g/100.0, 0.15, -0.05) + prod_factor)/2
|
| 127 |
-
|
| 128 |
-
# ⑤将来リスク: 利益率の低さやレバレッジ上昇をマイナス評価
|
| 129 |
-
risk = 10.0 - ((ramp(margin, 0.12, 0.01) + ramp(lev, 0.3, 0.9))/2)
|
| 130 |
-
|
| 131 |
-
# 重み(外部評価と差別化)
|
| 132 |
-
weights = {
|
| 133 |
-
"効率性": 0.25,
|
| 134 |
-
"資金繰り耐性": 0.20,
|
| 135 |
-
"資本政策・希薄化": 0.20,
|
| 136 |
-
"成長余地": 0.20,
|
| 137 |
-
"将来リスク": 0.15,
|
| 138 |
-
}
|
| 139 |
cats = {
|
| 140 |
-
"効率性": eff,
|
| 141 |
-
"
|
|
|
|
|
|
|
|
|
|
| 142 |
}
|
| 143 |
-
|
| 144 |
-
total = round(total_0_10*10.0, 1) # 0-100換算
|
| 145 |
-
|
| 146 |
-
grade = "S" if total>=85 else "A" if total>=75 else "B" if total>=65 else "C" if total>=50 else "D"
|
| 147 |
-
|
| 148 |
-
# 短評(LLM)
|
| 149 |
-
memo = ""
|
| 150 |
-
try:
|
| 151 |
-
cli = _client()
|
| 152 |
-
memo_prompt = f"""以下の財務数値(推定含む)を踏まえ、「良い点3つ」「懸念3つ」「総評(100字以内)」を簡潔に日本語で:
|
| 153 |
-
[主要比率] 営業利益率={margin or '—'} / 総資産回転率={at or '—'} / 流動比率={cr or '—'}
|
| 154 |
-
[市場成長率(%)]={market_g or '—'} / 成長中商品比 ~ {prod_factor*10:.0f}% (概算)
|
| 155 |
-
"""
|
| 156 |
-
resp = cli.chat.completions.create(
|
| 157 |
-
model=OPENAI_MODEL_TEXT,
|
| 158 |
-
messages=[
|
| 159 |
-
{"role": "system", "content": "簡潔・中立な財務アナリスト。数値に即した短評を出力。"},
|
| 160 |
-
{"role": "user", "content": memo_prompt},
|
| 161 |
-
],
|
| 162 |
-
temperature=0.3,
|
| 163 |
-
)
|
| 164 |
-
memo = resp.choices[0].message.content.strip()
|
| 165 |
-
except Exception:
|
| 166 |
-
memo = "短評生成に失敗しました(ネットワーク/鍵設定をご確認ください)。"
|
| 167 |
|
| 168 |
return {
|
|
|
|
|
|
|
| 169 |
"ai_total": total,
|
| 170 |
-
"
|
| 171 |
-
"category_scores": {k: round(v*10.0,1) for k,v in cats.items()},
|
| 172 |
-
"weights": weights,
|
| 173 |
-
"memo": memo,
|
| 174 |
}
|
|
|
|
| 1 |
# core/ai_judgement.py
|
| 2 |
from __future__ import annotations
|
| 3 |
+
from typing import Dict, Any, Optional
|
| 4 |
import os, json
|
|
|
|
| 5 |
from openai import OpenAI
|
| 6 |
|
| 7 |
OPENAI_MODEL_TEXT = os.environ.get("OPENAI_TEXT_MODEL", "gpt-4o-mini")
|
| 8 |
|
| 9 |
+
def _to_f(x):
|
| 10 |
+
if x is None: return None
|
| 11 |
+
try: return float(str(x).replace(",","").replace("▲","-").replace("△","-"))
|
| 12 |
+
except Exception: return None
|
| 13 |
+
|
| 14 |
+
def _safe_div(a,b):
|
| 15 |
+
a=_to_f(a); b=_to_f(b)
|
| 16 |
+
if a is None or b is None or b==0: return None
|
| 17 |
+
return a/b
|
| 18 |
+
|
| 19 |
+
def _clamp01(x):
|
| 20 |
+
if x is None: return 0.5
|
| 21 |
+
return max(0.0, min(1.0, x))
|
| 22 |
+
|
| 23 |
+
def _score01(x, good, bad):
|
| 24 |
+
# good側=1.0, bad側=0.0 の線形
|
| 25 |
+
if x is None: return 0.5
|
| 26 |
+
if good > bad:
|
| 27 |
+
if x <= bad: return 0.0
|
| 28 |
+
if x >= good: return 1.0
|
| 29 |
+
return (x-bad)/(good-bad)
|
| 30 |
+
else:
|
| 31 |
+
if x >= bad: return 0.0
|
| 32 |
+
if x <= good: return 1.0
|
| 33 |
+
return (bad-x)/(bad-good)
|
| 34 |
|
| 35 |
def suggest_external_with_llm(fin: Dict[str, Any], company: str = "") -> Dict[str, Any]:
|
| 36 |
"""
|
| 37 |
+
外部入力の埋め草候補。LLM不要で作れる範囲はローカルで。
|
| 38 |
+
(例)月商(円)_再掲 を 売上/12 で推定、自己資本比率 を BS から計算 等
|
| 39 |
"""
|
| 40 |
+
out: Dict[str, Any] = {}
|
| 41 |
try:
|
| 42 |
+
bs = fin.get("balance_sheet", {}) or {}
|
| 43 |
+
is_ = fin.get("income_statement", {}) or {}
|
| 44 |
+
sales = _to_f(is_.get("sales"))
|
| 45 |
+
if sales is not None:
|
| 46 |
+
out["月商(円)_再掲"] = sales / 12.0
|
| 47 |
+
ta = _to_f(bs.get("total_assets")); te = _to_f(bs.get("total_equity"))
|
| 48 |
+
if ta and te is not None and ta>0:
|
| 49 |
+
out["自己資本比率(%)"] = te/ta*100.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
except Exception:
|
| 51 |
+
pass
|
| 52 |
+
return out
|
| 53 |
|
| 54 |
+
def ai_evaluate(fin: Dict[str, Any], ext_like: Dict[str, Any]) -> Dict[str, Any]:
|
| 55 |
"""
|
| 56 |
+
LLMに依存せず数式でAI評点を作る(別軸:効率・流動性・資本・成長余地・リスク)。
|
| 57 |
+
0-100に正規化、カテゴリ平均で合計点。
|
| 58 |
"""
|
| 59 |
bs = fin.get("balance_sheet", {}) or {}
|
| 60 |
is_ = fin.get("income_statement", {}) or {}
|
| 61 |
cf = fin.get("cash_flows", {}) or {}
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
sales = _to_f(is_.get("sales"))
|
| 64 |
op = _to_f(is_.get("operating_income"))
|
| 65 |
+
net = _to_f(is_.get("net_income"))
|
|
|
|
|
|
|
|
|
|
| 66 |
ca = _to_f(bs.get("current_assets"))
|
| 67 |
cl = _to_f(bs.get("current_liabilities"))
|
| 68 |
+
ta = _to_f(bs.get("total_assets"))
|
| 69 |
+
tl = _to_f(bs.get("total_liabilities"))
|
| 70 |
+
te = _to_f(bs.get("total_equity"))
|
| 71 |
ocf = _to_f(cf.get("operating_cash_flow"))
|
| 72 |
|
| 73 |
+
# 1) 効率性(営業利益率・最終利益率)
|
| 74 |
+
opm = _safe_div(op, sales) # 0.15良 / -0.05悪
|
| 75 |
+
npm = _safe_div(net, sales) # 0.10良 / -0.05悪
|
| 76 |
+
eff = 0.6 * _score01(opm, 0.15, -0.05) + 0.4 * _score01(npm, 0.10, -0.05)
|
| 77 |
+
|
| 78 |
+
# 2) 流動性(当座/流動比率+OCF/Sales)
|
| 79 |
+
cur = _safe_div(ca, cl) # 2.0良 / 0.7悪
|
| 80 |
+
ocf_ratio = _safe_div(ocf, sales) # 0.12良 / -0.02悪
|
| 81 |
+
liq = 0.6 * _score01(cur, 2.0, 0.7) + 0.4 * _score01(ocf_ratio, 0.12, -0.02)
|
| 82 |
+
|
| 83 |
+
# 3) 資本(自己資本比率・D/E)
|
| 84 |
+
eq_ratio = _safe_div(te, ta) # 0.40良 / 0.05悪
|
| 85 |
+
de = _safe_div(tl, te) # 0.5良 / 4.0悪(逆評価)
|
| 86 |
+
cap = 0.6 * _score01(eq_ratio, 0.40, 0.05) + 0.4 * _score01(de, 0.5, 4.0)
|
| 87 |
+
|
| 88 |
+
# 4) 成長余地(外部の市場成長&製品の数/成長中比の“軽ウェイト”)
|
| 89 |
+
mg = _to_f(ext_like.get("市場の年成長率(%)"))
|
| 90 |
+
prods = _to_f(ext_like.get("主力商品数")) or 0.0
|
| 91 |
+
prods_g = _to_f(ext_like.get("成長中主力商品数")) or 0.0
|
| 92 |
+
grow = 0.5 * _score01(None if mg is None else mg/100.0, 0.15, -0.05) \
|
| 93 |
+
+ 0.25 * _score01(prods, 4, 0) \
|
| 94 |
+
+ 0.25 * _score01((prods_g/(prods or 1.0)), 0.7, 0.0)
|
| 95 |
+
|
| 96 |
+
# 5) リスク(赤字/債務過多/OCFマイナス)
|
| 97 |
+
risk = 0.0
|
| 98 |
+
risk += 0.4 * (1.0 - _score01(npm, 0.10, -0.05))
|
| 99 |
+
risk += 0.3 * (1.0 - _score01(de, 0.5, 4.0))
|
| 100 |
+
risk += 0.3 * (1.0 - _score01(ocf_ratio, 0.12, -0.02))
|
| 101 |
+
risk = 1.0 - _clamp01(risk) # 高いほど良い
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
cats = {
|
| 104 |
+
"効率性": round(eff*100, 1),
|
| 105 |
+
"流動性": round(liq*100, 1),
|
| 106 |
+
"資本": round(cap*100, 1),
|
| 107 |
+
"成長余地": round(grow*100, 1),
|
| 108 |
+
"リスク耐性": round(risk*100, 1),
|
| 109 |
}
|
| 110 |
+
total = round(sum(cats.values())/len(cats), 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
return {
|
| 113 |
+
"name": "AI評点(数式・中立)",
|
| 114 |
+
"category_scores": cats,
|
| 115 |
"ai_total": total,
|
| 116 |
+
"notes": "LLM非依存。外部評価とは別軸(効率・流動性・資本・成長余地・リスク)で定量化。",
|
|
|
|
|
|
|
|
|
|
| 117 |
}
|