mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
feat: bandarmology (idx stocks bandar, analyze_bandar)
`idx stocks bandar SYMBOL...` reads the broker summary: for the top 1, 3, 5 and 10 brokers per side, buy value minus sell value in basis points of value traded, with a signal from big accumulation to big distribution on the headline tier (--top, default 5). Several symbols give a screen ranked by that figure (at most 100, one request each), which pipes from `stocks screen`. The MCP server exposes it as analyze_bandar (20 symbols). - analysis/bandar.rs is pure and versioned (`top_concentration_v1`): integer basis points, no figure missing counted as zero, buy/sell VWAP from exact averages, and the brokers on both sides listed because their gross figures overstate their direction. - Thresholds (±5%/±15%) follow fzn0x/idx (MIT); docs call them heuristics. - Invalid input fails a screen at once; other errors fail one symbol and are warned about on stderr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
parent
ae7a1e2b5c
commit
970da26a0e
25 changed files with 1588 additions and 37 deletions
|
|
@ -1,3 +1,4 @@
|
|||
pub mod bandar;
|
||||
pub mod fundamental;
|
||||
pub mod report;
|
||||
pub mod signals;
|
||||
|
|
@ -8,9 +9,11 @@ use serde::de::DeserializeOwned;
|
|||
|
||||
use crate::api::history_provider;
|
||||
use crate::api::types::{Fundamentals, Interval, Period};
|
||||
use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerRange};
|
||||
use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch};
|
||||
use crate::error::IdxError;
|
||||
use crate::market::HistorySource;
|
||||
use bandar::{BandarReport, analyze_bandar, check_top};
|
||||
use fundamental::{
|
||||
FundamentalReport, GrowthReport, RiskReport, ValuationReport, analyze_fundamental,
|
||||
analyze_growth, analyze_risk, analyze_valuation,
|
||||
|
|
@ -28,7 +31,69 @@ pub struct Comparison {
|
|||
pub failures: Vec<(String, IdxError)>,
|
||||
}
|
||||
|
||||
/// Bandar readings for several symbols, strongest accumulation first;
|
||||
/// symbols that failed are listed with their error.
|
||||
pub struct BandarScreen {
|
||||
pub reports: Vec<Fetched<BandarReport>>,
|
||||
pub failures: Vec<(String, IdxError)>,
|
||||
}
|
||||
|
||||
/// The sessions and trades a bandar reading covers.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BandarQuery {
|
||||
pub range: BrokerRange,
|
||||
pub investor: BrokerInvestor,
|
||||
pub board: BrokerBoard,
|
||||
/// Headline tier, one of `bandar::TIERS`.
|
||||
pub top: u8,
|
||||
}
|
||||
|
||||
impl Analysis<'_> {
|
||||
/// Bandarmology over the broker summary `query` selects.
|
||||
pub fn bandar(
|
||||
&self,
|
||||
symbol: &str,
|
||||
query: &BandarQuery,
|
||||
policy: CachePolicy,
|
||||
) -> Result<Fetched<BandarReport>, IdxError> {
|
||||
let top = check_top(query.top)?;
|
||||
let summary =
|
||||
self.idx
|
||||
.broker()
|
||||
.fetch(symbol, query.range, query.investor, query.board, policy)?;
|
||||
Ok(summary.map(|summary| analyze_bandar(&summary, top)))
|
||||
}
|
||||
|
||||
/// [`Self::bandar`] for each symbol, ranked by the headline imbalance.
|
||||
/// Invalid input fails the whole screen; other errors fail one symbol.
|
||||
pub fn bandar_screen<S: AsRef<str>>(
|
||||
&self,
|
||||
symbols: &[S],
|
||||
query: &BandarQuery,
|
||||
policy: CachePolicy,
|
||||
) -> Result<BandarScreen, IdxError> {
|
||||
check_top(query.top)?;
|
||||
let mut reports = Vec::new();
|
||||
let mut failures = Vec::new();
|
||||
for symbol in symbols {
|
||||
let resolved = self.idx.resolve(symbol.as_ref())?;
|
||||
match self.bandar(&resolved, query, policy) {
|
||||
Ok(report) => reports.push(report),
|
||||
Err(err @ IdxError::InvalidInput(_)) => return Err(err),
|
||||
Err(err) => failures.push((resolved, err)),
|
||||
}
|
||||
}
|
||||
if reports.is_empty() {
|
||||
return Err(failures
|
||||
.pop()
|
||||
.map(|(_, err)| err)
|
||||
.unwrap_or_else(|| IdxError::InvalidInput("no symbols to screen".to_string())));
|
||||
}
|
||||
// Strongest accumulation first; readings without an imbalance last.
|
||||
reports.sort_by_key(|report| std::cmp::Reverse(report.data.imbalance_bps));
|
||||
Ok(BandarScreen { reports, failures })
|
||||
}
|
||||
|
||||
/// Technical analysis over one year of daily history from `source`
|
||||
/// (see `Market::history_source`).
|
||||
pub fn technical(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue