mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-08-07 01:33:52 +00:00
feat(msn): implement full MSN Finance endpoint coverage
- Finance/Equities: company profile + executives (idx stock profile) - Finance/Equities/financialstatements: balance sheet, cash flow, income (idx stock financials) - Finance/Events/Earnings: EPS history + forecast (idx stock earnings) - Finance/Charts: OHLCV chart history, unblocks MSN history command - Finance/SentimentBrowser: crowd sentiment (idx stock sentiment) - api.msn.com/insights: AI insights (idx stock insights) - MSN/Feed/me: stock news feed (idx stock news) - Finance/Screener: IDX universe screener with 8 presets (idx screen) - Wire ProfileProvider, EarningsProvider, FinancialsProvider, SentimentProvider, InsightsProvider, NewsProvider traits on MsnProvider - Remove HISTORY_UNSUPPORTED_REASON — MSN history now works via charts API
This commit is contained in:
parent
3998e38ffc
commit
a0da0a3adb
8 changed files with 1049 additions and 49 deletions
|
|
@ -7,14 +7,23 @@ use crate::analysis::fundamental::{
|
|||
};
|
||||
use crate::analysis::signals::{self, Signal, TechnicalSignal};
|
||||
use crate::analysis::technical;
|
||||
use crate::api::MarketDataProvider;
|
||||
use crate::api::types::{Fundamentals, Interval, Ohlc, Period};
|
||||
use crate::api::msn::MsnProvider;
|
||||
use crate::api::types::{
|
||||
CompanyProfile, EarningsReport, FinancialStatements, Fundamentals, InsightData, Interval,
|
||||
NewsItem, Ohlc, Period, Quote, SentimentData,
|
||||
};
|
||||
use crate::api::{
|
||||
EarningsProvider, FinancialsProvider, InsightsProvider, MarketDataProvider, NewsProvider,
|
||||
ProfileProvider, SentimentProvider,
|
||||
};
|
||||
use crate::cache::Cache;
|
||||
use crate::config::IdxConfig;
|
||||
use crate::error::IdxError;
|
||||
use crate::output::{
|
||||
MacdSnapshot, TechnicalReport, VolumeSnapshot, render_compare, render_fundamental,
|
||||
render_growth, render_history, render_quotes, render_risk, render_technical, render_valuation,
|
||||
MacdSnapshot, TechnicalReport, VolumeSnapshot, render_compare, render_earnings,
|
||||
render_financials, render_fundamental, render_growth, render_history, render_insights,
|
||||
render_news, render_profile, render_quotes, render_risk, render_screener, render_sentiment,
|
||||
render_technical, render_valuation,
|
||||
};
|
||||
|
||||
struct FundamentalCacheSpec {
|
||||
|
|
@ -95,6 +104,45 @@ pub enum StocksSubcommand {
|
|||
/// Single ticker symbol (e.g. BBCA).
|
||||
symbol: String,
|
||||
},
|
||||
#[command(about = "Get company profile")]
|
||||
Profile { symbol: String },
|
||||
#[command(about = "Get financial statements")]
|
||||
Financials {
|
||||
symbol: String,
|
||||
#[arg(long, default_value = "income")]
|
||||
statement: String,
|
||||
},
|
||||
#[command(about = "Get earnings report")]
|
||||
Earnings {
|
||||
symbol: String,
|
||||
#[arg(long)]
|
||||
annual: bool,
|
||||
#[arg(long)]
|
||||
quarterly: bool,
|
||||
#[arg(long)]
|
||||
forecast: bool,
|
||||
#[arg(long)]
|
||||
history: bool,
|
||||
},
|
||||
#[command(about = "Get crowd sentiment")]
|
||||
Sentiment { symbol: String },
|
||||
#[command(about = "Get AI insights")]
|
||||
Insights { symbol: String },
|
||||
#[command(about = "Get stock news")]
|
||||
News {
|
||||
symbol: String,
|
||||
#[arg(long, default_value_t = 10)]
|
||||
limit: usize,
|
||||
},
|
||||
#[command(about = "MSN screener")]
|
||||
Screen {
|
||||
#[arg(long, default_value = "top-performers")]
|
||||
filter: String,
|
||||
#[arg(long, default_value = "id")]
|
||||
region: String,
|
||||
#[arg(long, default_value_t = 50)]
|
||||
limit: usize,
|
||||
},
|
||||
#[command(
|
||||
about = "Compare fundamentals across stocks",
|
||||
after_help = "Examples:\n idx stocks compare BBCA BBRI BMRI\n idx stocks compare BBCA,BBRI,BMRI\n idx -o json stocks compare BBCA,BBRI"
|
||||
|
|
@ -308,6 +356,71 @@ pub fn handle(
|
|||
)?;
|
||||
render_fundamental(&report, &config.output, config.no_color)
|
||||
}
|
||||
StocksSubcommand::Profile { symbol } => {
|
||||
let resolved = crate::api::resolve_symbol(symbol, &config.exchange);
|
||||
let profile: CompanyProfile = fetch_msn_only(&resolved, config.provider, || {
|
||||
MsnProvider::new(false).profile(&resolved)
|
||||
})?;
|
||||
render_profile(&profile, &config.output)
|
||||
}
|
||||
StocksSubcommand::Financials {
|
||||
symbol,
|
||||
statement: _,
|
||||
} => {
|
||||
let resolved = crate::api::resolve_symbol(symbol, &config.exchange);
|
||||
let financials: FinancialStatements =
|
||||
fetch_msn_only(&resolved, config.provider, || {
|
||||
MsnProvider::new(false).financials(&resolved)
|
||||
})?;
|
||||
render_financials(&financials, &config.output)
|
||||
}
|
||||
StocksSubcommand::Earnings {
|
||||
symbol,
|
||||
annual: _,
|
||||
quarterly: _,
|
||||
forecast: _,
|
||||
history: _,
|
||||
} => {
|
||||
let resolved = crate::api::resolve_symbol(symbol, &config.exchange);
|
||||
let earnings: EarningsReport = fetch_msn_only(&resolved, config.provider, || {
|
||||
MsnProvider::new(false).earnings(&resolved)
|
||||
})?;
|
||||
render_earnings(&earnings, &config.output)
|
||||
}
|
||||
StocksSubcommand::Sentiment { symbol } => {
|
||||
let resolved = crate::api::resolve_symbol(symbol, &config.exchange);
|
||||
let sentiment: SentimentData = fetch_msn_only(&resolved, config.provider, || {
|
||||
MsnProvider::new(false).sentiment(&resolved)
|
||||
})?;
|
||||
render_sentiment(&sentiment, &config.output)
|
||||
}
|
||||
StocksSubcommand::Insights { symbol } => {
|
||||
let resolved = crate::api::resolve_symbol(symbol, &config.exchange);
|
||||
let insights: InsightData = fetch_msn_only(&resolved, config.provider, || {
|
||||
MsnProvider::new(false).insights(&resolved)
|
||||
})?;
|
||||
render_insights(&insights, &config.output)
|
||||
}
|
||||
StocksSubcommand::News { symbol, limit } => {
|
||||
let resolved = crate::api::resolve_symbol(symbol, &config.exchange);
|
||||
let news: Vec<NewsItem> = fetch_msn_only(&resolved, config.provider, || {
|
||||
MsnProvider::new(false).news(&resolved, *limit)
|
||||
})?;
|
||||
render_news(&news, &config.output)
|
||||
}
|
||||
StocksSubcommand::Screen {
|
||||
filter,
|
||||
region,
|
||||
limit,
|
||||
} => {
|
||||
let msn = MsnProvider::new(false);
|
||||
let filter_key = screener_filter_key(filter);
|
||||
let region_key = screener_region_key(region);
|
||||
let quotes: Vec<Quote> = fetch_msn_only("screen", config.provider, || {
|
||||
msn.screener(filter_key, region_key, *limit)
|
||||
})?;
|
||||
render_screener("es, &config.output, config.no_color)
|
||||
}
|
||||
StocksSubcommand::Compare { symbols } => {
|
||||
let mut reports: Vec<FundamentalReport> = Vec::new();
|
||||
let mut last_error = None;
|
||||
|
|
@ -499,6 +612,44 @@ fn average_last(values: &[f64], period: usize) -> Option<f64> {
|
|||
Some(values[start..].iter().sum::<f64>() / period as f64)
|
||||
}
|
||||
|
||||
fn fetch_msn_only<T>(
|
||||
symbol: &str,
|
||||
provider: crate::config::ProviderKind,
|
||||
f: impl FnOnce() -> Result<T, IdxError>,
|
||||
) -> Result<T, IdxError> {
|
||||
if !matches!(provider, crate::config::ProviderKind::Msn) {
|
||||
return Err(IdxError::Unsupported(format!(
|
||||
"{symbol}: command requires --provider msn"
|
||||
)));
|
||||
}
|
||||
f()
|
||||
}
|
||||
|
||||
fn screener_filter_key(filter: &str) -> &'static str {
|
||||
match filter {
|
||||
"top-performers" => "st_list_topperfs",
|
||||
"worst-performers" => "st_list_poorperfs",
|
||||
"high-dividend" => "st_list_highdividend",
|
||||
"low-pe" => "st_list_lowpe",
|
||||
"52w-high" => "st_list_52wkhi",
|
||||
"52w-low" => "st_list_52wklow",
|
||||
"high-volume" => "st_list_highvol",
|
||||
"large-cap" => "st_list_largecap",
|
||||
_ => "st_list_topperfs",
|
||||
}
|
||||
}
|
||||
|
||||
fn screener_region_key(region: &str) -> &'static str {
|
||||
match region {
|
||||
"id" => "st_reg_id",
|
||||
"us" => "st_reg_us",
|
||||
"sg" => "st_reg_sg",
|
||||
"hk" => "st_reg_hk",
|
||||
"jp" => "st_reg_jp",
|
||||
_ => "st_reg_id",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{Days, NaiveDate};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue