b2970e4c-45a2-423e-b84d-257.../Sources/engine/RefreshScheduler.swift

88 lines
3.5 KiB
Swift
Raw Normal View History

//
// RefreshScheduler.swift
// plugin
//
// Drives the refresh cadence based on what's tradeable right now.
// Different things have different refresh budgets:
//
// Tradeable now Cadence
//
// A-share funds (intraday) 60 s (estimate API is ~1 min granularity)
// A-share funds (closed) 30 min (just to pick up newly published NAV)
// Foreign gold (24/5) 30 s (Sina updates GC/XAU continuously)
// Domestic gold 30 s while SHFE open
//
// Trade hours (Asia/Shanghai):
// A-share: 09:3011:30, 13:0015:00, MonFri
// SHFE day: 09:0010:15, 10:3011:30, 13:3015:00
// SHFE night: 21:0002:30
// COMEX gold: 06:0005:00 next day (essentially 23h, 5 days/week)
// LBMA London: rolls with COMEX in practice
//
import Foundation
@MainActor
final class RefreshScheduler {
enum Tier {
/// Funds intraday cadence.
case fundsActive
/// Funds idle cadence only listening for end-of-day NAV publish.
case fundsIdle
/// Realtime gold tier (always on, foreign markets virtually 24/7).
case gold
}
/// Returns refresh interval (seconds) for a tier at the given moment.
func interval(for tier: Tier, at now: Date = Date()) -> TimeInterval {
switch tier {
case .fundsActive:
return isAShareTradeHour(now) ? 60 : 30 * 60
case .fundsIdle:
return 30 * 60
case .gold:
// Always 30s foreign gold trades virtually 24/7. SHFE has
// closed windows but we don't bother backing off; the request
// is cheap and Sina caches the latest tick anyway.
return 30
}
}
/// True iff `now` falls inside an A-share trading session
/// (09:3011:30, 13:0015:00 China time, MonFri, no holiday awareness yet).
func isAShareTradeHour(_ now: Date = Date()) -> Bool {
var cal = Calendar(identifier: .gregorian)
cal.timeZone = TimeZone(identifier: "Asia/Shanghai")!
let weekday = cal.component(.weekday, from: now) // 1=Sun 7=Sat
guard (2...6).contains(weekday) else { return false }
let hm = cal.component(.hour, from: now) * 60 + cal.component(.minute, from: now)
// 09:3011:30
if hm >= 9 * 60 + 30 && hm < 11 * 60 + 30 { return true }
// 13:0015:00
if hm >= 13 * 60 && hm < 15 * 60 { return true }
return false
}
/// True iff SHFE gold is in a trading window (loose no holiday).
func isSHFETradeHour(_ now: Date = Date()) -> Bool {
var cal = Calendar(identifier: .gregorian)
cal.timeZone = TimeZone(identifier: "Asia/Shanghai")!
let weekday = cal.component(.weekday, from: now)
// Day session also runs Mon-Fri; night session technically can
// span into Sat early morning but we keep it simple.
guard (2...6).contains(weekday) else { return false }
let hm = cal.component(.hour, from: now) * 60 + cal.component(.minute, from: now)
// 09:00-10:15
if hm >= 9 * 60 && hm < 10 * 60 + 15 { return true }
// 10:30-11:30
if hm >= 10 * 60 + 30 && hm < 11 * 60 + 30 { return true }
// 13:30-15:00
if hm >= 13 * 60 + 30 && hm < 15 * 60 { return true }
// 21:00-23:59 + 00:00-02:30
if hm >= 21 * 60 { return true }
if hm < 2 * 60 + 30 { return true }
return false
}
}