1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! QDII fund data from Jisilu (集思录).
use serde::Deserialize;
use crate::client::AkShareClient;
use crate::error::Result;
use crate::types::MacroDataPoint;
// ---------------------------------------------------------------------------
// Wire types
// ---------------------------------------------------------------------------
#[derive(Debug, Deserialize)]
struct JisiluResponse {
rows: Option<Vec<JisiluRow>>,
}
#[derive(Debug, Deserialize)]
struct JisiluRow {
cell: Option<serde_json::Value>,
}
// ---------------------------------------------------------------------------
// Implementation
// ---------------------------------------------------------------------------
impl AkShareClient {
/// Jisilu T+0 QDII - Asian market index funds.
///
/// Returns QDII fund data for Asian market index tracking funds.
/// `cookie` is optional for authenticated access.
pub async fn qdii_a_index_jsl(&self, cookie: &str) -> Result<Vec<MacroDataPoint>> {
self.fetch_qdii_jsl("A", cookie).await
}
/// Jisilu T+0 QDII - European/American market index funds.
///
/// Returns QDII fund data for European/American market index tracking funds.
pub async fn qdii_e_index_jsl(&self, cookie: &str) -> Result<Vec<MacroDataPoint>> {
self.fetch_qdii_jsl("E", cookie).await
}
/// Jisilu T+0 QDII - European/American commodity funds.
///
/// Returns QDII fund data for European/American commodity tracking funds.
pub async fn qdii_e_comm_jsl(&self, cookie: &str) -> Result<Vec<MacroDataPoint>> {
self.fetch_qdii_jsl("E", cookie).await
}
// Internal helper
async fn fetch_qdii_jsl(&self, market: &str, cookie: &str) -> Result<Vec<MacroDataPoint>> {
let url = format!("https://www.jisilu.cn/data/qdii/qdii_list/{}", market);
let mut req = self
.get(&url)
.query(&[("___jsl", "LST___t=1728207798534"), ("rp", "22")]);
if !cookie.is_empty() {
req = req.header("Cookie", cookie);
}
let resp: JisiluResponse = req.send().await?.json().await?;
let rows = resp.rows.unwrap_or_default();
let mut items = Vec::new();
for row in &rows {
if let Some(cell) = &row.cell {
let fund_id = cell
.get("fund_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let fund_name = cell
.get("fund_nm")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let price = cell.get("price").and_then(|v| v.as_f64()).unwrap_or(0.0);
let nav = cell.get("fund_nav").and_then(|v| v.as_f64()).unwrap_or(0.0);
let discount_rt = cell
.get("discount_rt")
.and_then(|v| v.as_str())
.and_then(|s| s.trim_end_matches('%').parse::<f64>().ok())
.unwrap_or(0.0);
if !fund_id.is_empty() {
items.push(MacroDataPoint {
date: fund_id,
value: price,
name: fund_name,
});
// Also add NAV as a separate data point
items.push(MacroDataPoint {
date: format!("{}_nav", items.last().unwrap().date),
value: nav,
name: format!("{} NAV", items.last().unwrap().name),
});
// Add discount rate
items.push(MacroDataPoint {
date: format!("{}_discount", items[items.len() - 2].date),
value: discount_rt,
name: format!("{} Discount%", items[items.len() - 2].name),
});
}
}
}
Ok(items)
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_qdii_jsl_response_structure() {
let json = r#"{
"rows": [
{
"cell": {
"fund_id": "513100",
"fund_nm": "纳指ETF",
"price": 1.234,
"fund_nav": 1.230,
"discount_rt": "0.33%"
}
}
]
}"#;
let resp: super::JisiluResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.rows.as_ref().unwrap().len(), 1);
let rows = resp.rows.unwrap();
let cell = rows[0].cell.as_ref().unwrap();
assert_eq!(cell.get("fund_id").unwrap().as_str().unwrap(), "513100");
}
}