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}