#![allow(dead_code)]
use serde::Deserialize;
use crate::client::AkShareClient;
use crate::error::{Error, Result};
use crate::util::parse_f64_safe;
#[derive(Debug, Deserialize)]
struct SinaStockNameResponse {
result: Option<SinaStockNameResult>,
}
#[derive(Debug, Deserialize)]
struct SinaStockNameResult {
data: Option<SinaStockNameData>,
}
#[derive(Debug, Deserialize)]
struct SinaStockNameData {
#[serde(default)]
contract_month: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct SinaRemainderResponse {
result: Option<SinaRemainderResult>,
}
#[derive(Debug, Deserialize)]
struct SinaRemainderResult {
data: Option<SinaRemainderData>,
}
#[derive(Debug, Deserialize)]
struct SinaRemainderData {
#[serde(default)]
expire_day: String,
#[serde(default)]
remainder_days: String,
}
#[derive(Debug, Deserialize)]
struct SinaMinuteResponse {
result: Option<SinaMinuteResult>,
}
#[derive(Debug, Deserialize)]
struct SinaMinuteResult {
data: Option<Vec<serde_json::Value>>,
}
#[derive(Debug, Deserialize)]
struct SinaDailyJsonpResponse {
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct OptionSseCodeEntry {
pub index: usize,
pub code: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct OptionFieldValuePair {
pub field: String,
pub value: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct OptionSseMinuteRow {
pub date: String,
pub time: String,
pub price: f64,
pub volume: f64,
pub open_interest: f64,
pub average_price: f64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct OptionSseDailyRow {
pub date: String,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct OptionFinanceMinuteRow {
pub date: String,
pub time: String,
pub price: f64,
pub average_price: f64,
pub volume: f64,
}
const SINA_REFERER: &str = "https://stock.finance.sina.com.cn/";
impl AkShareClient {
pub async fn option_sse_list_sina(&self, symbol: &str, exchange: &str) -> Result<Vec<String>> {
let url = "https://stock.finance.sina.com.cn/futures/api/openapi.php/StockOptionService.getStockName";
let resp: SinaStockNameResponse = self
.get(url)
.query(&[("exchange", exchange), ("cate", symbol)])
.send()
.await
.map_err(Error::from)?
.json()
.await
.map_err(Error::from)?;
let months = resp
.result
.and_then(|r| r.data)
.map(|d| d.contract_month)
.unwrap_or_default();
Ok(months
.into_iter()
.skip(1)
.map(|m| m.replace('-', ""))
.collect())
}
pub async fn option_sse_expire_day_sina(
&self,
trade_date: &str,
symbol: &str,
exchange: &str,
) -> Result<(String, i64)> {
let url = "https://stock.finance.sina.com.cn/futures/api/openapi.php/StockOptionService.getRemainderDay";
let date_param = if trade_date.len() >= 6 {
format!("{}-{}", &trade_date[..4], &trade_date[4..])
} else {
trade_date.to_string()
};
let resp: SinaRemainderResponse = self
.get(url)
.query(&[
("exchange", exchange),
("cate", symbol),
("date", date_param.as_str()),
])
.send()
.await
.map_err(Error::from)?
.json()
.await
.map_err(Error::from)?;
let data = resp
.result
.and_then(|r| r.data)
.ok_or_else(|| Error::upstream("sina expire day: missing data"))?;
let remaining = data.remainder_days.parse::<i64>().unwrap_or(0);
if remaining < 0 {
let xd_symbol = format!("XD{symbol}");
let resp2: SinaRemainderResponse = self
.get(url)
.query(&[
("exchange", exchange),
("cate", xd_symbol.as_str()),
("date", date_param.as_str()),
])
.send()
.await
.map_err(Error::from)?
.json()
.await
.map_err(Error::from)?;
if let Some(data2) = resp2.result.and_then(|r| r.data) {
let remaining2 = data2.remainder_days.parse::<i64>().unwrap_or(0);
return Ok((data2.expire_day, remaining2));
}
}
Ok((data.expire_day, remaining))
}
pub async fn option_sse_codes_sina(
&self,
symbol: &str,
trade_date: &str,
underlying: &str,
) -> Result<Vec<OptionSseCodeEntry>> {
let suffix = &trade_date[trade_date.len().saturating_sub(4)..];
let list_prefix = if symbol.contains("涨") {
"OP_UP_"
} else {
"OP_DOWN_"
};
let url = format!("https://hq.sinajs.cn/list={list_prefix}{underlying}{suffix}");
let body = self
.get(&url)
.header("Referer", SINA_REFERER)
.send()
.await
.map_err(Error::from)?
.text()
.await
.map_err(Error::from)?;
let data = body
.split_once('"')
.and_then(|(_, r)| r.split_once('"'))
.map_or("", |(s, _)| s);
let codes: Vec<OptionSseCodeEntry> = data
.split(',')
.filter_map(|item| {
let trimmed = item.trim();
let code = trimmed.strip_prefix("CON_OP_").or(Some(trimmed))?;
if code.is_empty() || code == trimmed && !trimmed.starts_with("CON_OP_") {
return None;
}
Some(code.to_string())
})
.enumerate()
.map(|(i, code)| OptionSseCodeEntry { index: i + 1, code })
.collect();
if codes.is_empty() {
return Err(Error::not_found(format!(
"no SSE option codes for {symbol} {trade_date} {underlying}"
)));
}
Ok(codes)
}
pub async fn option_sse_spot_price_sina(
&self,
symbol: &str,
) -> Result<Vec<OptionFieldValuePair>> {
let url = format!("https://hq.sinajs.cn/list=CON_OP_{symbol}");
let body = self
.get(&url)
.header("Referer", SINA_REFERER)
.send()
.await
.map_err(Error::from)?
.text()
.await
.map_err(Error::from)?;
let data = body
.split_once('"')
.and_then(|(_, r)| r.split_once('"'))
.map_or("", |(s, _)| s);
let values: Vec<&str> = data.split(',').collect();
let field_list = [
"买量",
"买价",
"最新价",
"卖价",
"卖量",
"持仓量",
"涨幅",
"行权价",
"昨收价",
"开盘价",
"涨停价",
"跌停价",
"申卖价五",
"申卖量五",
"申卖价四",
"申卖量四",
"申卖价三",
"申卖量三",
"申卖价二",
"申卖量二",
"申卖价一",
"申卖量一",
"申买价一",
"申买量一",
"申买价二",
"申买量二",
"申买价三",
"申买量三",
"申买价四",
"申买量四",
"申买价五",
"申买量五",
"行情时间",
"主力合约标识",
"状态码",
"标的证券类型",
"标的股票",
"期权合约简称",
"振幅",
"最高价",
"最低价",
"成交量",
"成交额",
];
let pairs: Vec<OptionFieldValuePair> = field_list
.iter()
.zip(values.iter())
.map(|(field, value)| OptionFieldValuePair {
field: field.to_string(),
value: value.trim().to_string(),
})
.collect();
if pairs.is_empty() {
return Err(Error::not_found(format!(
"no SSE option spot data for {symbol}"
)));
}
Ok(pairs)
}
pub async fn option_sse_underlying_spot_price_sina(
&self,
symbol: &str,
) -> Result<Vec<OptionFieldValuePair>> {
let url = format!("https://hq.sinajs.cn/list={symbol}");
let body = self
.get(&url)
.header("Referer", "https://vip.stock.finance.sina.com.cn/")
.send()
.await
.map_err(Error::from)?
.text()
.await
.map_err(Error::from)?;
let data = body
.split_once('"')
.and_then(|(_, r)| r.split_once('"'))
.map_or("", |(s, _)| s);
let values: Vec<&str> = data.split(',').collect();
let field_list = [
"证券简称",
"今日开盘价",
"昨日收盘价",
"最近成交价",
"最高成交价",
"最低成交价",
"买入价",
"卖出价",
"成交数量",
"成交金额",
"买数量一",
"买价位一",
"买数量二",
"买价位二",
"买数量三",
"买价位三",
"买数量四",
"买价位四",
"买数量五",
"买价位五",
"卖数量一",
"卖价位一",
"卖数量二",
"卖价位二",
"卖数量三",
"卖价位三",
"卖数量四",
"卖价位四",
"卖数量五",
"卖价位五",
"行情日期",
"行情时间",
"停牌状态",
];
let pairs: Vec<OptionFieldValuePair> = field_list
.iter()
.zip(values.iter())
.map(|(field, value)| OptionFieldValuePair {
field: field.to_string(),
value: value.trim().to_string(),
})
.collect();
if pairs.is_empty() {
return Err(Error::not_found(format!(
"no underlying spot data for {symbol}"
)));
}
Ok(pairs)
}
pub async fn option_sse_greeks_sina(&self, symbol: &str) -> Result<Vec<OptionFieldValuePair>> {
let url = format!("https://hq.sinajs.cn/list=CON_SO_{symbol}");
let body = self
.get(&url)
.header("Referer", "https://vip.stock.finance.sina.com.cn/")
.send()
.await
.map_err(Error::from)?
.text()
.await
.map_err(Error::from)?;
let data = body
.split_once('"')
.and_then(|(_, r)| r.split_once('"'))
.map_or("", |(s, _)| s);
let values: Vec<&str> = data.split(',').collect();
let field_list = [
"期权合约简称",
"成交量",
"Delta",
"Gamma",
"Theta",
"Vega",
"隐含波动率",
"最高价",
"最低价",
"交易代码",
"行权价",
"最新价",
"理论价值",
];
let mut filtered_values = Vec::new();
if let Some(first) = values.first() {
filtered_values.push(*first);
}
for v in values.iter().skip(4) {
filtered_values.push(*v);
}
let pairs: Vec<OptionFieldValuePair> = field_list
.iter()
.zip(filtered_values.iter())
.map(|(field, value)| OptionFieldValuePair {
field: field.to_string(),
value: value.trim().to_string(),
})
.collect();
if pairs.is_empty() {
return Err(Error::not_found(format!("no greeks data for {symbol}")));
}
Ok(pairs)
}
pub async fn option_sse_minute_sina(&self, symbol: &str) -> Result<Vec<OptionSseMinuteRow>> {
let url = "https://stock.finance.sina.com.cn/futures/api/openapi.php/StockOptionDaylineService.getOptionMinline";
let con_symbol = format!("CON_OP_{symbol}");
let resp: SinaMinuteResponse = self
.get(url)
.query(&[("symbol", con_symbol.as_str())])
.header(
"Referer",
"https://stock.finance.sina.com.cn/option/quotes.html",
)
.send()
.await
.map_err(Error::from)?
.json()
.await
.map_err(Error::from)?;
let data = resp
.result
.and_then(|r| r.data)
.ok_or_else(|| Error::upstream("sina minute: missing data"))?;
let mut rows = Vec::with_capacity(data.len());
for item in &data {
if let Some(arr) = item.as_array() {
if arr.len() < 6 {
continue;
}
let date = arr[5].as_str().unwrap_or("").to_string();
let time = arr[0].as_str().unwrap_or("").to_string();
let price = json_to_f64(&arr[1]);
let volume = json_to_f64(&arr[2]);
let open_interest = json_to_f64(&arr[3]);
let average_price = json_to_f64(&arr[4]);
rows.push(OptionSseMinuteRow {
date,
time,
price,
volume,
open_interest,
average_price,
});
}
}
if rows.is_empty() {
return Err(Error::not_found(format!("no minute data for {symbol}")));
}
for i in 1..rows.len() {
if rows[i].date.is_empty() {
let prev_date = rows[i - 1].date.clone();
rows[i].date = prev_date;
}
}
Ok(rows)
}
pub async fn option_sse_daily_sina(&self, symbol: &str) -> Result<Vec<OptionSseDailyRow>> {
let url = "https://stock.finance.sina.com.cn/futures/api/jsonp_v2.php//StockOptionDaylineService.getSymbolInfo";
let con_symbol = format!("CON_OP_{symbol}");
let body = self
.get(url)
.query(&[("symbol", con_symbol.as_str())])
.header(
"Referer",
"https://stock.finance.sina.com.cn/option/quotes.html",
)
.send()
.await
.map_err(Error::from)?
.text()
.await
.map_err(Error::from)?;
let json_start = body.find('[').unwrap_or(0);
let json_end = body.rfind(']').map_or(body.len(), |i| i + 1);
if json_start >= json_end {
return Err(Error::decode("sina daily: no JSON array found"));
}
let json_str = &body[json_start..json_end];
let raw: Vec<Vec<serde_json::Value>> = serde_json::from_str(json_str)
.map_err(|e| Error::decode(format!("sina daily json: {e}")))?;
let rows: Vec<OptionSseDailyRow> = raw
.iter()
.filter(|item| item.len() >= 6)
.map(|item| OptionSseDailyRow {
date: item[0].as_str().unwrap_or("").to_string(),
open: json_to_f64(&item[1]),
high: json_to_f64(&item[2]),
low: json_to_f64(&item[3]),
close: json_to_f64(&item[4]),
volume: json_to_f64(&item[5]),
})
.collect();
if rows.is_empty() {
return Err(Error::not_found(format!("no daily data for {symbol}")));
}
Ok(rows)
}
pub async fn option_finance_minute_sina(
&self,
symbol: &str,
) -> Result<Vec<OptionFinanceMinuteRow>> {
let url = "https://stock.finance.sina.com.cn/futures/api/openapi.php/StockOptionDaylineService.getFiveDayLine";
let con_symbol = format!("CON_OP_{symbol}");
let body = self
.get(url)
.query(&[("symbol", con_symbol.as_str())])
.header(
"Referer",
"https://stock.finance.sina.com.cn/option/quotes.html",
)
.send()
.await
.map_err(Error::from)?
.text()
.await
.map_err(Error::from)?;
let resp: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| Error::decode(format!("sina finance minute json: {e}")))?;
let data = resp
.get("result")
.and_then(|r| r.get("data"))
.and_then(|d| d.as_array())
.cloned()
.unwrap_or_default();
let mut all_rows = Vec::new();
for day_data in &data {
if let Some(arr) = day_data.as_array() {
let mut last_date = String::new();
for item in arr {
if let Some(row) = item.as_array() {
if row.len() < 6 {
continue;
}
let time = row[0].as_str().unwrap_or("").to_string();
let price = json_to_f64(&row[1]);
let volume = json_to_f64(&row[2]);
let average_price = json_to_f64(&row[4]);
let date_raw = row[5].as_str().unwrap_or("").to_string();
if !date_raw.is_empty() {
last_date = date_raw;
}
all_rows.push(OptionFinanceMinuteRow {
date: last_date.clone(),
time,
price,
average_price,
volume,
});
}
}
}
}
if all_rows.is_empty() {
return Err(Error::not_found(format!(
"no finance minute data for {symbol}"
)));
}
Ok(all_rows)
}
}
fn json_to_f64(v: &serde_json::Value) -> f64 {
match v {
serde_json::Value::Number(n) => n.as_f64().unwrap_or(0.0),
serde_json::Value::String(s) => parse_f64_safe(s),
_ => 0.0,
}
}