Investment Plans workspace
Open raw ↗
# -*- coding: utf-8 -*-
"""
export_model_json.py — projects both financial models and the trade study into one JSON file
for the Node deck builder.

OWNER: 01_System/export_model_json.py
REVIEW: whenever a model gains a field the deck reads.

WHY: build_deck.js is JavaScript and cannot import the Python models. Before this file existed
it read /tmp/model.json, which someone had written by hand in a shell — so the deck's figures
came from a file outside the project, outside configuration control, and gone the moment the
machine restarted (GOV-F3.11: no links to live files outside the project). The deck now reads a
generated projection inside the project, and build_all.py regenerates it before every deck build.
"""
import os, sys, json

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import model_params as M
import model_agedcare as AC
import trade_study as TS

# 02_Work/_scratch is for throwaway probes only — the placement map forbids anything another
# file depends on living there, and the deck depends on this. It is a generated projection,
# so it belongs in 01_System beside the other generated projections.
OUT = os.path.join(HERE, "model_export.json")


def build():
    R = M.results()
    A = AC.results()
    S = A["structure"]
    base, winner, runner, sens = TS.sensitivity()
    d = dict(R)
    d["base_cum"] = [r["cumulative_cash"] for r in R["base_rows"]]
    d["slow_cum"] = [r["cumulative_cash"] for r in R["slow_rows"]]
    d["paid_cum"] = [r["cumulative_cash"] for r in R["paid_rows"]]
    d["base_final"] = d["base_cum"][-1]
    d["slow_final"] = d["slow_cum"][-1]
    d["paid_final"] = d["paid_cum"][-1]
    d["agedcare"] = A
    d["structure"] = S
    d["trade"] = {
        "weights_record": TS.WEIGHTS_RECORD,
        "criteria": [{"key": k, "label": l, "weight": w} for k, l, w in TS.CRITERIA],
        "ranking": [{"option": k, "label": l, "score": sc} for k, l, sc in base],
        "margin": TS.margin(),
        "flips": [{"criterion": lab, "weight_now": wt,
                   "flips_at": (fl[0] if fl else None),
                   "hands_it_to": (fl[1] if fl else None),
                   "new_score": (fl[2] if fl else None)} for lab, wt, fl, _ in sens],
    }
    d["combined"] = {
        "one_off": round(R["one_off_core"][1] + A["one_off"][1], 2),
        "fixed_month": S["two_entities_fixed_month"],
        "runway_6mo": round(R["runway_6mo_core_no_draw"] + A["runway_6mo_no_draw"], 2),
        "working_capital_low": round(R["working_capital_at_300h"] + A["working_capital"], 2),
        "working_capital_high": round(R["working_capital_at_300h_30day"] + A["working_capital"], 2),
        "total_capital_low": round(R["runway_6mo_core_no_draw"] + A["runway_6mo_no_draw"]
                                   + R["working_capital_at_300h"] + A["working_capital"], 2),
        "total_capital_high": round(R["runway_6mo_core_no_draw"] + A["runway_6mo_no_draw"]
                                    + R["working_capital_at_300h_30day"] + A["working_capital"], 2),
    }
    return d


if __name__ == "__main__":
    os.makedirs(os.path.dirname(OUT), exist_ok=True)
    with open(OUT, "w", encoding="utf-8") as f:
        json.dump(build(), f, indent=1, default=str)
    print("wrote %s" % os.path.relpath(os.path.abspath(OUT), os.path.dirname(HERE)))