nx_piotroski_lib.nx source
↩ module page · 64 lines · 3438 B
1// nx_piotroski_lib.nx -- F-SCORE: Piotroski 9-point fundamental-strength score, integer-exact.
2//
3// Pairs with the Altman-Z distress engine (nx_fin_zscore): Altman asks "is this firm in trouble?", the
4// Piotroski F-score asks "is this firm's fundamental picture STRENGTHENING?" -- nine binary year-over-year
5// tests across profitability, leverage/liquidity, and operating efficiency, each worth one point, summed
6// 0..9. High (8-9) = strong improving fundamentals; low (0-2) = deteriorating.
7//
8// Every test is an integer comparison -- no float, so the score is reproducible to the point. Inputs are
9// scaled integers (ratios in basis points, money in minor units); a real pipeline gets them from
10// nx_xbrl-parsed filings.
11//
12// ★FAIL-CLOSED + COMPOSES nx_xbrl: the missing-input sentinel PIO_NA is the SAME value nx_xbrl returns
13// for an absent fact (XB_NO_FACT = -999999999). So if any required metric could not be parsed from the
14// filing, pio_fscore returns PIO_INCOMPLETE rather than scoring an unverifiable firm -- a partial F-score
15// read as a real one is exactly the kind of false signal that gets money committed on bad data.
16//
17// DRY: composes nx_matter_lib helpers. license_tier: ORIGINAL No hw writes (Rule 26). LIB.
18
19import "nx_matter_lib.nx"
20
21const PIO_NA: i64 = 0 - 999999999 // missing input (matches nx_xbrl XB_NO_FACT) -> incomplete
22const PIO_INCOMPLETE: i64 = 0 - 1 // a required metric was absent; score not computable
23const PIO_N: i64 = 14 // number of input metrics
24
25// metric indices into the m[] array (a declared contract):
26// 0 net_income 1 roa_bp 2 roa_bp_prior 3 cfo (operating cash flow)
27// 4 ltd_ratio_bp 5 ltd_ratio_bp_prior 6 current_ratio_bp 7 current_ratio_bp_prior
28// 8 shares 9 shares_prior 10 gross_margin_bp 11 gross_margin_bp_prior
29// 12 asset_turnover_bp 13 asset_turnover_bp_prior
30
31// ★the F-score (0..9), or PIO_INCOMPLETE if any input is the missing sentinel.
32func pio_fscore(m: *i64) -> i64 {
33 var i: i64 = 0
34 while i < PIO_N { if m[i] == PIO_NA { return PIO_INCOMPLETE } i = i + 1 }
35 var s: i64 = 0
36 // -- profitability (4) --
37 if m[1] > 0 { s = s + 1 } // T1 ROA positive
38 if m[3] > 0 { s = s + 1 } // T2 operating cash flow positive
39 if m[1] > m[2] { s = s + 1 } // T3 ROA improving year over year
40 if m[3] > m[0] { s = s + 1 } // T4 CFO > net income (earnings backed by cash, low accruals)
41 // -- leverage / liquidity / source of funds (3) --
42 if m[4] < m[5] { s = s + 1 } // T5 long-term-debt ratio decreasing
43 if m[6] > m[7] { s = s + 1 } // T6 current ratio increasing (liquidity)
44 if m[8] <= m[9] { s = s + 1 } // T7 no share dilution
45 // -- operating efficiency (2) --
46 if m[10] > m[11] { s = s + 1 } // T8 gross margin increasing
47 if m[12] > m[13] { s = s + 1 } // T9 asset turnover increasing
48 return s
49}
50
51// human grade for a score. INCOMPLETE is surfaced, never silently treated as WEAK.
52func pio_grade(score: i64) -> *u8 {
53 if score < 0 { return "INCOMPLETE" as *u8 }
54 if score >= 8 { return "STRONG" as *u8 }
55 if score >= 4 { return "MODERATE" as *u8 }
56 return "WEAK" as *u8
57}
58
59// 1 only if every input is present (the score stands on complete data).
60func pio_complete(m: *i64) -> i64 {
61 var i: i64 = 0
62 while i < PIO_N { if m[i] == PIO_NA { return 0 } i = i + 1 }
63 return 1
64}