mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
fix(analysis): ratings need evidence; negative equity is high risk
From the full-repo Codex audit: - The overall fundamental rating was "healthy" whenever growth was growing and valuation and risk were merely not "expensive" / not "high risk", so with no valuation or risk data at all (partial MSN fundamentals) it still read "healthy". It now needs valuation undervalued or fairly valued and risk low or moderate. - Risk recognized only heavy leverage or a weak current ratio as high risk; negative equity fell through to "moderate risk", and a known adverse signal with the other metric missing became "incomplete data". Any known adverse signal now makes it high risk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
parent
5cff14bf90
commit
49b2d3aaba
2 changed files with 44 additions and 6 deletions
|
|
@ -148,12 +148,16 @@ pub fn analyze_risk(fundamentals: &Fundamentals) -> RiskReport {
|
||||||
let de_signal = de_signal(fundamentals.debt_to_equity);
|
let de_signal = de_signal(fundamentals.debt_to_equity);
|
||||||
let current_ratio_signal = cr_signal(fundamentals.current_ratio);
|
let current_ratio_signal = cr_signal(fundamentals.current_ratio);
|
||||||
|
|
||||||
let overall_signal = if de_signal == "no data" && current_ratio_signal == "no data" {
|
// A known adverse signal decides on its own: negative equity or heavy
|
||||||
|
// leverage is high risk even when the current ratio is missing.
|
||||||
|
let overall_signal = if matches!(de_signal, "highly leveraged" | "negative equity")
|
||||||
|
|| current_ratio_signal == "weak"
|
||||||
|
{
|
||||||
|
"high risk"
|
||||||
|
} else if de_signal == "no data" && current_ratio_signal == "no data" {
|
||||||
"no data"
|
"no data"
|
||||||
} else if de_signal == "no data" || current_ratio_signal == "no data" {
|
} else if de_signal == "no data" || current_ratio_signal == "no data" {
|
||||||
"incomplete data"
|
"incomplete data"
|
||||||
} else if de_signal == "highly leveraged" || current_ratio_signal == "weak" {
|
|
||||||
"high risk"
|
|
||||||
} else if de_signal == "conservative" && matches!(current_ratio_signal, "strong" | "adequate") {
|
} else if de_signal == "conservative" && matches!(current_ratio_signal, "strong" | "adequate") {
|
||||||
"low risk"
|
"low risk"
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -176,9 +180,14 @@ pub fn analyze_fundamental(symbol: &str, fundamentals: &Fundamentals) -> Fundame
|
||||||
let valuation = analyze_valuation(fundamentals);
|
let valuation = analyze_valuation(fundamentals);
|
||||||
let risk = analyze_risk(fundamentals);
|
let risk = analyze_risk(fundamentals);
|
||||||
|
|
||||||
|
// "healthy" needs evidence on all three: missing valuation or risk data
|
||||||
|
// must not pass for "not expensive" or "not risky".
|
||||||
let overall_signal = if growth.overall_signal == "growing"
|
let overall_signal = if growth.overall_signal == "growing"
|
||||||
&& valuation.overall_signal != "expensive"
|
&& matches!(
|
||||||
&& risk.overall_signal != "high risk"
|
valuation.overall_signal.as_str(),
|
||||||
|
"undervalued" | "fairly valued"
|
||||||
|
)
|
||||||
|
&& matches!(risk.overall_signal.as_str(), "low risk" | "moderate risk")
|
||||||
{
|
{
|
||||||
"healthy"
|
"healthy"
|
||||||
} else if growth.overall_signal == "shrinking" && risk.overall_signal == "high risk" {
|
} else if growth.overall_signal == "shrinking" && risk.overall_signal == "high risk" {
|
||||||
|
|
@ -375,6 +384,35 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_valuation_and_risk_data_is_not_healthy() {
|
||||||
|
// Growth alone (12% revenue, 22% earnings) used to rate "healthy".
|
||||||
|
let fundamentals = Fundamentals {
|
||||||
|
revenue_growth: Some(0.12),
|
||||||
|
earnings_growth: Some(0.22),
|
||||||
|
..Fundamentals::default()
|
||||||
|
};
|
||||||
|
let report = analyze_fundamental("X.JK", &fundamentals);
|
||||||
|
assert_eq!(report.growth.overall_signal, "growing");
|
||||||
|
assert_eq!(report.overall_signal, "mixed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negative_equity_is_high_risk_and_not_healthy() {
|
||||||
|
let mut fundamentals = sample_fundamentals();
|
||||||
|
fundamentals.debt_to_equity = Some(-10.0);
|
||||||
|
assert_eq!(analyze_risk(&fundamentals).overall_signal, "high risk");
|
||||||
|
assert_eq!(
|
||||||
|
analyze_fundamental("X.JK", &fundamentals).overall_signal,
|
||||||
|
"mixed"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Known heavy leverage decides even with the current ratio missing.
|
||||||
|
fundamentals.debt_to_equity = Some(250.0);
|
||||||
|
fundamentals.current_ratio = None;
|
||||||
|
assert_eq!(analyze_risk(&fundamentals).overall_signal, "high risk");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn growth_signal_thresholds_match_python() {
|
fn growth_signal_thresholds_match_python() {
|
||||||
assert_eq!(growth_signal(None), "no data");
|
assert_eq!(growth_signal(None), "no data");
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ pub struct Ohlc {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fundamental metrics normalized from Yahoo Finance `/v10/finance/quoteSummary`.
|
/// Fundamental metrics normalized from Yahoo Finance `/v10/finance/quoteSummary`.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct Fundamentals {
|
pub struct Fundamentals {
|
||||||
pub trailing_pe: Option<f64>,
|
pub trailing_pe: Option<f64>,
|
||||||
pub forward_pe: Option<f64>,
|
pub forward_pe: Option<f64>,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue