Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/chrome/src/ports/chromeDataPort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ export class ChromeDataPort implements DataPort {
return sendMessage({ type: 'FETCH_INDEX_HISTORY', code, range })
}

async fetchFundIntraday(fundKey: string): Promise<IntradayPoint[]> {
async fetchFundIntraday(code: string, fundKey?: string): Promise<IntradayPoint[]> {
const data = await sendMessage<FundIntradayPayload>({
type: 'FETCH_FUND_INTRADAY',
code: fundKey,
code,
fundKey,
})
return data.points
Expand Down
19 changes: 15 additions & 4 deletions apps/tauri/src-tauri/src/calc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,10 @@ pub fn calc_holdings(
}
let overview_has_cost = overview_cost_row > 0.0;

let using_estimate = q.percent_source.as_deref() == Some("estimate")
|| (q.percent_source.as_deref() != Some("confirmed")
&& latest_estimate_nav(q).is_some());
let using_estimate = !q.is_market_quote.unwrap_or(false)
&& (q.percent_source.as_deref() == Some("estimate")
|| (q.percent_source.as_deref() != Some("confirmed")
&& latest_estimate_nav(q).is_some()));

let mut pnl: Option<f64> = None;
if shares > 0.0 && prev_nav.is_some() && curr_nav.is_some() {
Expand Down Expand Up @@ -218,7 +219,13 @@ pub fn calc_holdings(
persist_patches.push(patch);
}

let confirmed_updated = should_show_confirmed_updated_badge(
// 「今日已更新」只描述场外基金的确认净值披露;场内 ETF/股票的
// 成交价本来就是实时行情,不应显示这个基金净值状态。
let is_off_exchange_fund = crate::market::infer_asset_kind(
&raw.code,
raw.asset_kind.as_deref(),
) == "fund";
let confirmed_updated = is_off_exchange_fund && should_show_confirmed_updated_badge(
q.percent_source.as_deref(),
Some(if nav_day.is_empty() {
q.net_value_date.as_str()
Expand All @@ -239,6 +246,9 @@ pub fn calc_holdings(
}
row_fund.sectors = sectors.clone();
row_fund.shares = Some(shares);
if q.is_market_quote.unwrap_or(false) && row_fund.asset_kind.is_none() {
row_fund.asset_kind = Some(crate::market::infer_asset_kind(&row_fund.code, None).to_string());
}

rows.push(FundQuoteRow {
fund: row_fund,
Expand Down Expand Up @@ -350,4 +360,5 @@ static EMPTY_QUOTE: FundQuote = FundQuote {
error: None,
use_calc: None,
is_qdii: None,
is_market_quote: None,
};
30 changes: 20 additions & 10 deletions apps/tauri/src-tauri/src/calendar.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
//! 交易日历 —— 对应 `packages/core/src/tradingCalendar.ts` 1:1 迁移。
//! A 股简易交易日:仅跳周末(不含法定节假日,与 TS 版一致)。
//! A 股交易日历。除周末外,内置交易所公布的 2026 年休市日,避免将调休工作日误判为可交易。

use chrono::{DateTime, Datelike, Local, NaiveDate, Timelike, Weekday};
use chrono::{DateTime, Datelike, Local, NaiveDate, TimeZone, Timelike, Weekday};

fn now_local() -> DateTime<Local> {
Local::now()
}

/// 上交所 2026 年休市安排(不含天然周末)。年度数据更新前,未知年份仅按周末判断。
const A_SHARE_CLOSED_2026: &[&str] = &[
"2026-01-01", "2026-01-02", "2026-02-16", "2026-02-17", "2026-02-18", "2026-02-19", "2026-02-20", "2026-02-23",
"2026-04-06", "2026-05-01", "2026-05-04", "2026-05-05", "2026-06-19",
"2026-09-25", "2026-10-01", "2026-10-02", "2026-10-05", "2026-10-06", "2026-10-07",
];

/// A 股是否开市。调休工作日不交易;法定休市日优先于星期判断。
pub fn is_a_share_trading_day(now: &DateTime<Local>) -> bool {
if matches!(now.weekday(), Weekday::Sat | Weekday::Sun) { return false; }
!A_SHARE_CLOSED_2026.contains(&today_date_str(now).as_str())
}

pub fn today_date_str(d: &DateTime<Local>) -> String {
format!("{:04}-{:02}-{:02}", d.year(), d.month(), d.day())
}
Expand Down Expand Up @@ -61,8 +74,8 @@ pub fn next_trading_day(date_str: &str, now: &DateTime<Local>) -> String {
};
loop {
d = d.succ_opt().unwrap_or(d);
let wd = d.weekday();
if wd != Weekday::Sat && wd != Weekday::Sun {
let next = d.and_hms_opt(12, 0, 0).and_then(|v| Local.from_local_datetime(&v).single());
if next.is_some_and(|v| is_a_share_trading_day(&v)) {
break;
}
}
Expand Down Expand Up @@ -123,8 +136,7 @@ fn hms_to_minutes(d: &DateTime<Local>) -> u32 {

/// A 股交易日(周一到周五)盘中时段:09:15 - 15:30
pub fn is_a_share_trading_time(now: &DateTime<Local>) -> bool {
let day = now.weekday();
if day == Weekday::Sat || day == Weekday::Sun {
if !is_a_share_trading_day(now) {
return false;
}
let m = hms_to_minutes(now);
Expand All @@ -135,8 +147,7 @@ pub fn is_a_share_trading_time(now: &DateTime<Local>) -> bool {
/// 避免 09:00-09:15 定时器空转——开盘前拉到的是静态数据,无副作用;
/// 盘中估值 + 空窗自算 + 晚间确认)
pub fn should_refresh_fund(now: &DateTime<Local>) -> bool {
let day = now.weekday();
if day == Weekday::Sat || day == Weekday::Sun {
if !is_a_share_trading_day(now) {
return false;
}
let m = hms_to_minutes(now);
Expand All @@ -146,8 +157,7 @@ pub fn should_refresh_fund(now: &DateTime<Local>) -> bool {
/// A 股指数 / 大盘:交易日 09:00-15:30(09:00 起覆盖黄金 AU9999 日盘 09:00 开盘,
/// 避免 09:00-09:15 空窗)
pub fn should_refresh_a_share_market(now: &DateTime<Local>) -> bool {
let day = now.weekday();
if day == Weekday::Sat || day == Weekday::Sun {
if !is_a_share_trading_day(now) {
return false;
}
let m = hms_to_minutes(now);
Expand Down
3 changes: 3 additions & 0 deletions apps/tauri/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ pub async fn fetch_fund_intraday(
req: FundIntradayRequest,
) -> Result<FundIntradayPayload, String> {
let _ = state;
if crate::market::infer_asset_kind(&req.code, None) != "fund" {
return crate::market::fetch_market_intraday(&req.code).await;
}
crate::providers::fund123::fetch_intraday_for_dialog(
&req.code,
req.fund_key.as_deref(),
Expand Down
17 changes: 17 additions & 0 deletions apps/tauri/src-tauri/src/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,22 @@ struct VerifyResult {
pub async fn resolve_fund(
payload: &crate::model::ResolveFundRequest,
) -> Result<ResolveFundPayload, String> {
let asset_kind = crate::market::infer_asset_kind(&payload.code, payload.asset_kind.as_deref());
if asset_kind != "fund" {
let input = crate::providers::FundQuoteInput {
code: payload.code.clone(), asset_kind: Some(asset_kind.to_string()), fund_key: None,
name: payload.name.clone(), sectors: payload.sectors.clone().unwrap_or_default(),
};
let quote = crate::market::fetch_market_quotes(&[input]).await.into_iter().next()
.ok_or_else(|| "未获取到场内证券行情,请核对代码".to_string())?;
let price = quote.net_value.ok_or_else(|| format!("未找到证券代码 {} 的场内行情,请核对代码", payload.code))?;
return Ok(ResolveFundPayload {
code: quote.code, name: quote.name.clone(), fund_key: String::new(), sectors: vec![],
asset_kind: Some(asset_kind.to_string()), net_value: Some(price), prev_net_value: quote.prev_net_value,
prev_net_value_date: None, net_value_date: Some(chrono::Local::now().format("%Y-%m-%d").to_string()),
confirmed_session: Some(true), official_name: Some(quote.name), name_mismatch: None, code_corrected: None,
});
}
let mut code = payload.code.trim().to_string();
let mut meta: Option<SearchMeta> = None;
match search_fund(&code).await {
Expand Down Expand Up @@ -431,6 +447,7 @@ pub async fn resolve_fund(
.map(|m| m.fund_key.clone())
.unwrap_or_default(),
sectors,
asset_kind: Some("fund".to_string()),
net_value,
prev_net_value,
prev_net_value_date: if prev_net_value_date.is_empty() {
Expand Down
Loading