mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
fix(core): version the broker summary cache bucket
The cache stores normalized summaries, so one written before buy_value and sell_value existed read back without them: bandar gave null readings (the table said "No trades.") for a week, and an investor-filtered entry lacked its totals. Summaries now cache under ipot-broker-v2, so an old-shape entry is never served; offline it is a cache miss, online it is fetched again. A CLI test seeds an old entry and checks both, in JSON and table output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
parent
5f64319157
commit
b137dbf540
2 changed files with 73 additions and 2 deletions
|
|
@ -2933,3 +2933,69 @@ fn bandar_reads_one_investor_side_against_its_own_trading() {
|
||||||
"{table}"
|
"{table}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bandar_never_reads_a_broker_summary_cached_in_the_old_shape() {
|
||||||
|
// A summary cached before buy_value/sell_value existed: fresh for a
|
||||||
|
// week, with trades, but no denominators for a reading.
|
||||||
|
let root = test_env_dir("bandar-legacy-cache");
|
||||||
|
let bucket = root.join("cache").join("idx").join("ipot-broker");
|
||||||
|
fs::create_dir_all(&bucket).unwrap();
|
||||||
|
let flow = |code: &str, value: i64| {
|
||||||
|
serde_json::json!({"rank": 1, "code": code, "name": null, "kind": null,
|
||||||
|
"lots": value / 100, "value": value, "average_price": 1})
|
||||||
|
};
|
||||||
|
let legacy = serde_json::json!({
|
||||||
|
"fetched_at": chrono::Utc::now().to_rfc3339(),
|
||||||
|
"ttl_secs": 7 * 24 * 3600,
|
||||||
|
"schema_version": 1,
|
||||||
|
"data": {
|
||||||
|
"symbol": "BBCA.JK", "from": "2026-10-16", "to": "2026-10-16",
|
||||||
|
"investor": "all", "board": "all",
|
||||||
|
"buyers": [flow("AA", 60_000)], "sellers": [flow("BB", 40_000)],
|
||||||
|
"total_value": 100_000, "total_lots": 1_000, "average_price": 100,
|
||||||
|
"foreign_net_value": null, "rounded": false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fs::write(
|
||||||
|
bucket.join("BBCA-2026-10-16-2026-10-16-all-all.json"),
|
||||||
|
legacy.to_string(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Offline, the old entry is not served as a complete summary.
|
||||||
|
run_error_json(
|
||||||
|
mock_bin_with_root(&root).args([
|
||||||
|
"-o",
|
||||||
|
"json",
|
||||||
|
"--offline",
|
||||||
|
"stocks",
|
||||||
|
"bandar",
|
||||||
|
"BBCA",
|
||||||
|
"--date",
|
||||||
|
"2026-10-16",
|
||||||
|
]),
|
||||||
|
"CACHEMISS",
|
||||||
|
);
|
||||||
|
// Online, the summary is fetched again and read in full.
|
||||||
|
let reports = run_success_json(mock_bin_with_root(&root).args([
|
||||||
|
"-o",
|
||||||
|
"json",
|
||||||
|
"stocks",
|
||||||
|
"bandar",
|
||||||
|
"BBCA",
|
||||||
|
"--date",
|
||||||
|
"2026-10-16",
|
||||||
|
]));
|
||||||
|
assert_eq!(reports[0]["imbalance_bps"], -2425);
|
||||||
|
let table = run_success_stdout(mock_bin_with_root(&root).args([
|
||||||
|
"--no-color",
|
||||||
|
"stocks",
|
||||||
|
"bandar",
|
||||||
|
"BBCA",
|
||||||
|
"--date",
|
||||||
|
"2026-10-16",
|
||||||
|
]));
|
||||||
|
assert!(!table.contains("No trades."), "{table}");
|
||||||
|
assert!(table.contains("big distribution"), "{table}");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ use crate::error::IdxError;
|
||||||
use types::{BrokerBoard, BrokerInvestor, BrokerQuery, BrokerRange, BrokerSummary};
|
use types::{BrokerBoard, BrokerInvestor, BrokerQuery, BrokerRange, BrokerSummary};
|
||||||
|
|
||||||
const SOURCE: &str = "ipot";
|
const SOURCE: &str = "ipot";
|
||||||
|
/// Cache bucket for normalized summaries, versioned with their shape: a
|
||||||
|
/// summary cached before `buy_value`/`sell_value` existed would read back
|
||||||
|
/// without them (and an investor-filtered one without its totals), so a new
|
||||||
|
/// shape gets a new bucket rather than serving such entries.
|
||||||
|
const CACHE_BUCKET: &str = "ipot-broker-v2";
|
||||||
/// Longest range one summary may cover.
|
/// Longest range one summary may cover.
|
||||||
pub const MAX_RANGE_DAYS: i64 = 366;
|
pub const MAX_RANGE_DAYS: i64 = 366;
|
||||||
/// Calendar days searched back for the latest session with trades.
|
/// Calendar days searched back for the latest session with trades.
|
||||||
|
|
@ -98,7 +103,7 @@ impl Broker<'_> {
|
||||||
cached_fetch(
|
cached_fetch(
|
||||||
&self.idx.cache,
|
&self.idx.cache,
|
||||||
CacheSpec {
|
CacheSpec {
|
||||||
bucket: "ipot-broker",
|
bucket: CACHE_BUCKET,
|
||||||
key: &key,
|
key: &key,
|
||||||
ttl_secs: if query.to < today {
|
ttl_secs: if query.to < today {
|
||||||
TTL_CLOSED_SECS
|
TTL_CLOSED_SECS
|
||||||
|
|
@ -146,7 +151,7 @@ impl Broker<'_> {
|
||||||
}
|
}
|
||||||
if policy == CachePolicy::Offline {
|
if policy == CachePolicy::Offline {
|
||||||
return Err(IdxError::CacheMiss(format!(
|
return Err(IdxError::CacheMiss(format!(
|
||||||
"ipot-broker/{resolved} (no cached session in the last {LATEST_LOOKBACK_DAYS} days)"
|
"{CACHE_BUCKET}/{resolved} (no cached session in the last {LATEST_LOOKBACK_DAYS} days)"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
Err(IdxError::NoMarketData(format!(
|
Err(IdxError::NoMarketData(format!(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue