code wiki / (root) / nx_finance_svc.nx

nx_finance_svc.nx source

↩ module page · 205 lines · 9939 B

1// nx_finance_svc.nx -- FINANCE SERVICE FACADE. One agentic MCP tool over the sovereign finance stack. 2// 3// This is the MCP / API / AGENTIC / WORKFLOW-READY / SCALABLE layer the gate-proven libraries did not yet 4// have. It composes the integer-exact finance modules (nx_dcf, nx_bond, nx_taxlot, nx_amort) behind a verb- 5// dispatched JSON contract inherited from nx_service, adding NO new domain logic -- every number comes from a 6// gated module, so the facade cannot drift from the math it exposes (Rule 15 DRY, Rule 6 OOP, Rule 22 compose). 7// 8// MCP-ready: a single fork-exec organ, stable name + argv verb contract, registerable in the allowlist. 9// API-ready: pure stdout JSON, versioned envelope, CORS-safe structured errors (400/404/422). 10// agentic-ready: the `describe` verb emits this facade's verb catalog with arg shapes (tools/list analog). 11// workflow-ready: every verb is a PURE function of its args returning a self-contained JSON object. 12// scalable: stateless; a call is a fork-exec; concurrency is free. 13// 14// *THE FAIL-CLOSED PROPERTY SURVIVES THE FACADE: a domain refusal sentinel (the DCF growth>=rate trap, an 15// unreachable bond yield, a bad tax-lot or amortization input) is mapped to a structured 422 REFUSED / 400 16// BAD_ARGS error, never a bogus number in the data. An agent sees the refusal as a first-class outcome. 17// 18// USAGE: nx_finance_svc <verb> [args...] 19// genealogy_id: finance_spine + service_infrastructure 20 21import "nx_syscalls.nx" 22import "nx_service.nx" 23import "nx_grounding.nx" 24import "nx_dcf_lib.nx" 25import "nx_bond_lib.nx" 26import "nx_taxlot_lib.nx" 27import "nx_amort_lib.nx" 28const FIN_MAGIC_8192: i64 = 8192 29 30const FIN_SVC: *u8 = "finance" as *u8 31 32// ===== describe: agentic self-discovery ============================== 33func fin_describe(j: *NxJson) -> i64 { 34 nsvc_ok_open(j, FIN_SVC, "describe" as *u8) 35 nj_kv_str(j, "service" as *u8, "sovereign finance: valuation, fixed income, tax, lending" as *u8) 36 nj_comma(j) 37 nj_kv_int(j, "verb_count" as *u8, 7) 38 nj_comma(j) 39 nj_kv_bool(j, "integer_exact" as *u8, 1) 40 nj_comma(j) 41 nj_key(j, "verbs" as *u8) 42 nj_puts(j, "[" as *u8) 43 nj_puts(j, "{\"verb\":\"dcf.verdict\",\"args\":[\"intrinsic_cents:int\",\"price_cents:int\"]}," as *u8) 44 nj_puts(j, "{\"verb\":\"dcf.terminal\",\"args\":[\"final_cf:int\",\"growth_bp:int\",\"rate_bp:int\"]}," as *u8) 45 nj_puts(j, "{\"verb\":\"bond.price\",\"args\":[\"coupon_bp:int\",\"face:int\",\"periods:int\",\"ytm_bp:int\"]}," as *u8) 46 nj_puts(j, "{\"verb\":\"bond.ytm\",\"args\":[\"price:int\",\"coupon_bp:int\",\"face:int\",\"periods:int\"]}," as *u8) 47 nj_puts(j, "{\"verb\":\"bond.duration\",\"args\":[\"coupon_bp:int\",\"face:int\",\"periods:int\",\"ytm_bp:int\"]}," as *u8) 48 nj_puts(j, "{\"verb\":\"taxlot.wash\",\"args\":[\"loss:int\",\"sale_day:int\",\"repl_day:int\",\"repl_shares:int\",\"sold_shares:int\"]}," as *u8) 49 nj_puts(j, "{\"verb\":\"amort.schedule\",\"args\":[\"principal_cents:int\",\"annual_bp:int\",\"term_months:int\"]}" as *u8) 50 nj_puts(j, "]" as *u8) 51 nsvc_ok_close_g(j, GND_ASSERTED) 52 return 0 53} 54 55// ===== dcf.verdict =================================================== 56func fin_dcf_verdict(j: *NxJson, intrinsic: i64, price: i64) -> i64 { 57 let m: i64 = dcf_margin_of_safety_bp(intrinsic, price) 58 if m == DCF_BAD { 59 nsvc_error(j, FIN_SVC, "dcf.verdict" as *u8, NSVC_ERR_BAD_ARGS, "price must be positive" as *u8) 60 return 0 61 } 62 let v: i64 = dcf_verdict(intrinsic, price) 63 nsvc_ok_open(j, FIN_SVC, "dcf.verdict" as *u8) 64 nj_kv_int(j, "intrinsic_cents" as *u8, intrinsic) 65 nj_comma(j) 66 nj_kv_int(j, "price_cents" as *u8, price) 67 nj_comma(j) 68 nj_kv_int(j, "margin_of_safety_bp" as *u8, m) 69 nj_comma(j) 70 nj_kv_str(j, "verdict" as *u8, dcf_verdict_str(v)) 71 nsvc_ok_close_g(j, GND_ASSERTED) 72 return 0 73} 74 75// ===== dcf.terminal (the fail-closed trap) =========================== 76func fin_dcf_terminal(j: *NxJson, final_cf: i64, growth_bp: i64, rate_bp: i64) -> i64 { 77 let t: i64 = dcf_terminal(final_cf, growth_bp, rate_bp) 78 if t == DCF_UNDEFINED { 79 nsvc_error(j, FIN_SVC, "dcf.terminal" as *u8, NSVC_ERR_REFUSED, "terminal value undefined: growth rate >= discount rate" as *u8) 80 return 0 81 } 82 nsvc_ok_open(j, FIN_SVC, "dcf.terminal" as *u8) 83 nj_kv_int(j, "terminal_value" as *u8, t) 84 nsvc_ok_close_g(j, GND_ASSERTED) 85 return 0 86} 87 88// ===== bond.price ==================================================== 89func fin_bond_price(j: *NxJson, coupon_bp: i64, face: i64, periods: i64, ytm_bp: i64) -> i64 { 90 if face <= 0 { 91 nsvc_error(j, FIN_SVC, "bond.price" as *u8, NSVC_ERR_BAD_ARGS, "face must be positive" as *u8) 92 return 0 93 } 94 let p: i64 = bond_price(coupon_bp, face, periods, ytm_bp) 95 nsvc_ok_open(j, FIN_SVC, "bond.price" as *u8) 96 nj_kv_int(j, "price" as *u8, p) 97 nj_comma(j) 98 nj_kv_str(j, "class" as *u8, bond_price_class(p, face)) 99 nsvc_ok_close_g(j, GND_ASSERTED) 100 return 0 101} 102 103// ===== bond.ytm (fail-closed no-converge) ============================ 104func fin_bond_ytm(j: *NxJson, price: i64, coupon_bp: i64, face: i64, periods: i64) -> i64 { 105 let y: i64 = bond_ytm(price, coupon_bp, face, periods) 106 if y == BOND_BAD { 107 nsvc_error(j, FIN_SVC, "bond.ytm" as *u8, NSVC_ERR_BAD_ARGS, "price must be positive" as *u8) 108 return 0 109 } 110 if y == BOND_NO_CONVERGE { 111 nsvc_error(j, FIN_SVC, "bond.ytm" as *u8, NSVC_ERR_REFUSED, "target price unreachable within the yield search bounds" as *u8) 112 return 0 113 } 114 nsvc_ok_open(j, FIN_SVC, "bond.ytm" as *u8) 115 nj_kv_int(j, "ytm_bp" as *u8, y) 116 nsvc_ok_close_g(j, GND_ASSERTED) 117 return 0 118} 119 120// ===== bond.duration ================================================= 121func fin_bond_duration(j: *NxJson, coupon_bp: i64, face: i64, periods: i64, ytm_bp: i64) -> i64 { 122 let mac: i64 = bond_macaulay_duration(coupon_bp, face, periods, ytm_bp) 123 if mac == BOND_BAD { 124 nsvc_error(j, FIN_SVC, "bond.duration" as *u8, NSVC_ERR_BAD_ARGS, "degenerate bond (no positive present value)" as *u8) 125 return 0 126 } 127 let modd: i64 = bond_modified_duration(mac, ytm_bp) 128 nsvc_ok_open(j, FIN_SVC, "bond.duration" as *u8) 129 nj_kv_int(j, "macaulay_x1000" as *u8, mac) 130 nj_comma(j) 131 nj_kv_int(j, "modified_x1000" as *u8, modd) 132 nsvc_ok_close_g(j, GND_ASSERTED) 133 return 0 134} 135 136// ===== taxlot.wash =================================================== 137func fin_taxlot_wash(j: *NxJson, loss: i64, sale_day: i64, repl_day: i64, repl_shares: i64, sold_shares: i64) -> i64 { 138 let dis: i64 = tl_wash_disallowed(loss, sale_day, repl_day, repl_shares, sold_shares) 139 if dis == TL_BAD { 140 nsvc_error(j, FIN_SVC, "taxlot.wash" as *u8, NSVC_ERR_BAD_ARGS, "sold_shares must be positive" as *u8) 141 return 0 142 } 143 let alw: i64 = tl_allowed_loss(loss, sale_day, repl_day, repl_shares, sold_shares) 144 nsvc_ok_open(j, FIN_SVC, "taxlot.wash" as *u8) 145 nj_kv_int(j, "disallowed_loss" as *u8, dis) 146 nj_comma(j) 147 nj_kv_int(j, "allowed_loss" as *u8, alw) 148 nsvc_ok_close_g(j, GND_ASSERTED) 149 return 0 150} 151 152// ===== amort.schedule ================================================ 153func fin_amort_schedule(j: *NxJson, principal: i64, annual_bp: i64, term: i64) -> i64 { 154 let pay: i64 = amort_level_payment(principal, annual_bp, term) 155 if pay == AMORT_BAD { 156 nsvc_error(j, FIN_SVC, "amort.schedule" as *u8, NSVC_ERR_BAD_ARGS, "principal and term must be positive and rate non-negative" as *u8) 157 return 0 158 } 159 let fin_pay: i64 = amort_final_payment(principal, annual_bp, term) 160 let ti: i64 = amort_total_interest(principal, annual_bp, term) 161 nsvc_ok_open(j, FIN_SVC, "amort.schedule" as *u8) 162 nj_kv_int(j, "level_payment_cents" as *u8, pay) 163 nj_comma(j) 164 nj_kv_int(j, "final_payment_cents" as *u8, fin_pay) 165 nj_comma(j) 166 nj_kv_int(j, "total_interest_cents" as *u8, ti) 167 nsvc_ok_close_g(j, GND_ASSERTED) 168 return 0 169} 170 171// ===== dispatch ====================================================== 172func main(argc: i64, argv: *i64) -> i64 { 173 let j: *NxJson = nx_json_new(FIN_MAGIC_8192) 174 let verb: *u8 = nsvc_arg(argc, argv, 1) 175 var handled: i64 = 0 176 177 if nsvc_streq(verb, "describe" as *u8) == 1 { fin_describe(j); handled = 1 } 178 if nsvc_streq(verb, "dcf.verdict" as *u8) == 1 { 179 fin_dcf_verdict(j, nsvc_arg_int(argc, argv, 2), nsvc_arg_int(argc, argv, 3)); handled = 1 180 } 181 if nsvc_streq(verb, "dcf.terminal" as *u8) == 1 { 182 fin_dcf_terminal(j, nsvc_arg_int(argc, argv, 2), nsvc_arg_int(argc, argv, 3), nsvc_arg_int(argc, argv, 4)); handled = 1 183 } 184 if nsvc_streq(verb, "bond.price" as *u8) == 1 { 185 fin_bond_price(j, nsvc_arg_int(argc, argv, 2), nsvc_arg_int(argc, argv, 3), nsvc_arg_int(argc, argv, 4), nsvc_arg_int(argc, argv, 5)); handled = 1 186 } 187 if nsvc_streq(verb, "bond.ytm" as *u8) == 1 { 188 fin_bond_ytm(j, nsvc_arg_int(argc, argv, 2), nsvc_arg_int(argc, argv, 3), nsvc_arg_int(argc, argv, 4), nsvc_arg_int(argc, argv, 5)); handled = 1 189 } 190 if nsvc_streq(verb, "bond.duration" as *u8) == 1 { 191 fin_bond_duration(j, nsvc_arg_int(argc, argv, 2), nsvc_arg_int(argc, argv, 3), nsvc_arg_int(argc, argv, 4), nsvc_arg_int(argc, argv, 5)); handled = 1 192 } 193 if nsvc_streq(verb, "taxlot.wash" as *u8) == 1 { 194 fin_taxlot_wash(j, nsvc_arg_int(argc, argv, 2), nsvc_arg_int(argc, argv, 3), nsvc_arg_int(argc, argv, 4), nsvc_arg_int(argc, argv, 5), nsvc_arg_int(argc, argv, 6)); handled = 1 195 } 196 if nsvc_streq(verb, "amort.schedule" as *u8) == 1 { 197 fin_amort_schedule(j, nsvc_arg_int(argc, argv, 2), nsvc_arg_int(argc, argv, 3), nsvc_arg_int(argc, argv, 4)); handled = 1 198 } 199 200 if handled == 0 { 201 nsvc_error(j, FIN_SVC, verb, NSVC_ERR_UNKNOWN_VERB, "unknown verb; call describe for the catalog" as *u8) 202 } 203 nj_flush(j) 204 return 0 205}