implement a server cache sync
This commit is contained in:
parent
2f49a99ca4
commit
ab76693367
3 changed files with 77 additions and 1 deletions
4
src/cache/store.zig
vendored
4
src/cache/store.zig
vendored
|
|
@ -374,7 +374,9 @@ pub const Store = struct {
|
|||
};
|
||||
}
|
||||
|
||||
fn writeRaw(self: *Store, symbol: []const u8, data_type: DataType, data: []const u8) !void {
|
||||
/// Write raw bytes to a cache file. Used by server sync to write
|
||||
/// pre-serialized SRF data directly to the cache.
|
||||
pub fn writeRaw(self: *Store, symbol: []const u8, data_type: DataType, data: []const u8) !void {
|
||||
try self.ensureSymbolDir(symbol);
|
||||
const path = try self.symbolPath(symbol, data_type.fileName());
|
||||
defer self.allocator.free(path);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ pub const Config = struct {
|
|||
finnhub_key: ?[]const u8 = null,
|
||||
alphavantage_key: ?[]const u8 = null,
|
||||
openfigi_key: ?[]const u8 = null,
|
||||
/// URL of a zfin-server instance for lazy cache sync (e.g. "https://zfin.lerch.org")
|
||||
server_url: ?[]const u8 = null,
|
||||
cache_dir: []const u8,
|
||||
allocator: ?std.mem.Allocator = null,
|
||||
/// Raw .env file contents (keys/values in env_map point into this).
|
||||
|
|
@ -32,6 +34,7 @@ pub const Config = struct {
|
|||
self.finnhub_key = self.resolve("FINNHUB_API_KEY");
|
||||
self.alphavantage_key = self.resolve("ALPHAVANTAGE_API_KEY");
|
||||
self.openfigi_key = self.resolve("OPENFIGI_API_KEY");
|
||||
self.server_url = self.resolve("ZFIN_SERVER");
|
||||
|
||||
const env_cache = self.resolve("ZFIN_CACHE_DIR");
|
||||
self.cache_dir = env_cache orelse blk: {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const alphavantage = @import("providers/alphavantage.zig");
|
|||
const OpenFigi = @import("providers/openfigi.zig");
|
||||
const fmt = @import("format.zig");
|
||||
const performance = @import("analytics/performance.zig");
|
||||
const http = @import("net/http.zig");
|
||||
|
||||
pub const DataError = error{
|
||||
NoApiKey,
|
||||
|
|
@ -164,6 +165,12 @@ pub const DataService = struct {
|
|||
if (s.read(T, symbol, postProcess, .fresh_only)) |cached|
|
||||
return .{ .data = cached.data, .source = .cached, .timestamp = cached.timestamp };
|
||||
|
||||
// Try server sync before hitting providers
|
||||
if (self.syncFromServer(symbol, data_type)) {
|
||||
if (s.read(T, symbol, postProcess, .fresh_only)) |cached|
|
||||
return .{ .data = cached.data, .source = .cached, .timestamp = cached.timestamp };
|
||||
}
|
||||
|
||||
const fetched = self.fetchFromProvider(T, symbol) catch |err| {
|
||||
if (err == error.RateLimited) {
|
||||
// Wait and retry once
|
||||
|
|
@ -232,6 +239,15 @@ pub const DataService = struct {
|
|||
return .{ .data = r.data, .source = .cached, .timestamp = mr.created };
|
||||
}
|
||||
|
||||
// Stale — try server sync before incremental fetch
|
||||
if (self.syncCandlesFromServer(symbol)) {
|
||||
if (s.isCandleMetaFresh(symbol)) {
|
||||
if (s.read(Candle, symbol, null, .any)) |r|
|
||||
return .{ .data = r.data, .source = .cached, .timestamp = std.time.timestamp() };
|
||||
}
|
||||
// Server data also stale — fall through to incremental fetch
|
||||
}
|
||||
|
||||
// Stale — try incremental update using last_date from meta
|
||||
const fetch_from = m.last_date.addDays(1);
|
||||
|
||||
|
|
@ -283,6 +299,15 @@ pub const DataService = struct {
|
|||
}
|
||||
}
|
||||
|
||||
// No usable cache — try server sync first
|
||||
if (self.syncCandlesFromServer(symbol)) {
|
||||
if (s.isCandleMetaFresh(symbol)) {
|
||||
if (s.read(Candle, symbol, null, .any)) |r|
|
||||
return .{ .data = r.data, .source = .cached, .timestamp = std.time.timestamp() };
|
||||
}
|
||||
// Server data also stale — fall through to full fetch
|
||||
}
|
||||
|
||||
// No usable cache — full fetch (~10 years, plus buffer for leap years)
|
||||
var td = try self.getProvider(TwelveData);
|
||||
const from = today.addDays(-3700);
|
||||
|
|
@ -347,6 +372,12 @@ pub const DataService = struct {
|
|||
self.allocator.free(cached.data);
|
||||
}
|
||||
|
||||
// Try server sync before hitting Finnhub
|
||||
if (self.syncFromServer(symbol, .earnings)) {
|
||||
if (s.read(EarningsEvent, symbol, earningsPostProcess, .fresh_only)) |cached|
|
||||
return .{ .data = cached.data, .source = .cached, .timestamp = cached.timestamp };
|
||||
}
|
||||
|
||||
var fh = try self.getProvider(Finnhub);
|
||||
const from = today.subtractYears(5);
|
||||
const to = today.addDays(365);
|
||||
|
|
@ -721,6 +752,46 @@ pub const DataService = struct {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Server sync ──────────────────────────────────────────────
|
||||
|
||||
/// Try to sync a cache file from the configured zfin-server.
|
||||
/// Returns true if the file was successfully synced, false on any error.
|
||||
/// Silently returns false if no server is configured.
|
||||
fn syncFromServer(self: *DataService, symbol: []const u8, data_type: cache.DataType) bool {
|
||||
const server_url = self.config.server_url orelse return false;
|
||||
const endpoint = switch (data_type) {
|
||||
.candles_daily => "/candles",
|
||||
.candles_meta => "/candles_meta",
|
||||
.dividends => "/dividends",
|
||||
.earnings => "/earnings",
|
||||
.options => "/options",
|
||||
.splits => return false, // not served
|
||||
.etf_profile => return false, // not served
|
||||
.meta => return false,
|
||||
};
|
||||
|
||||
const full_url = std.fmt.allocPrint(self.allocator, "{s}/{s}{s}", .{ server_url, symbol, endpoint }) catch return false;
|
||||
defer self.allocator.free(full_url);
|
||||
|
||||
var client = http.Client.init(self.allocator);
|
||||
defer client.deinit();
|
||||
|
||||
var response = client.get(full_url) catch return false;
|
||||
defer response.deinit();
|
||||
|
||||
// Write to local cache
|
||||
var s = self.store();
|
||||
s.writeRaw(symbol, data_type, response.body) catch return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Sync candle data (both daily and meta) from the server.
|
||||
fn syncCandlesFromServer(self: *DataService, symbol: []const u8) bool {
|
||||
const daily = self.syncFromServer(symbol, .candles_daily);
|
||||
const meta = self.syncFromServer(symbol, .candles_meta);
|
||||
return daily and meta;
|
||||
}
|
||||
|
||||
/// Mutual funds use 5-letter tickers ending in X (e.g. FDSCX, VSTCX, FAGIX).
|
||||
/// These don't have quarterly earnings on Finnhub.
|
||||
fn isMutualFund(symbol: []const u8) bool {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue