special case for last close

This commit is contained in:
Emil Lerch 2026-02-27 14:08:26 -08:00
parent 2b62827bdb
commit f7505936d2
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 66 additions and 24 deletions

36
src/cache/store.zig vendored
View file

@ -215,6 +215,42 @@ pub const Store = struct {
std.fs.cwd().deleteFile(path) catch {};
}
/// Read the close price from the last candle record without parsing the entire file.
/// Seeks to the end, reads the last ~256 bytes, and extracts `close:num:X`.
/// Returns null if the file doesn't exist or has no candle data.
pub fn readLastClose(self: *Store, symbol: []const u8) ?f64 {
const path = self.symbolPath(symbol, DataType.candles_daily.fileName()) catch return null;
defer self.allocator.free(path);
const file = std.fs.cwd().openFile(path, .{}) catch return null;
defer file.close();
const stat = file.stat() catch return null;
const file_size = stat.size;
if (file_size < 20) return null; // too small to have candle data
// Read the last 256 bytes (one candle line is ~100 bytes, gives margin)
const read_size: u64 = @min(256, file_size);
file.seekTo(file_size - read_size) catch return null;
var buf: [256]u8 = undefined;
const n = file.readAll(buf[0..@intCast(read_size)]) catch return null;
const chunk = buf[0..n];
// Find the last complete line (skip trailing newline, then find the previous newline)
const trimmed = std.mem.trimRight(u8, chunk, "\n");
if (trimmed.len == 0) return null;
const last_nl = std.mem.lastIndexOfScalar(u8, trimmed, '\n');
const last_line = if (last_nl) |pos| trimmed[pos + 1 ..] else trimmed;
// Extract close:num:VALUE from the line
const marker = "close:num:";
const close_start = std.mem.indexOf(u8, last_line, marker) orelse return null;
const val_start = close_start + marker.len;
const val_end = std.mem.indexOfScalar(u8, last_line[val_start..], ',') orelse (last_line.len - val_start);
return std.fmt.parseFloat(f64, last_line[val_start .. val_start + val_end]) catch null;
}
/// Clear all cached data.
pub fn clearAll(self: *Store) !void {
std.fs.cwd().deleteTree(self.cache_dir) catch {};

View file

@ -76,17 +76,14 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
const is_fresh = svc.isCandleCacheFresh(sym);
if (is_fresh and !force_refresh) {
// Load from cache (no network)
if (svc.getCachedCandles(sym)) |cs| {
defer allocator.free(cs);
if (cs.len > 0) {
try prices.put(sym, cs[cs.len - 1].close);
}
// Cached (including negative cache entries with 0 candles)
cached_count += 1;
try cli.stderrProgress(sym, " (cached)", loaded_count, all_syms_count, color);
continue;
// Read only the last close price from cache (no full deserialization)
if (svc.getCachedLastClose(sym)) |close| {
try prices.put(sym, close);
}
// Cached (including negative cache entries where getCachedLastClose returns null)
cached_count += 1;
try cli.stderrProgress(sym, " (cached)", loaded_count, all_syms_count, color);
continue;
}
// Need to fetch from API
@ -242,14 +239,15 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
try out.print(" Lots: {d} open, {d} closed Positions: {d} symbols\n", .{ open_lots, closed_lots, positions.len });
try cli.reset(out, color);
// Historical portfolio value snapshots
// Build candle map once for historical snapshots and risk metrics.
// This avoids parsing the full candle history multiple times.
var candle_map = std.StringHashMap([]const zfin.Candle).init(allocator);
defer {
var it = candle_map.valueIterator();
while (it.next()) |v| allocator.free(v.*);
candle_map.deinit();
}
{
var candle_map = std.StringHashMap([]const zfin.Candle).init(allocator);
defer {
var it = candle_map.valueIterator();
while (it.next()) |v| allocator.free(v.*);
candle_map.deinit();
}
const stock_syms = try portfolio.stockSymbols(allocator);
defer allocator.free(stock_syms);
for (stock_syms) |sym| {
@ -257,6 +255,10 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
try candle_map.put(sym, cs);
}
}
}
// Historical portfolio value snapshots
{
if (candle_map.count() > 0) {
const snapshots = zfin.risk.computeHistoricalSnapshots(
fmt.todayDate(),
@ -676,6 +678,7 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
// Helper to render a watch symbol
const renderWatch = struct {
fn f(o: *std.Io.Writer, c: bool, s: *zfin.DataService, a2: std.mem.Allocator, sym: []const u8, any: *bool) !void {
_ = a2;
if (!any.*) {
try o.print("\n", .{});
try cli.setBold(o, c);
@ -685,11 +688,8 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
}
var price_str2: [16]u8 = undefined;
var ps2: []const u8 = "--";
if (s.getCachedCandles(sym)) |candles2| {
defer a2.free(candles2);
if (candles2.len > 0) {
ps2 = fmt.fmtMoney2(&price_str2, candles2[candles2.len - 1].close);
}
if (s.getCachedLastClose(sym)) |close| {
ps2 = fmt.fmtMoney2(&price_str2, close);
}
try o.print(" " ++ fmt.sym_col_spec ++ " {s:>10}\n", .{ sym, ps2 });
}
@ -733,8 +733,7 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
var any_risk = false;
for (summary.allocations) |a| {
if (svc.getCachedCandles(a.symbol)) |candles| {
defer allocator.free(candles);
if (candle_map.get(a.symbol)) |candles| {
if (zfin.risk.computeRisk(candles)) |metrics| {
if (!any_risk) {
try out.print("\n", .{});

View file

@ -394,6 +394,13 @@ pub const DataService = struct {
return s.isFresh(symbol, .candles_daily) catch false;
}
/// Read only the latest close price from cached candles (no full deserialization).
/// Returns null if no cached data exists.
pub fn getCachedLastClose(self: *DataService, symbol: []const u8) ?f64 {
var s = self.store();
return s.readLastClose(symbol);
}
/// Estimate wait time (in seconds) before the next TwelveData API call can proceed.
/// Returns 0 if a request can be made immediately. Returns null if no API key.
pub fn estimateWaitSeconds(self: *DataService) ?u64 {