FIP転シミュレーターをMCP対応にした
——AIエージェントに叩かせて見つかった、制度の読み違いによるバグ
何をしたか
FIP転+蓄電池の事業性シミュレーター(pv-sim-fip)を
MCPサーバーとして公開した。
シリーズ3本目の記事だが、MCP化としてはこれが1件目で、
ここで作った設計を住宅用・産業用に横展開している。
エンドポイントはここだ。
https://hachinai-pv-sim-fip.hf.space/gradio_api/mcp/
FIP転とは何か
FIT認定の太陽光発電所をFIP制度に移行し、
蓄電池を併設して収益を最大化するビジネスモデルだ。
FITは固定価格買取なので、いつ発電しても単価は同じ。
FIPは市場価格(JEPXスポット)+プレミアムで売るので、
高く売れる時間帯に売るという戦略が意味を持つ。
だから蓄電池が効いてくる。
FIP収益 = JEPX市場売電収入(30分コマ価格×売電量)
+ プレミアム(基準価格 − 参照価格)
+ 非化石証書売却収入
− BG手数料
公開しているツール一覧
| ツール名 | 機能 |
|---|---|
list_stations |
対応地点一覧(NEDO METPV-20収録の50地点) |
get_jepx_stats |
JEPXスポット価格の統計(LP計算なし・即答) |
estimate_pv_generation |
発電量のみの簡易試算 |
validate_fip_params |
パラメータの妥当性チェック・正規化・警告提示 |
simulate_fip_case_a |
ケースA: 新規FIP発電所+蓄電池 |
simulate_fip_case_b |
ケースB: 既存FIT→FIP転+蓄電池後付け |
validate_grid_battery_params |
系統用蓄電池の検証 |
simulate_grid_battery |
系統用蓄電池(PVなし)単独の事業性 |
下2つの系統用蓄電池は別記事で扱う。
30秒で試す
MCPクライアントから
Claude Codeならこの1行で繋がる。
claude mcp add --scope user --transport http pv-sim-fip https://hachinai-pv-sim-fip.hf.space/gradio_api/mcp/
MCPクライアントを持っていなくても試せる
pip install gradio_client
まずはLP計算の要らないget_jepx_statsから。即答で返る。
from gradio_client import Client
client = Client("hachinai/pv-sim-fip")
stats = client.predict(
area="九州",
fiscal_years=[2023],
api_name="/get_jepx_stats",
)
print("年平均価格 :", stats["annual_avg_yen_per_kwh"], "円/kWh")
print("日次スプレッド :", stats["avg_daily_spread_yen_per_kwh"], "円/kWh")
print("0.01円近傍のコマ:", stats["near_zero_slots_ratio_pct"], "%")
年平均価格 : 9.14 円/kWh
日次スプレッド : 12.94 円/kWh
0.01円近傍のコマ: 13.39 %
この年平均9.14円/kWhが、そのまま後述の参照価格になる。
九州は年間13.4%のコマで価格が0.01円まで落ちていて、
これが蓄電池アービトラージの原資になる。
本計算(ケースA)はLPを解くので30〜90秒かかる。
result = client.predict(
station_no="82182", # 福岡
ppeak_kw=5000,
jepx_area="九州",
jepx_fiscal_years=[2023],
fip_base_price_yen_per_kwh=15, # 基準価格(公表値をそのまま)
battery_capacity_kwh=1000,
annual_curtailment_rate_pct=12.4,
api_name="/simulate_fip_case_a",
)
a = result["assumptions"]
print("参照価格 :", a["reference_price_yen_per_kwh"], "円/kWh")
print("実効プレミアム:", a["premium_effective_yen_per_kwh"], "円/kWh")
print("ゼロクリップ :", a["premium_clipped_at_zero"])
print("IRR :", result["kpis"]["project_irr_pct"], "%")
print("PVの捕捉率 :", result["annual"]["capture_rate_pct"], "%")
参照価格 : 9.14 円/kWh
実効プレミアム: 5.86 円/kWh
ゼロクリップ : False
IRR : -4.26 %
PVの捕捉率 : 66.2 %
指定しなかった引数はサーバ側のデフォルトが使われる。
jepx_area="東京" に変えて基準価格をデフォルトの9.6円に戻すと、
今度はpremium_clipped_at_zero: Trueが返る。
その意味は次節で説明する。
基準価格をプレミアムだと思い込んでいた
最初に、一番大きかった間違いの話を書く。
FIPのプレミアムは、こう決まる。
プレミアム = 基準価格 − 参照価格
基準価格は国が決める値(例: 2026年度・50kW以上地上設置で9.6円/kWh)。
参照価格は市場価格の平均から算定される値。
事業者が受け取る上乗せ分は、その差額だ。
ところが最初の実装では、ユーザーが入力した基準価格を
そのままプレミアムとして売電単価に足していた。
誤: 売電単価 = JEPX価格 + 基準価格 + 非化石証書 − BG手数料
正: 売電単価 = JEPX価格 + (基準価格 − 参照価格) + 非化石証書 − BG手数料
参照価格はJEPX年平均と同じオーダーなので、
この間違いは売電単価をおよそ2倍にする。
スモークテスト(九州・5MW+1MWh・基準価格15円/kWh)の
IRRは +7.11% → -3.03% に変わった。
黒字判定だった案件が赤字になる規模の誤りだ。
現在は、選択したエリア・年度のJEPX単純平均から
参照価格を自動算定し、差額をツール内部で計算している。
ユーザーは公表値の基準価格をそのまま入れればいい。
戻り値にも算定過程を明示するようにした。
{
"assumptions": {
"fip_base_price_yen_per_kwh": 15.0,
"reference_price_yen_per_kwh": 9.14,
"premium_effective_yen_per_kwh": 5.86,
"premium_clipped_at_zero": false
}
}
プレミアムがマイナスになったらどうするか
上の式をそのまま使うと、参照価格が基準価格を上回ったときに
プレミアムが負になる。
これは制度上あり得ない。FIPに「参照価格を超えた分を
事業者から徴収する」仕組みは無いからだ。
そこで max(0, 基準価格 − 参照価格) でクリップしている。
問題は、これが例外的なケースではないことだ。
デフォルトの基準価格9.6円/kWhは、2023〜2025年度平均では
全9エリアの参照価格を下回る(四国9.68円〜東京12.77円)。
つまりデフォルト設定では必ずこのクリップ経路を通る。
東京・2MWで実行すると、こう返ってくる。
{
"premium_effective_yen_per_kwh": 0.0,
"premium_clipped_at_zero": true
}
参照価格12.77円/kWhが基準価格9.60円/kWhを上回るため、
プレミアムを0円/kWhに制限しました(FIP制度上、参照価格
超過分を事業者から徴収する仕組みは存在しません)。
この条件ではFIPプレミアムによる収益上乗せは発生しません
黙って0にすると、エージェントは
「プレミアムが乗った上での試算結果」だと思って報告してしまう。
warningsとcaveatsの両方に出して、
「今回はプレミアムが効いていない」ことを必ず伝えるようにした。
実現単価(capture price)——蓄電池が効く場所は場所による
FIPで効いてくる指標をひとつ追加した。
PVが実際に捕捉できている市場価格だ。
太陽光は昼に発電する。ところがJEPXの昼間価格は
太陽光の大量導入で沈む。だからPVの実現単価は
市場の年平均を下回る。
実測するとこうなる。
| エリア | JEPX年平均 | PVの実現単価 | 捕捉率 |
|---|---|---|---|
| 九州(2023年度) | 9.14 円/kWh | 6.04 円/kWh | 66.2 % |
| 東京(2023-25年度) | 12.77 円/kWh | 11.40 円/kWh | 89.2 % |
九州はPVが市場価格の3分の1を取りこぼしている。
昼にPVが集中して価格を潰しているからだ。
その代わり日次スプレッドが大きい(九州2023年度で平均12.94円/kWh、
価格が0.01円まで落ちるコマが年間13.4%)。
捕捉率が低い=蓄電池で時間をずらす価値が大きい。
東京は逆で、捕捉率89.2%と取りこぼしが少ない代わりに
スプレッドが小さく、蓄電池の出番が少ない。
同じ「FIP転+蓄電池」でも処方箋が真逆になる。
これを1つの数字で示せるようにした。
ケースAとケースBで計算の意味が変わる
FIP転には2つの入り口がある。
ケースA: 新規FIP発電所+蓄電池
最初からFIP認定を取る。投資額はPV+蓄電池。
プレミアム期間は20年。
ケースB: 既存FIT→FIP転+蓄電池後付け
既設のFIT発電所に蓄電池を足してFIPに移す。
投資額は蓄電池のみ(PVはサンクコスト)。
そして重要なのが、
FIPプレミアムの交付期間はFIT残存期間を引き継ぐこと。
FIT20年契約の12年目に転換すれば、
プレミアムは残り9年しか出ない。
だからケースBのIRRは、
「FIP転した場合」と「FIT継続した場合」の
増分キャッシュフローで評価している。
蓄電池の追加投資に対するリターンを見るのが目的だからだ。
戻り値でも effective_premium_years: 9 のように
実際に効くプレミアム年数を返す。
「FIT転しない方がいい」と正直に返す
ケースBを実測するとこうなる。
条件: 東京・2MW・FIT単価36円/kWh・12年目にFIP転・蓄電池1MWh
| 項目 | 値 |
|---|---|
| FIT継続の20年運営CF | 842,783,951 円 |
| FIP転+蓄電池の20年運営CF | 343,213,824 円 |
| 差額 | −499,570,127 円 |
| 蓄電池投資を含めた損得 | −567,570,127 円 |
| 増分IRR | 計算不能(全期間マイナス) |
FIT36円は今のJEPX価格(東京平均12.77円)の3倍近い。
プレミアムもゼロにクリップされる条件なので、
FIT期間が残っているうちに転換する意味は全く無い。
シミュレーターとしては当たり前の結果を出しているだけだが、
「自社ツールで試算したら不利と出た」を隠さず返すことは
けっこう大事だと思っている。
特にエージェント経由だと、
返した数字がそのまま人間の判断材料になる。
なお、増分IRRが計算不能なときは、
project_irr_pct に無理やり数字を入れず null を返している。
IRRは全キャッシュフローが同符号なら存在しない。
存在しないものを返さないのは当然だが、
Newton法で適当な初期値から回すと
それらしい値が出てしまうことがあるので注意がいる。
(そもそも蓄電池のCFは投資→収益→廃止措置で
符号が2回変わるため複数の解を持ちうる。
粗くスキャンして全ての符号変化区間を二分法で解き、
ゼロに最も近い根を返す実装にしている。)
ケースAの実測値
条件: 福岡・5MW+蓄電池1MWh・九州エリア・
基準価格15円/kWh・JEPX 2023年度・出力制御12.4%
| 項目 | 値 |
|---|---|
| 年間発電量 | 5,036,405 kWh |
| 参照価格(自動算定) | 9.14 円/kWh |
| 実効プレミアム | +5.86 円/kWh |
| PVの実現単価 / 捕捉率 | 6.04 円/kWh / 66.2 % |
| 出力制御量(蓄電池なし) | 624,514 kWh |
| 出力制御量(蓄電池あり) | 445,850 kWh |
| 出力制御回避量 | 178,665 kWh |
| 蓄電池の増分収益(LP理論値) | 4,314,926 円/年 |
| 蓄電池の増分収益(実現率85%適用後) | 3,667,687 円/年 |
| Project IRR | −4.26 % |
| NPV | −406,475,690 円 |
LPの理論値をそのまま返さない
蓄電池の充放電最適化は、30分×365日=17,520コマの
線形計画問題として解いている(PuLP+CBC)。
ただしこのLPは1年分のJEPX価格を完全予見する前提だ。
実運用は前日の価格予測に基づくので、必ずこれを下回る。
そこで蓄電池の増分収益にだけ
arbitrage_realization_rate_pct(デフォルト85%)を掛けている。
ベースラインの市場直売収入には掛けない。
蓄電池が生む増分だけが予測精度の影響を受けるからだ。
透明性のため、理論値と補正後の値を両方返す。
{
"battery_annual_increment_theoretical_yen": 4314926,
"battery_annual_increment_realized_yen": 3667687,
"arbitrage_realization_rate_pct": 85.0
}
85%という数字自体に強い根拠があるわけではない。
だからこそ理論値を隠さず、
利用者が自分の見立てで割り戻せるようにしている。
出力抑制には2種類ある
実装していて気づいた非対称性の話も書いておく。
出力を絞る理由は2つある。
| 種別 | 発生条件 | 蓄電池で回避できるか |
|---|---|---|
| 強制出力制御 | 系統からの指令 | ○ 充電して制御解除後に売る |
| 経済的自主抑制 | 売電単価が負のコマ | × 蓄電池があっても発生する |
LPは元々どちらも選べたのだが、
比較対象の「蓄電池なし」ベースラインが
損失が出る価格帯でも全量売電していた。
つまり「自主抑制できること自体の価値」が
蓄電池の増分収益に混ざり込んでいた。
東京・プレミアム0の条件で増分の約2%、
プレミアムが負になる条件では約1割がこれだった。
現在は両者とも売電単価が負のコマで抑制する。
集計も分けている。
蓄電池の「出力制御回避量」は強制分だけで評価する。
自主抑制分まで足すと回避量が負値になって
意味が取れなくなるからだ。
{
"forced_curtailed_with_battery_kwh": 445850,
"economic_curtailed_with_battery_kwh": 0,
"curtailment_avoided_kwh": 178665
}
三菱総研の試算と突合する——「一致しない」の中身を分解する
経産省の検討会に出ている三菱総研の試算
(2024年度 第5回 定置用蓄電システム普及拡大検討会 資料3)と
数字を突き合わせる検証を入れている。
最初は「1.4pt以内で一致」していた。
ところが一次資料を精読したら、こちらの前提が3つ間違っていた。
- 基準価格15円をプレミアムだと解釈していた(前述のバグ)
- PCSの時間率を0.25Cだと思っていたが、資料には3時間率(0.333C)とあった
- JEPXを3年平均で使っていたが、資料は2023年度単年だった
3つとも直した結果、乖離は-7〜-9ptに拡大した。
以前の一致は、3つの誤りが偶然打ち消し合っていただけだった。
ここで大事なのは、拡大した後の乖離は
こちらの実装ミスではないということだ。
残差の内訳は分解できている。
収入段階で比べると、当社75,317円/kWh/20年に対し
三菱総研123,060円/kWh/20年で、比率61.2%。
IRRの差はほぼ全部この収入差から来ている。その内訳は3つ。
1. BG手数料2.0円/kWh(残差の約11%)
三菱総研の算定諸元にこの費用は載っていない。
こちらが保守的に計上しているだけで、実装の誤りではない。
20年で約10,900円/kWh分に相当する。
2. プレミアムの粒度(影響は軽微。検証済み)
三菱総研は月別プレミアム、こちらは年間一律値を使っている。
一見効きそうなので九州2023年度を月別集計して実測した。
| 参照価格の取り方 | 参照価格 | 実効プレミアム |
|---|---|---|
| 単純年平均(現行方式) | 9.14 円/kWh | +5.86 円/kWh |
| 発電量加重平均(月別化した場合の近似) | 8.93 円/kWh | +6.07 円/kWh |
月別プレミアム自体は+2.87円(11月)〜+8.98円(6月)と
3倍以上ばらつくのに、加重平均すると差は+0.20円/kWhしかない。
九州では発電量の多い4〜6月(参照価格が安くプレミアムが厚い)と
同じく発電量の多い8〜9月(参照価格が高くプレミアムが薄い)が
相殺してしまうためだ。
月別化は有力に見える論点だが、実測の結果見送りを確定した。
3. 出力制御の配分機構の違い(残差の主因)
1と2を足しても残差の約28%にしか届かない。残りがこれだ。
三菱総研は「その日の最安コマ(0.01円)で充電して
逸失プレミアムを回収する」設計なので、
蓄電池のスループットがほぼ全量プレミアム回収に直結する。
一方こちらは出力制御を月×時間帯の合成確率テーブルで配分しており、
蓄電池の充電窓と必ずしも一致しない。
実測すると、当社の蓄電池年間放電量297,679kWhのうち
出力制御回避が59.8%(178,132kWh)、残り約4割が純アービトラージだった。
つまりモデルの機構が違うだけで、
どちらかの計算が間違っているわけではない。
全CAPEX水準で乖離がほぼ一定(-7〜-9pt)だったことも、
特定条件のバグではなく一律のスケール差であることを裏付けている。
そして、この差を埋めにはいかないと決めた。
三菱総研の前提(1日1サイクル保証+最安コマ全量捕捉)は
かなり楽観的で、そこに数字を寄せるのは筋が悪い。
出力制御を合成テーブルではなく
JEPXの安値コマそのものから駆動する方式への変更は検討している。
ただしそれは**「三菱総研に数値を合わせるため」ではなく
「モデルの現実性そのものの改善」として**やるべきものだ。
公表試算と合わないとき、
慌てて自分の数字を寄せにいくのが一番まずい。
差の出どころを分解して、
どちらの前提がどう違うかまで書ければ、
「合わない」は失敗ではなく情報になる。
まとめ
- FIP転シミュレーターをMCPサーバーとして公開した(8ツール)
- 基準価格とプレミアムを取り違えていたバグを直した。
IRRが+7.11%→-3.03%変わる規模の誤りだった - プレミアムのゼロ下限クリップは例外処理ではなく、
デフォルト設定では必ず通る経路。だから必ず警告を返す - PVの実現単価(捕捉率)を出力に追加した。
九州66.2%・東京89.2%——蓄電池の処方箋が真逆になる - 「FIT継続の方が有利」という結果もそのまま返す。
増分IRRが存在しないときはnullを返す - LPの完全予見な理論値と、実現率85%を掛けた値を両方返す
- 公表試算との突合で乖離が拡大したが、
その内訳(BG手数料・プレミアム粒度・出力制御の配分機構)を
分解し、実装ミスではなくモデル前提の違いだと確認した。
数字を寄せにいく対応はしていない - MCPエンドポイント:
https://hachinai-pv-sim-fip.hf.space/gradio_api/mcp/
このSpaceには太陽光を伴わない系統用蓄電池単独の
試算ツールも入っている。UIを持たずMCPツールだけで
公開しているので、その話は別記事で書く。