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:
Ciphercat 2026-03-06 19:32:56 +00:00
commit a0da0a3adb
8 changed files with 1049 additions and 49 deletions

View file

@ -4,17 +4,25 @@ mod parse;
mod raw_types;
mod symbols;
use crate::api::types::{Bar, Fundamentals, Interval, Period, Quote};
use crate::api::{FundamentalsProvider, HistoryProvider, QuoteProvider};
use crate::api::types::{
Bar, CompanyProfile, EarningsReport, FinancialStatements, Fundamentals, InsightData, Interval,
NewsItem, Period, Quote, SentimentData,
};
use crate::api::{
EarningsProvider, FinancialsProvider, FundamentalsProvider, HistoryProvider, InsightsProvider,
NewsProvider, ProfileProvider, QuoteProvider, SentimentProvider,
};
use crate::error::IdxError;
use client::MsnClient;
use map::{parse_fundamentals, parse_quote};
use map::{
parse_chart_history, parse_earnings, parse_financial_statements, parse_fundamentals,
parse_insights, parse_news, parse_profile, parse_quote, parse_screener_results,
parse_sentiment,
};
pub(crate) use parse::{parse_fundamentals_from_str, parse_history_from_str, parse_quote_from_str};
const HISTORY_UNSUPPORTED_REASON: &str = "MSN provider does not currently support history or technical analysis because MSN charts do not consistently expose real OHLCV data";
pub struct MsnProvider {
client: MsnClient,
}
@ -25,6 +33,16 @@ impl MsnProvider {
client: MsnClient::new(),
}
}
pub fn screener(
&self,
filter: &str,
region: &str,
limit: usize,
) -> Result<Vec<Quote>, IdxError> {
let raw = self.client.fetch_screener(filter, region, limit)?;
parse_screener_results(&raw)
}
}
impl QuoteProvider for MsnProvider {
@ -45,33 +63,67 @@ impl FundamentalsProvider for MsnProvider {
impl HistoryProvider for MsnProvider {
fn history(
&self,
_symbol: &str,
_period: &Period,
symbol: &str,
period: &Period,
_interval: &Interval,
) -> Result<Vec<Bar>, IdxError> {
Err(IdxError::Unsupported(
HISTORY_UNSUPPORTED_REASON.to_string(),
))
let chart_type = period_to_chart_type(period);
let raw = self.client.fetch_charts(symbol, chart_type)?;
parse_chart_history(symbol, period, &raw)
}
}
#[cfg(test)]
mod tests {
use super::MsnProvider;
use crate::api::HistoryProvider;
use crate::api::types::{Interval, Period};
use crate::error::IdxError;
impl ProfileProvider for MsnProvider {
fn profile(&self, symbol: &str) -> Result<CompanyProfile, IdxError> {
let raw = self.client.fetch_equities(symbol)?;
parse_profile(symbol, &raw)
}
}
#[test]
fn history_is_explicitly_unsupported() {
let provider = MsnProvider::new(false);
let err = provider
.history("BBCA.JK", &Period::OneMonth, &Interval::Day)
.expect_err("history should be unsupported");
assert!(matches!(err, IdxError::Unsupported(_)));
assert!(
err.to_string()
.contains("MSN provider does not currently support history or technical analysis")
);
impl EarningsProvider for MsnProvider {
fn earnings(&self, symbol: &str) -> Result<EarningsReport, IdxError> {
let raw = self.client.fetch_earnings(symbol)?;
parse_earnings(symbol, &raw)
}
}
impl FinancialsProvider for MsnProvider {
fn financials(&self, symbol: &str) -> Result<FinancialStatements, IdxError> {
let raw = self.client.fetch_financial_statements(symbol)?;
parse_financial_statements(symbol, &raw)
}
}
impl SentimentProvider for MsnProvider {
fn sentiment(&self, symbol: &str) -> Result<SentimentData, IdxError> {
let raw = self.client.fetch_sentiment(symbol)?;
parse_sentiment(symbol, &raw)
}
}
impl InsightsProvider for MsnProvider {
fn insights(&self, symbol: &str) -> Result<InsightData, IdxError> {
let raw = self.client.fetch_insights(symbol)?;
parse_insights(symbol, &raw)
}
}
impl NewsProvider for MsnProvider {
fn news(&self, symbol: &str, limit: usize) -> Result<Vec<NewsItem>, IdxError> {
let raw = self.client.fetch_news(symbol, limit)?;
parse_news(&raw)
}
}
fn period_to_chart_type(period: &Period) -> &'static str {
match period {
Period::OneDay => "1D",
Period::FiveDays => "1W",
Period::OneMonth => "1M",
Period::ThreeMonths => "3M",
Period::SixMonths => "6M",
Period::OneYear => "1Y",
Period::TwoYears => "3Y",
Period::FiveYears => "5Y",
}
}