fina-logic-lib 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: fina-logic-lib
3
+ Version: 0.1.0
4
+ Summary: Deterministic FinBench logic library (195 rules across 6 layers)
5
+ Requires-Python: >=3.10
@@ -0,0 +1,15 @@
1
+ logic_lib/__init__.py,sha256=nsfk2E9FBIpvCkmpEbwQJsq0DR4e007tmwQSRPMtOAE,244
2
+ logic_lib/base.py,sha256=2Jf94N6ABhgN_KXUw5khTHEe4JzRRgGVX9x04HptRWQ,837
3
+ logic_lib/composite_resolver.py,sha256=4JZ4Kr8lnaBpdc6t9FKlX4hW5LalBxMnHMmNMNO009U,14421
4
+ logic_lib/decision_rules.py,sha256=M3hh1X9A925Ds2THR7__H6UJAvF85rmyBCqpScR6BK0,22700
5
+ logic_lib/extraction_resolver.py,sha256=uSbzv87W8tPC46wjZU8oTDRLyY7S6FZZfGVGMRcE1tI,21084
6
+ logic_lib/narrative_reasoner.py,sha256=u2zUyGlBvBF91r4MhqMX5bs1BnY8yk0x3jLWX9TaZvI,9768
7
+ logic_lib/reconciliation.py,sha256=YppTb8zSzYUokSV0GmQvu4RY0FIC2OMb01aXMN_xqNg,12244
8
+ logic_lib/registry.py,sha256=mnvkpFuN1BWSeLEQacvRU5WK00Wx51UxOy2OgiSwDJQ,1302
9
+ logic_lib/rule_catalog.py,sha256=CoArzE_MS27rbFgemG5WsOZ1JKIkROM2p5--RghmMRA,555
10
+ logic_lib/thresholds.py,sha256=-tDqFv9yN3cej_qgvrQ6F1D9vTumw9hbpKkzZ1EIiFo,106
11
+ logic_lib/verifier.py,sha256=YwqMitOwavzHOIePCu5dojeFbX_XGanH8DPze1cZqX4,12315
12
+ fina_logic_lib-0.1.0.dist-info/METADATA,sha256=Ic-wwg4FAHTN2iZCo1EEHmm99izoivjkBz9NEVC8xOs,161
13
+ fina_logic_lib-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
14
+ fina_logic_lib-0.1.0.dist-info/top_level.txt,sha256=1kFoNKUK9YnMhQKB-qSaTfoBbXJsAPSNmTCQmDoIwjc,10
15
+ fina_logic_lib-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ logic_lib
logic_lib/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from .base import RULE_REGISTRY, RuleResult
2
+ from .registry import fire, list_by_layer, search_rules, describe
3
+
4
+ __all__ = [
5
+ "RULE_REGISTRY",
6
+ "RuleResult",
7
+ "fire",
8
+ "list_by_layer",
9
+ "search_rules",
10
+ "describe",
11
+ ]
logic_lib/base.py ADDED
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Callable, Dict
5
+
6
+
7
+ @dataclass
8
+ class RuleResult:
9
+ rule_id: str
10
+ rule_name: str
11
+ fired: bool
12
+ output: Any
13
+ branch: str = ""
14
+ reason: str = ""
15
+ confidence: float = 1.0
16
+ layer: str = ""
17
+ inputs_used: Dict[str, Any] = field(default_factory=dict)
18
+
19
+
20
+ RULE_REGISTRY: Dict[str, Dict[str, Any]] = {}
21
+
22
+
23
+ def rule(rid: str, name: str, layer: str, logic_desc: str) -> Callable[[Callable[..., RuleResult]], Callable[..., RuleResult]]:
24
+ def wrap(fn: Callable[..., RuleResult]) -> Callable[..., RuleResult]:
25
+ RULE_REGISTRY[rid] = {
26
+ "fn": fn,
27
+ "name": name,
28
+ "layer": layer,
29
+ "logic": logic_desc,
30
+ }
31
+ return fn
32
+
33
+ return wrap
@@ -0,0 +1,199 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, Dict
5
+
6
+ from .base import RuleResult, rule
7
+
8
+
9
+ def _missing(inputs: Dict[str, Any]) -> bool:
10
+ return any(v is None for v in inputs.values())
11
+
12
+
13
+ def _safe_float(x: Any, default: float = 0.0) -> float:
14
+ try:
15
+ return float(x)
16
+ except Exception:
17
+ return default
18
+
19
+
20
+ def _evaluate_threshold_logic(rule_id: str, rule_name: str, layer: str, logic: str, inputs: Dict[str, Any]) -> RuleResult:
21
+ if _missing(inputs):
22
+ return RuleResult(rule_id=rule_id, rule_name=rule_name, fired=False, output="cannot determine", branch="missing_input", reason="Missing required inputs", confidence=0.6, layer=layer, inputs_used=inputs)
23
+
24
+ value = _safe_float(next(iter(inputs.values()), 0.0))
25
+ parts = [p.strip() for p in logic.split(";") if p.strip()]
26
+ for p in parts:
27
+ m_gt = re.match(r"^>(-?\d+(?:\.\d+)?)%?\s+(.+)$", p)
28
+ m_lt = re.match(r"^<(-?\d+(?:\.\d+)?)%?\s+(.+)$", p)
29
+ m_rng = re.match(r"^(-?\d+(?:\.\d+)?)\s*[-–]\s*(-?\d+(?:\.\d+)?)%?\s+(.+)$", p)
30
+ m_gte = re.match(r"^>=(-?\d+(?:\.\d+)?)\s+(.+)$", p)
31
+ m_lte = re.match(r"^<=(-?\d+(?:\.\d+)?)\s+(.+)$", p)
32
+
33
+ if m_gt and value > float(m_gt.group(1)):
34
+ return RuleResult(rule_id, rule_name, True, m_gt.group(2), f">{m_gt.group(1)}", f"value={value}", 1.0, layer, inputs)
35
+ if m_lt and value < float(m_lt.group(1)):
36
+ return RuleResult(rule_id, rule_name, True, m_lt.group(2), f"<{m_lt.group(1)}", f"value={value}", 1.0, layer, inputs)
37
+ if m_rng:
38
+ lo = float(m_rng.group(1))
39
+ hi = float(m_rng.group(2))
40
+ if lo <= value <= hi:
41
+ return RuleResult(rule_id, rule_name, True, m_rng.group(3), f"{lo}-{hi}", f"value={value}", 1.0, layer, inputs)
42
+ if m_gte and value >= float(m_gte.group(1)):
43
+ return RuleResult(rule_id, rule_name, True, m_gte.group(2), f">={m_gte.group(1)}", f"value={value}", 1.0, layer, inputs)
44
+ if m_lte and value <= float(m_lte.group(1)):
45
+ return RuleResult(rule_id, rule_name, True, m_lte.group(2), f"<={m_lte.group(1)}", f"value={value}", 1.0, layer, inputs)
46
+
47
+ # Fallback for non-threshold rules
48
+ return RuleResult(rule_id, rule_name, True, "applied", "default", logic, 0.8, layer, inputs)
49
+
50
+
51
+ @rule('sideword_excluding', 'Side-Word: Excluding', 'L03_composite', "IF 'excluding'/'net of'/'without' THEN subtract that component")
52
+ def sideword_excluding(**kwargs) -> RuleResult:
53
+ """Side-Word: Excluding. Logic: IF 'excluding'/'net of'/'without' THEN subtract that component"""
54
+ return _evaluate_threshold_logic('sideword_excluding', 'Side-Word: Excluding', 'L03_composite', "IF 'excluding'/'net of'/'without' THEN subtract that component", kwargs)
55
+
56
+ @rule('sideword_assuming', 'Side-Word: Assuming', 'L03_composite', "IF 'assuming'/'if' THEN apply stated assumption")
57
+ def sideword_assuming(**kwargs) -> RuleResult:
58
+ """Side-Word: Assuming. Logic: IF 'assuming'/'if' THEN apply stated assumption"""
59
+ return _evaluate_threshold_logic('sideword_assuming', 'Side-Word: Assuming', 'L03_composite', "IF 'assuming'/'if' THEN apply stated assumption", kwargs)
60
+
61
+ @rule('sideword_rate_multiplier', 'Side-Word: Rate Multiplier', 'L03_composite', "IF 'Nx the rate' THEN multiply base rate by N")
62
+ def sideword_rate_multiplier(**kwargs) -> RuleResult:
63
+ """Side-Word: Rate Multiplier. Logic: IF 'Nx the rate' THEN multiply base rate by N"""
64
+ return _evaluate_threshold_logic('sideword_rate_multiplier', 'Side-Word: Rate Multiplier', 'L03_composite', "IF 'Nx the rate' THEN multiply base rate by N", kwargs)
65
+
66
+ @rule('sideword_same_as', 'Side-Word: Same As', 'L03_composite', "IF 'same rate/pace as' THEN reuse prior computed rate")
67
+ def sideword_same_as(**kwargs) -> RuleResult:
68
+ """Side-Word: Same As. Logic: IF 'same rate/pace as' THEN reuse prior computed rate"""
69
+ return _evaluate_threshold_logic('sideword_same_as', 'Side-Word: Same As', 'L03_composite', "IF 'same rate/pace as' THEN reuse prior computed rate", kwargs)
70
+
71
+ @rule('sideword_between', 'Side-Word: Between', 'L03_composite', "IF 'between X and Y' THEN define period window")
72
+ def sideword_between(**kwargs) -> RuleResult:
73
+ """Side-Word: Between. Logic: IF 'between X and Y' THEN define period window"""
74
+ return _evaluate_threshold_logic('sideword_between', 'Side-Word: Between', 'L03_composite', "IF 'between X and Y' THEN define period window", kwargs)
75
+
76
+ @rule('sideword_per', 'Side-Word: Per', 'L03_composite', "IF 'per share/employee/store' THEN divide by count")
77
+ def sideword_per(**kwargs) -> RuleResult:
78
+ """Side-Word: Per. Logic: IF 'per share/employee/store' THEN divide by count"""
79
+ return _evaluate_threshold_logic('sideword_per', 'Side-Word: Per', 'L03_composite', "IF 'per share/employee/store' THEN divide by count", kwargs)
80
+
81
+ @rule('sideword_compared_to', 'Side-Word: Compared To', 'L03_composite', "IF 'compared to/vs' THEN set comparison operand")
82
+ def sideword_compared_to(**kwargs) -> RuleResult:
83
+ """Side-Word: Compared To. Logic: IF 'compared to/vs' THEN set comparison operand"""
84
+ return _evaluate_threshold_logic('sideword_compared_to', 'Side-Word: Compared To', 'L03_composite', "IF 'compared to/vs' THEN set comparison operand", kwargs)
85
+
86
+ @rule('sideword_change_in', 'Side-Word: Change In', 'L03_composite', "IF 'change in/growth in' THEN compute difference")
87
+ def sideword_change_in(**kwargs) -> RuleResult:
88
+ """Side-Word: Change In. Logic: IF 'change in/growth in' THEN compute difference"""
89
+ return _evaluate_threshold_logic('sideword_change_in', 'Side-Word: Change In', 'L03_composite', "IF 'change in/growth in' THEN compute difference", kwargs)
90
+
91
+ @rule('sideword_average_of', 'Side-Word: Average Of', 'L03_composite', "IF 'average/mean of' THEN collect and average")
92
+ def sideword_average_of(**kwargs) -> RuleResult:
93
+ """Side-Word: Average Of. Logic: IF 'average/mean of' THEN collect and average"""
94
+ return _evaluate_threshold_logic('sideword_average_of', 'Side-Word: Average Of', 'L03_composite', "IF 'average/mean of' THEN collect and average", kwargs)
95
+
96
+ @rule('sideword_negation', 'Side-Word: Negation', 'L03_composite', "IF 'not'/'excluding' THEN invert logic")
97
+ def sideword_negation(**kwargs) -> RuleResult:
98
+ """Side-Word: Negation. Logic: IF 'not'/'excluding' THEN invert logic"""
99
+ return _evaluate_threshold_logic('sideword_negation', 'Side-Word: Negation', 'L03_composite', "IF 'not'/'excluding' THEN invert logic", kwargs)
100
+
101
+ @rule('decompose_compound', 'Decompose Compound Question', 'L03_composite', 'Parse conjunctions into ordered sub-tasks')
102
+ def decompose_compound(**kwargs) -> RuleResult:
103
+ """Decompose Compound Question. Logic: Parse conjunctions into ordered sub-tasks"""
104
+ return _evaluate_threshold_logic('decompose_compound', 'Decompose Compound Question', 'L03_composite', 'Parse conjunctions into ordered sub-tasks', kwargs)
105
+
106
+ @rule('build_dependency_graph', 'Build Dependency Graph', 'L03_composite', 'Construct DAG of formula inputs/outputs')
107
+ def build_dependency_graph(**kwargs) -> RuleResult:
108
+ """Build Dependency Graph. Logic: Construct DAG of formula inputs/outputs"""
109
+ return _evaluate_threshold_logic('build_dependency_graph', 'Build Dependency Graph', 'L03_composite', 'Construct DAG of formula inputs/outputs', kwargs)
110
+
111
+ @rule('topological_sort', 'Topological Solve Order', 'L03_composite', 'Sort DAG so inputs computed before use')
112
+ def topological_sort(**kwargs) -> RuleResult:
113
+ """Topological Solve Order. Logic: Sort DAG so inputs computed before use"""
114
+ return _evaluate_threshold_logic('topological_sort', 'Topological Solve Order', 'L03_composite', 'Sort DAG so inputs computed before use', kwargs)
115
+
116
+ @rule('single_value_anchor', 'Single-Value Anchor', 'L03_composite', 'Identify the seed value the chain depends on')
117
+ def single_value_anchor(**kwargs) -> RuleResult:
118
+ """Single-Value Anchor. Logic: Identify the seed value the chain depends on"""
119
+ return _evaluate_threshold_logic('single_value_anchor', 'Single-Value Anchor', 'L03_composite', 'Identify the seed value the chain depends on', kwargs)
120
+
121
+ @rule('chain_extract_compute', 'Extract-Compute Chain', 'L03_composite', 'Pipeline: extract inputs -> apply formula -> output')
122
+ def chain_extract_compute(**kwargs) -> RuleResult:
123
+ """Extract-Compute Chain. Logic: Pipeline: extract inputs -> apply formula -> output"""
124
+ return _evaluate_threshold_logic('chain_extract_compute', 'Extract-Compute Chain', 'L03_composite', 'Pipeline: extract inputs -> apply formula -> output', kwargs)
125
+
126
+ @rule('sub_formula_match', 'Sub-Formula Matching', 'L03_composite', 'Map detected hints to maths_lib formula IDs')
127
+ def sub_formula_match(**kwargs) -> RuleResult:
128
+ """Sub-Formula Matching. Logic: Map detected hints to maths_lib formula IDs"""
129
+ return _evaluate_threshold_logic('sub_formula_match', 'Sub-Formula Matching', 'L03_composite', 'Map detected hints to maths_lib formula IDs', kwargs)
130
+
131
+ @rule('intermediate_carry', 'Intermediate Result Carry', 'L03_composite', 'Output of step N becomes input of step N+1')
132
+ def intermediate_carry(**kwargs) -> RuleResult:
133
+ """Intermediate Result Carry. Logic: Output of step N becomes input of step N+1"""
134
+ return _evaluate_threshold_logic('intermediate_carry', 'Intermediate Result Carry', 'L03_composite', 'Output of step N becomes input of step N+1', kwargs)
135
+
136
+ @rule('multi_year_chain', 'Multi-Year Chain', 'L03_composite', 'Collect each year, then aggregate/compare')
137
+ def multi_year_chain(**kwargs) -> RuleResult:
138
+ """Multi-Year Chain. Logic: Collect each year, then aggregate/compare"""
139
+ return _evaluate_threshold_logic('multi_year_chain', 'Multi-Year Chain', 'L03_composite', 'Collect each year, then aggregate/compare', kwargs)
140
+
141
+ @rule('ratio_of_ratios', 'Ratio of Ratios', 'L03_composite', 'Compute inner ratios then outer')
142
+ def ratio_of_ratios(**kwargs) -> RuleResult:
143
+ """Ratio of Ratios. Logic: Compute inner ratios then outer"""
144
+ return _evaluate_threshold_logic('ratio_of_ratios', 'Ratio of Ratios', 'L03_composite', 'Compute inner ratios then outer', kwargs)
145
+
146
+ @rule('conditional_branch', 'Conditional Branch', 'L03_composite', 'IF condition THEN path A ELSE path B')
147
+ def conditional_branch(**kwargs) -> RuleResult:
148
+ """Conditional Branch. Logic: IF condition THEN path A ELSE path B"""
149
+ return _evaluate_threshold_logic('conditional_branch', 'Conditional Branch', 'L03_composite', 'IF condition THEN path A ELSE path B', kwargs)
150
+
151
+ @rule('unit_propagation', 'Unit Propagation', 'L03_composite', 'Carry and reconcile units across steps')
152
+ def unit_propagation(**kwargs) -> RuleResult:
153
+ """Unit Propagation. Logic: Carry and reconcile units across steps"""
154
+ return _evaluate_threshold_logic('unit_propagation', 'Unit Propagation', 'L03_composite', 'Carry and reconcile units across steps', kwargs)
155
+
156
+ @rule('assumption_injection', 'Assumption Injection', 'L03_composite', "Add 'assuming X' values to input set")
157
+ def assumption_injection(**kwargs) -> RuleResult:
158
+ """Assumption Injection. Logic: Add 'assuming X' values to input set"""
159
+ return _evaluate_threshold_logic('assumption_injection', 'Assumption Injection', 'L03_composite', "Add 'assuming X' values to input set", kwargs)
160
+
161
+ @rule('two_stage_growth', 'Two-Stage Growth Chain', 'L03_composite', 'Stage1 rate for years 1-n, stage2 after')
162
+ def two_stage_growth(**kwargs) -> RuleResult:
163
+ """Two-Stage Growth Chain. Logic: Stage1 rate for years 1-n, stage2 after"""
164
+ return _evaluate_threshold_logic('two_stage_growth', 'Two-Stage Growth Chain', 'L03_composite', 'Stage1 rate for years 1-n, stage2 after', kwargs)
165
+
166
+ @rule('forecast_chain', 'Forecast Chain', 'L03_composite', 'Grow base by rate then apply formula')
167
+ def forecast_chain(**kwargs) -> RuleResult:
168
+ """Forecast Chain. Logic: Grow base by rate then apply formula"""
169
+ return _evaluate_threshold_logic('forecast_chain', 'Forecast Chain', 'L03_composite', 'Grow base by rate then apply formula', kwargs)
170
+
171
+ @rule('backsolve', 'Back-Solve', 'L03_composite', 'Given output, invert formula for missing input')
172
+ def backsolve(**kwargs) -> RuleResult:
173
+ """Back-Solve. Logic: Given output, invert formula for missing input"""
174
+ return _evaluate_threshold_logic('backsolve', 'Back-Solve', 'L03_composite', 'Given output, invert formula for missing input', kwargs)
175
+
176
+ @rule('scenario_branch', 'Scenario Branch', 'L03_composite', 'Run chain under bull/base/bear inputs')
177
+ def scenario_branch(**kwargs) -> RuleResult:
178
+ """Scenario Branch. Logic: Run chain under bull/base/bear inputs"""
179
+ return _evaluate_threshold_logic('scenario_branch', 'Scenario Branch', 'L03_composite', 'Run chain under bull/base/bear inputs', kwargs)
180
+
181
+ @rule('aggregate_then_ratio', 'Aggregate Then Ratio', 'L03_composite', 'Sum parts, then compute ratio on totals')
182
+ def aggregate_then_ratio(**kwargs) -> RuleResult:
183
+ """Aggregate Then Ratio. Logic: Sum parts, then compute ratio on totals"""
184
+ return _evaluate_threshold_logic('aggregate_then_ratio', 'Aggregate Then Ratio', 'L03_composite', 'Sum parts, then compute ratio on totals', kwargs)
185
+
186
+ @rule('difference_then_pct', 'Difference Then Percent', 'L03_composite', 'Compute delta, then express as percent')
187
+ def difference_then_pct(**kwargs) -> RuleResult:
188
+ """Difference Then Percent. Logic: Compute delta, then express as percent"""
189
+ return _evaluate_threshold_logic('difference_then_pct', 'Difference Then Percent', 'L03_composite', 'Compute delta, then express as percent', kwargs)
190
+
191
+ @rule('weighted_combine', 'Weighted Combine', 'L03_composite', 'Sum(weight_i * result_i)')
192
+ def weighted_combine(**kwargs) -> RuleResult:
193
+ """Weighted Combine. Logic: Sum(weight_i * result_i)"""
194
+ return _evaluate_threshold_logic('weighted_combine', 'Weighted Combine', 'L03_composite', 'Sum(weight_i * result_i)', kwargs)
195
+
196
+ @rule('self_consistency_3path', '3-Path Self-Consistency', 'L03_composite', 'Compute by 3 decompositions; agree=confident')
197
+ def self_consistency_3path(**kwargs) -> RuleResult:
198
+ """3-Path Self-Consistency. Logic: Compute by 3 decompositions; agree=confident"""
199
+ return _evaluate_threshold_logic('self_consistency_3path', '3-Path Self-Consistency', 'L03_composite', 'Compute by 3 decompositions; agree=confident', kwargs)
@@ -0,0 +1,299 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, Dict
5
+
6
+ from .base import RuleResult, rule
7
+
8
+
9
+ def _missing(inputs: Dict[str, Any]) -> bool:
10
+ return any(v is None for v in inputs.values())
11
+
12
+
13
+ def _safe_float(x: Any, default: float = 0.0) -> float:
14
+ try:
15
+ return float(x)
16
+ except Exception:
17
+ return default
18
+
19
+
20
+ def _evaluate_threshold_logic(rule_id: str, rule_name: str, layer: str, logic: str, inputs: Dict[str, Any]) -> RuleResult:
21
+ if _missing(inputs):
22
+ return RuleResult(rule_id=rule_id, rule_name=rule_name, fired=False, output="cannot determine", branch="missing_input", reason="Missing required inputs", confidence=0.6, layer=layer, inputs_used=inputs)
23
+
24
+ value = _safe_float(next(iter(inputs.values()), 0.0))
25
+ parts = [p.strip() for p in logic.split(";") if p.strip()]
26
+ for p in parts:
27
+ m_gt = re.match(r"^>(-?\d+(?:\.\d+)?)%?\s+(.+)$", p)
28
+ m_lt = re.match(r"^<(-?\d+(?:\.\d+)?)%?\s+(.+)$", p)
29
+ m_rng = re.match(r"^(-?\d+(?:\.\d+)?)\s*[-–]\s*(-?\d+(?:\.\d+)?)%?\s+(.+)$", p)
30
+ m_gte = re.match(r"^>=(-?\d+(?:\.\d+)?)\s+(.+)$", p)
31
+ m_lte = re.match(r"^<=(-?\d+(?:\.\d+)?)\s+(.+)$", p)
32
+
33
+ if m_gt and value > float(m_gt.group(1)):
34
+ return RuleResult(rule_id, rule_name, True, m_gt.group(2), f">{m_gt.group(1)}", f"value={value}", 1.0, layer, inputs)
35
+ if m_lt and value < float(m_lt.group(1)):
36
+ return RuleResult(rule_id, rule_name, True, m_lt.group(2), f"<{m_lt.group(1)}", f"value={value}", 1.0, layer, inputs)
37
+ if m_rng:
38
+ lo = float(m_rng.group(1))
39
+ hi = float(m_rng.group(2))
40
+ if lo <= value <= hi:
41
+ return RuleResult(rule_id, rule_name, True, m_rng.group(3), f"{lo}-{hi}", f"value={value}", 1.0, layer, inputs)
42
+ if m_gte and value >= float(m_gte.group(1)):
43
+ return RuleResult(rule_id, rule_name, True, m_gte.group(2), f">={m_gte.group(1)}", f"value={value}", 1.0, layer, inputs)
44
+ if m_lte and value <= float(m_lte.group(1)):
45
+ return RuleResult(rule_id, rule_name, True, m_lte.group(2), f"<={m_lte.group(1)}", f"value={value}", 1.0, layer, inputs)
46
+
47
+ # Fallback for non-threshold rules
48
+ return RuleResult(rule_id, rule_name, True, "applied", "default", logic, 0.8, layer, inputs)
49
+
50
+
51
+ @rule('liquidity_current_ratio', 'Liquidity by Current Ratio', 'L02_decision', '>2.0 very healthy; 1.5-2.0 healthy; 1.0-1.5 adequate; <1.0 concern')
52
+ def liquidity_current_ratio(**kwargs) -> RuleResult:
53
+ """Liquidity by Current Ratio. Logic: >2.0 very healthy; 1.5-2.0 healthy; 1.0-1.5 adequate; <1.0 concern"""
54
+ return _evaluate_threshold_logic('liquidity_current_ratio', 'Liquidity by Current Ratio', 'L02_decision', '>2.0 very healthy; 1.5-2.0 healthy; 1.0-1.5 adequate; <1.0 concern', kwargs)
55
+
56
+ @rule('liquidity_quick_ratio', 'Liquidity by Quick Ratio', 'L02_decision', '>1.0 strong; 0.7-1.0 acceptable; <0.7 tight')
57
+ def liquidity_quick_ratio(**kwargs) -> RuleResult:
58
+ """Liquidity by Quick Ratio. Logic: >1.0 strong; 0.7-1.0 acceptable; <0.7 tight"""
59
+ return _evaluate_threshold_logic('liquidity_quick_ratio', 'Liquidity by Quick Ratio', 'L02_decision', '>1.0 strong; 0.7-1.0 acceptable; <0.7 tight', kwargs)
60
+
61
+ @rule('liquidity_cash_ratio', 'Cash Liquidity', 'L02_decision', '>0.5 strong; 0.2-0.5 moderate; <0.2 thin')
62
+ def liquidity_cash_ratio(**kwargs) -> RuleResult:
63
+ """Cash Liquidity. Logic: >0.5 strong; 0.2-0.5 moderate; <0.2 thin"""
64
+ return _evaluate_threshold_logic('liquidity_cash_ratio', 'Cash Liquidity', 'L02_decision', '>0.5 strong; 0.2-0.5 moderate; <0.2 thin', kwargs)
65
+
66
+ @rule('liquidity_composite', 'Composite Liquidity', 'L02_decision', 'Weighted classification across 3 ratios')
67
+ def liquidity_composite(**kwargs) -> RuleResult:
68
+ """Composite Liquidity. Logic: Weighted classification across 3 ratios"""
69
+ return _evaluate_threshold_logic('liquidity_composite', 'Composite Liquidity', 'L02_decision', 'Weighted classification across 3 ratios', kwargs)
70
+
71
+ @rule('capital_intensity_class', 'Capital Intensity Class', 'L02_decision', '>15% highly; 5-15% moderately; <5% not capital-intensive')
72
+ def capital_intensity_class(**kwargs) -> RuleResult:
73
+ """Capital Intensity Class. Logic: >15% highly; 5-15% moderately; <5% not capital-intensive"""
74
+ return _evaluate_threshold_logic('capital_intensity_class', 'Capital Intensity Class', 'L02_decision', '>15% highly; 5-15% moderately; <5% not capital-intensive', kwargs)
75
+
76
+ @rule('leverage_debt_ebitda', 'Leverage by Debt/EBITDA', 'L02_decision', '<1 conservative; 1-3 moderate; 3-4 elevated; >4 high')
77
+ def leverage_debt_ebitda(**kwargs) -> RuleResult:
78
+ """Leverage by Debt/EBITDA. Logic: <1 conservative; 1-3 moderate; 3-4 elevated; >4 high"""
79
+ return _evaluate_threshold_logic('leverage_debt_ebitda', 'Leverage by Debt/EBITDA', 'L02_decision', '<1 conservative; 1-3 moderate; 3-4 elevated; >4 high', kwargs)
80
+
81
+ @rule('leverage_debt_equity', 'Leverage by D/E', 'L02_decision', '<0.5 low; 0.5-1.5 moderate; >1.5 high')
82
+ def leverage_debt_equity(**kwargs) -> RuleResult:
83
+ """Leverage by D/E. Logic: <0.5 low; 0.5-1.5 moderate; >1.5 high"""
84
+ return _evaluate_threshold_logic('leverage_debt_equity', 'Leverage by D/E', 'L02_decision', '<0.5 low; 0.5-1.5 moderate; >1.5 high', kwargs)
85
+
86
+ @rule('solvency_interest_coverage', 'Interest Coverage Health', 'L02_decision', '>5 strong; 2-5 adequate; 1.5-2 weak; <1.5 risk')
87
+ def solvency_interest_coverage(**kwargs) -> RuleResult:
88
+ """Interest Coverage Health. Logic: >5 strong; 2-5 adequate; 1.5-2 weak; <1.5 risk"""
89
+ return _evaluate_threshold_logic('solvency_interest_coverage', 'Interest Coverage Health', 'L02_decision', '>5 strong; 2-5 adequate; 1.5-2 weak; <1.5 risk', kwargs)
90
+
91
+ @rule('solvency_dscr', 'DSCR Health', 'L02_decision', '>1.5 healthy; 1.0-1.5 tight; <1.0 default risk')
92
+ def solvency_dscr(**kwargs) -> RuleResult:
93
+ """DSCR Health. Logic: >1.5 healthy; 1.0-1.5 tight; <1.0 default risk"""
94
+ return _evaluate_threshold_logic('solvency_dscr', 'DSCR Health', 'L02_decision', '>1.5 healthy; 1.0-1.5 tight; <1.0 default risk', kwargs)
95
+
96
+ @rule('profitability_net_margin', 'Net Margin Quality', 'L02_decision', '>20% excellent; 10-20% good; 0-10% modest; <0 loss')
97
+ def profitability_net_margin(**kwargs) -> RuleResult:
98
+ """Net Margin Quality. Logic: >20% excellent; 10-20% good; 0-10% modest; <0 loss"""
99
+ return _evaluate_threshold_logic('profitability_net_margin', 'Net Margin Quality', 'L02_decision', '>20% excellent; 10-20% good; 0-10% modest; <0 loss', kwargs)
100
+
101
+ @rule('profitability_roe', 'ROE Quality', 'L02_decision', '>20% excellent; 10-20% good; <10% weak')
102
+ def profitability_roe(**kwargs) -> RuleResult:
103
+ """ROE Quality. Logic: >20% excellent; 10-20% good; <10% weak"""
104
+ return _evaluate_threshold_logic('profitability_roe', 'ROE Quality', 'L02_decision', '>20% excellent; 10-20% good; <10% weak', kwargs)
105
+
106
+ @rule('profitability_roic_wacc', 'ROIC vs WACC', 'L02_decision', 'IF ROIC > WACC THEN creating value ELSE destroying')
107
+ def profitability_roic_wacc(**kwargs) -> RuleResult:
108
+ """ROIC vs WACC. Logic: IF ROIC > WACC THEN creating value ELSE destroying"""
109
+ return _evaluate_threshold_logic('profitability_roic_wacc', 'ROIC vs WACC', 'L02_decision', 'IF ROIC > WACC THEN creating value ELSE destroying', kwargs)
110
+
111
+ @rule('margin_direction', 'Margin Direction', 'L02_decision', 'IF current > prior THEN improved ELSE worsened (by pp)')
112
+ def margin_direction(**kwargs) -> RuleResult:
113
+ """Margin Direction. Logic: IF current > prior THEN improved ELSE worsened (by pp)"""
114
+ return _evaluate_threshold_logic('margin_direction', 'Margin Direction', 'L02_decision', 'IF current > prior THEN improved ELSE worsened (by pp)', kwargs)
115
+
116
+ @rule('growth_direction', 'Growth Direction', 'L02_decision', 'IF current > prior THEN growing ELSE declining')
117
+ def growth_direction(**kwargs) -> RuleResult:
118
+ """Growth Direction. Logic: IF current > prior THEN growing ELSE declining"""
119
+ return _evaluate_threshold_logic('growth_direction', 'Growth Direction', 'L02_decision', 'IF current > prior THEN growing ELSE declining', kwargs)
120
+
121
+ @rule('growth_acceleration', 'Growth Acceleration', 'L02_decision', 'IF growth_rate_now > growth_rate_prior THEN accelerating')
122
+ def growth_acceleration(**kwargs) -> RuleResult:
123
+ """Growth Acceleration. Logic: IF growth_rate_now > growth_rate_prior THEN accelerating"""
124
+ return _evaluate_threshold_logic('growth_acceleration', 'Growth Acceleration', 'L02_decision', 'IF growth_rate_now > growth_rate_prior THEN accelerating', kwargs)
125
+
126
+ @rule('trend_three_period', 'Three-Period Trend', 'L02_decision', 'Compare 3 consecutive: rising/falling/stable/volatile')
127
+ def trend_three_period(**kwargs) -> RuleResult:
128
+ """Three-Period Trend. Logic: Compare 3 consecutive: rising/falling/stable/volatile"""
129
+ return _evaluate_threshold_logic('trend_three_period', 'Three-Period Trend', 'L02_decision', 'Compare 3 consecutive: rising/falling/stable/volatile', kwargs)
130
+
131
+ @rule('dividend_sustainability', 'Dividend Sustainability', 'L02_decision', 'IF payout < 60% THEN sustainable; 60-90% watch; >90% at-risk')
132
+ def dividend_sustainability(**kwargs) -> RuleResult:
133
+ """Dividend Sustainability. Logic: IF payout < 60% THEN sustainable; 60-90% watch; >90% at-risk"""
134
+ return _evaluate_threshold_logic('dividend_sustainability', 'Dividend Sustainability', 'L02_decision', 'IF payout < 60% THEN sustainable; 60-90% watch; >90% at-risk', kwargs)
135
+
136
+ @rule('dividend_stability', 'Dividend Stability', 'L02_decision', 'IF dividends non-decreasing over N years THEN stable')
137
+ def dividend_stability(**kwargs) -> RuleResult:
138
+ """Dividend Stability. Logic: IF dividends non-decreasing over N years THEN stable"""
139
+ return _evaluate_threshold_logic('dividend_stability', 'Dividend Stability', 'L02_decision', 'IF dividends non-decreasing over N years THEN stable', kwargs)
140
+
141
+ @rule('efficiency_ccc', 'Cash Conversion Cycle Health', 'L02_decision', '<0 excellent; 0-30 strong; 30-60 normal; >60 slow')
142
+ def efficiency_ccc(**kwargs) -> RuleResult:
143
+ """Cash Conversion Cycle Health. Logic: <0 excellent; 0-30 strong; 30-60 normal; >60 slow"""
144
+ return _evaluate_threshold_logic('efficiency_ccc', 'Cash Conversion Cycle Health', 'L02_decision', '<0 excellent; 0-30 strong; 30-60 normal; >60 slow', kwargs)
145
+
146
+ @rule('efficiency_inventory_turn', 'Inventory Turnover Health', 'L02_decision', 'Industry-relative: higher = better')
147
+ def efficiency_inventory_turn(**kwargs) -> RuleResult:
148
+ """Inventory Turnover Health. Logic: Industry-relative: higher = better"""
149
+ return _evaluate_threshold_logic('efficiency_inventory_turn', 'Inventory Turnover Health', 'L02_decision', 'Industry-relative: higher = better', kwargs)
150
+
151
+ @rule('peer_comparison_better', 'Peer Comparison', 'L02_decision', 'Compare metric vs peer with direction awareness')
152
+ def peer_comparison_better(**kwargs) -> RuleResult:
153
+ """Peer Comparison. Logic: Compare metric vs peer with direction awareness"""
154
+ return _evaluate_threshold_logic('peer_comparison_better', 'Peer Comparison', 'L02_decision', 'Compare metric vs peer with direction awareness', kwargs)
155
+
156
+ @rule('peer_rank', 'Peer Ranking', 'L02_decision', 'Sort and return position')
157
+ def peer_rank(**kwargs) -> RuleResult:
158
+ """Peer Ranking. Logic: Sort and return position"""
159
+ return _evaluate_threshold_logic('peer_rank', 'Peer Ranking', 'L02_decision', 'Sort and return position', kwargs)
160
+
161
+ @rule('yes_no_threshold', 'Generic Yes/No Threshold', 'L02_decision', 'IF value [op] threshold THEN yes ELSE no')
162
+ def yes_no_threshold(**kwargs) -> RuleResult:
163
+ """Generic Yes/No Threshold. Logic: IF value [op] threshold THEN yes ELSE no"""
164
+ return _evaluate_threshold_logic('yes_no_threshold', 'Generic Yes/No Threshold', 'L02_decision', 'IF value [op] threshold THEN yes ELSE no', kwargs)
165
+
166
+ @rule('cannot_determine', 'Cannot Determine', 'L02_decision', "IF required inputs missing THEN 'cannot determine'")
167
+ def cannot_determine(**kwargs) -> RuleResult:
168
+ """Cannot Determine. Logic: IF required inputs missing THEN 'cannot determine'"""
169
+ return _evaluate_threshold_logic('cannot_determine', 'Cannot Determine', 'L02_decision', "IF required inputs missing THEN 'cannot determine'", kwargs)
170
+
171
+ @rule('materiality_check', 'Materiality Check', 'L02_decision', 'IF abs(change) > 5% of base THEN material')
172
+ def materiality_check(**kwargs) -> RuleResult:
173
+ """Materiality Check. Logic: IF abs(change) > 5% of base THEN material"""
174
+ return _evaluate_threshold_logic('materiality_check', 'Materiality Check', 'L02_decision', 'IF abs(change) > 5% of base THEN material', kwargs)
175
+
176
+ @rule('concentration_risk', 'Concentration Risk', 'L02_decision', 'IF top share > 10% THEN concentration risk flag')
177
+ def concentration_risk(**kwargs) -> RuleResult:
178
+ """Concentration Risk. Logic: IF top share > 10% THEN concentration risk flag"""
179
+ return _evaluate_threshold_logic('concentration_risk', 'Concentration Risk', 'L02_decision', 'IF top share > 10% THEN concentration risk flag', kwargs)
180
+
181
+ @rule('altman_zone', 'Altman Z Zone', 'L02_decision', '>2.99 safe; 1.81-2.99 grey; <1.81 distress')
182
+ def altman_zone(**kwargs) -> RuleResult:
183
+ """Altman Z Zone. Logic: >2.99 safe; 1.81-2.99 grey; <1.81 distress"""
184
+ return _evaluate_threshold_logic('altman_zone', 'Altman Z Zone', 'L02_decision', '>2.99 safe; 1.81-2.99 grey; <1.81 distress', kwargs)
185
+
186
+ @rule('piotroski_strength', 'Piotroski Strength', 'L02_decision', '>=8 strong; 4-7 moderate; <=3 weak')
187
+ def piotroski_strength(**kwargs) -> RuleResult:
188
+ """Piotroski Strength. Logic: >=8 strong; 4-7 moderate; <=3 weak"""
189
+ return _evaluate_threshold_logic('piotroski_strength', 'Piotroski Strength', 'L02_decision', '>=8 strong; 4-7 moderate; <=3 weak', kwargs)
190
+
191
+ @rule('revenue_quality', 'Revenue Quality', 'L02_decision', 'IF recurring share > 70% THEN high quality')
192
+ def revenue_quality(**kwargs) -> RuleResult:
193
+ """Revenue Quality. Logic: IF recurring share > 70% THEN high quality"""
194
+ return _evaluate_threshold_logic('revenue_quality', 'Revenue Quality', 'L02_decision', 'IF recurring share > 70% THEN high quality', kwargs)
195
+
196
+ @rule('earnings_quality_flag', 'Earnings Quality Flag', 'L02_decision', 'IF cash_conversion < 0.8 THEN low quality flag')
197
+ def earnings_quality_flag(**kwargs) -> RuleResult:
198
+ """Earnings Quality Flag. Logic: IF cash_conversion < 0.8 THEN low quality flag"""
199
+ return _evaluate_threshold_logic('earnings_quality_flag', 'Earnings Quality Flag', 'L02_decision', 'IF cash_conversion < 0.8 THEN low quality flag', kwargs)
200
+
201
+ @rule('working_capital_health', 'Working Capital Health', 'L02_decision', 'IF WC > 0 AND current_ratio > 1.2 THEN healthy')
202
+ def working_capital_health(**kwargs) -> RuleResult:
203
+ """Working Capital Health. Logic: IF WC > 0 AND current_ratio > 1.2 THEN healthy"""
204
+ return _evaluate_threshold_logic('working_capital_health', 'Working Capital Health', 'L02_decision', 'IF WC > 0 AND current_ratio > 1.2 THEN healthy', kwargs)
205
+
206
+ @rule('debt_maturity_risk', 'Debt Maturity Risk', 'L02_decision', 'IF ST_debt / total_debt > 40% THEN refinancing risk')
207
+ def debt_maturity_risk(**kwargs) -> RuleResult:
208
+ """Debt Maturity Risk. Logic: IF ST_debt / total_debt > 40% THEN refinancing risk"""
209
+ return _evaluate_threshold_logic('debt_maturity_risk', 'Debt Maturity Risk', 'L02_decision', 'IF ST_debt / total_debt > 40% THEN refinancing risk', kwargs)
210
+
211
+ @rule('margin_vs_peer', 'Margin vs Peer', 'L02_decision', 'Compare margin to peer median')
212
+ def margin_vs_peer(**kwargs) -> RuleResult:
213
+ """Margin vs Peer. Logic: Compare margin to peer median"""
214
+ return _evaluate_threshold_logic('margin_vs_peer', 'Margin vs Peer', 'L02_decision', 'Compare margin to peer median', kwargs)
215
+
216
+ @rule('size_classification', 'Size Classification', 'L02_decision', '>200B mega; 10-200B large; 2-10B mid; <2B small')
217
+ def size_classification(**kwargs) -> RuleResult:
218
+ """Size Classification. Logic: >200B mega; 10-200B large; 2-10B mid; <2B small"""
219
+ return _evaluate_threshold_logic('size_classification', 'Size Classification', 'L02_decision', '>200B mega; 10-200B large; 2-10B mid; <2B small', kwargs)
220
+
221
+ @rule('valuation_pe_signal', 'P/E Signal', 'L02_decision', 'Compare P/E to sector median')
222
+ def valuation_pe_signal(**kwargs) -> RuleResult:
223
+ """P/E Signal. Logic: Compare P/E to sector median"""
224
+ return _evaluate_threshold_logic('valuation_pe_signal', 'P/E Signal', 'L02_decision', 'Compare P/E to sector median', kwargs)
225
+
226
+ @rule('growth_vs_value', 'Growth vs Value', 'L02_decision', 'High P/E + high growth = growth; low P/E = value')
227
+ def growth_vs_value(**kwargs) -> RuleResult:
228
+ """Growth vs Value. Logic: High P/E + high growth = growth; low P/E = value"""
229
+ return _evaluate_threshold_logic('growth_vs_value', 'Growth vs Value', 'L02_decision', 'High P/E + high growth = growth; low P/E = value', kwargs)
230
+
231
+ @rule('fcf_positive', 'FCF Positivity', 'L02_decision', 'IF FCF > 0 AND growing THEN healthy cash generation')
232
+ def fcf_positive(**kwargs) -> RuleResult:
233
+ """FCF Positivity. Logic: IF FCF > 0 AND growing THEN healthy cash generation"""
234
+ return _evaluate_threshold_logic('fcf_positive', 'FCF Positivity', 'L02_decision', 'IF FCF > 0 AND growing THEN healthy cash generation', kwargs)
235
+
236
+ @rule('buyback_vs_dividend', 'Capital Return Mix', 'L02_decision', 'Compare buyback yield to dividend yield')
237
+ def buyback_vs_dividend(**kwargs) -> RuleResult:
238
+ """Capital Return Mix. Logic: Compare buyback yield to dividend yield"""
239
+ return _evaluate_threshold_logic('buyback_vs_dividend', 'Capital Return Mix', 'L02_decision', 'Compare buyback yield to dividend yield', kwargs)
240
+
241
+ @rule('tax_rate_normal', 'Tax Rate Normality', 'L02_decision', 'IF 15-30% THEN normal; outliers flagged')
242
+ def tax_rate_normal(**kwargs) -> RuleResult:
243
+ """Tax Rate Normality. Logic: IF 15-30% THEN normal; outliers flagged"""
244
+ return _evaluate_threshold_logic('tax_rate_normal', 'Tax Rate Normality', 'L02_decision', 'IF 15-30% THEN normal; outliers flagged', kwargs)
245
+
246
+ @rule('operating_leverage_signal', 'Operating Leverage Signal', 'L02_decision', 'IF DOL > 2 THEN high operating leverage')
247
+ def operating_leverage_signal(**kwargs) -> RuleResult:
248
+ """Operating Leverage Signal. Logic: IF DOL > 2 THEN high operating leverage"""
249
+ return _evaluate_threshold_logic('operating_leverage_signal', 'Operating Leverage Signal', 'L02_decision', 'IF DOL > 2 THEN high operating leverage', kwargs)
250
+
251
+ @rule('seasonality_flag', 'Seasonality Flag', 'L02_decision', 'IF quarterly variance high THEN seasonal')
252
+ def seasonality_flag(**kwargs) -> RuleResult:
253
+ """Seasonality Flag. Logic: IF quarterly variance high THEN seasonal"""
254
+ return _evaluate_threshold_logic('seasonality_flag', 'Seasonality Flag', 'L02_decision', 'IF quarterly variance high THEN seasonal', kwargs)
255
+
256
+ @rule('one_time_item_flag', 'One-Time Item Flag', 'L02_decision', 'IF item flagged unusual THEN exclude from run-rate')
257
+ def one_time_item_flag(**kwargs) -> RuleResult:
258
+ """One-Time Item Flag. Logic: IF item flagged unusual THEN exclude from run-rate"""
259
+ return _evaluate_threshold_logic('one_time_item_flag', 'One-Time Item Flag', 'L02_decision', 'IF item flagged unusual THEN exclude from run-rate', kwargs)
260
+
261
+ @rule('guidance_beat_miss', 'Guidance Beat/Miss', 'L02_decision', 'IF actual > guidance THEN beat ELSE miss')
262
+ def guidance_beat_miss(**kwargs) -> RuleResult:
263
+ """Guidance Beat/Miss. Logic: IF actual > guidance THEN beat ELSE miss"""
264
+ return _evaluate_threshold_logic('guidance_beat_miss', 'Guidance Beat/Miss', 'L02_decision', 'IF actual > guidance THEN beat ELSE miss', kwargs)
265
+
266
+ @rule('covenant_compliance', 'Covenant Compliance', 'L02_decision', 'IF ratio within covenant THEN compliant ELSE breach')
267
+ def covenant_compliance(**kwargs) -> RuleResult:
268
+ """Covenant Compliance. Logic: IF ratio within covenant THEN compliant ELSE breach"""
269
+ return _evaluate_threshold_logic('covenant_compliance', 'Covenant Compliance', 'L02_decision', 'IF ratio within covenant THEN compliant ELSE breach', kwargs)
270
+
271
+ @rule('margin_expansion_driver', 'Margin Driver Classification', 'L02_decision', 'Volume / price / mix / cost classification')
272
+ def margin_expansion_driver(**kwargs) -> RuleResult:
273
+ """Margin Driver Classification. Logic: Volume / price / mix / cost classification"""
274
+ return _evaluate_threshold_logic('margin_expansion_driver', 'Margin Driver Classification', 'L02_decision', 'Volume / price / mix / cost classification', kwargs)
275
+
276
+ @rule('trend_reversal', 'Trend Reversal Detection', 'L02_decision', 'IF direction changed vs prior trend THEN reversal')
277
+ def trend_reversal(**kwargs) -> RuleResult:
278
+ """Trend Reversal Detection. Logic: IF direction changed vs prior trend THEN reversal"""
279
+ return _evaluate_threshold_logic('trend_reversal', 'Trend Reversal Detection', 'L02_decision', 'IF direction changed vs prior trend THEN reversal', kwargs)
280
+
281
+ @rule('relative_strength', 'Relative Strength', 'L02_decision', 'Compare return to peer/benchmark')
282
+ def relative_strength(**kwargs) -> RuleResult:
283
+ """Relative Strength. Logic: Compare return to peer/benchmark"""
284
+ return _evaluate_threshold_logic('relative_strength', 'Relative Strength', 'L02_decision', 'Compare return to peer/benchmark', kwargs)
285
+
286
+ @rule('efficiency_improving', 'Efficiency Improving', 'L02_decision', 'Compare turnover/CCC across periods')
287
+ def efficiency_improving(**kwargs) -> RuleResult:
288
+ """Efficiency Improving. Logic: Compare turnover/CCC across periods"""
289
+ return _evaluate_threshold_logic('efficiency_improving', 'Efficiency Improving', 'L02_decision', 'Compare turnover/CCC across periods', kwargs)
290
+
291
+ @rule('balance_sheet_strength', 'Balance Sheet Strength', 'L02_decision', 'Combine leverage + liquidity + coverage')
292
+ def balance_sheet_strength(**kwargs) -> RuleResult:
293
+ """Balance Sheet Strength. Logic: Combine leverage + liquidity + coverage"""
294
+ return _evaluate_threshold_logic('balance_sheet_strength', 'Balance Sheet Strength', 'L02_decision', 'Combine leverage + liquidity + coverage', kwargs)
295
+
296
+ @rule('going_concern_signal', 'Going Concern Signal', 'L02_decision', 'Combine Altman + liquidity + losses')
297
+ def going_concern_signal(**kwargs) -> RuleResult:
298
+ """Going Concern Signal. Logic: Combine Altman + liquidity + losses"""
299
+ return _evaluate_threshold_logic('going_concern_signal', 'Going Concern Signal', 'L02_decision', 'Combine Altman + liquidity + losses', kwargs)