upgrade to zig 0.16.0 and latest zfin lib, add /splits
This commit is contained in:
parent
675d8839a2
commit
536a427a08
5 changed files with 109 additions and 72 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,3 +2,4 @@
|
||||||
.zig-cache
|
.zig-cache
|
||||||
zig-out/
|
zig-out/
|
||||||
*.srf
|
*.srf
|
||||||
|
zig-pkg/
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[tools]
|
[tools]
|
||||||
prek = "0.3.1"
|
prek = "0.3.1"
|
||||||
zig = "0.15.2"
|
zig = "0.16.0"
|
||||||
zls = "0.15.1"
|
zls = "0.16.0"
|
||||||
"ubi:DonIsaac/zlint" = "0.7.9"
|
"ubi:DonIsaac/zlint" = "0.7.9"
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,21 @@ pub const Options = struct {
|
||||||
dirty_flag: []const u8 = "*",
|
dirty_flag: []const u8 = "*",
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Get git version information by reading .git files directly
|
/// Get git version information by reading .git files directly.
|
||||||
|
/// Runs at build-config time, so uses `b.graph.io` for I/O.
|
||||||
pub fn getVersion(b: *Build, options: Options) []const u8 {
|
pub fn getVersion(b: *Build, options: Options) []const u8 {
|
||||||
const allocator = b.allocator;
|
const allocator = b.allocator;
|
||||||
|
const io = b.graph.io;
|
||||||
|
|
||||||
// Find build root by looking for build.zig
|
// Find build root by looking for build.zig
|
||||||
const build_root = findBuildRoot(allocator) catch return "unknown";
|
const build_root = findBuildRoot(allocator, io) catch return "unknown";
|
||||||
defer allocator.free(build_root);
|
defer allocator.free(build_root);
|
||||||
|
|
||||||
// Read .git/HEAD relative to build root
|
// Read .git/HEAD relative to build root
|
||||||
const head_path = std.fmt.allocPrint(allocator, "{s}/.git/HEAD", .{build_root}) catch return "unknown";
|
const head_path = std.fmt.allocPrint(allocator, "{s}/.git/HEAD", .{build_root}) catch return "unknown";
|
||||||
defer allocator.free(head_path);
|
defer allocator.free(head_path);
|
||||||
|
|
||||||
const head_data = std.fs.cwd().readFileAlloc(allocator, head_path, 1024) catch {
|
const head_data = std.Io.Dir.cwd().readFileAlloc(io, head_path, allocator, .limited(1024)) catch {
|
||||||
return "not under version control";
|
return "not under version control";
|
||||||
};
|
};
|
||||||
defer allocator.free(head_data);
|
defer allocator.free(head_data);
|
||||||
|
|
@ -29,16 +31,12 @@ pub fn getVersion(b: *Build, options: Options) []const u8 {
|
||||||
|
|
||||||
// Parse HEAD - either "ref: refs/heads/branch" or direct hash
|
// Parse HEAD - either "ref: refs/heads/branch" or direct hash
|
||||||
const hash_owned = if (std.mem.startsWith(u8, head_trimmed, "ref: ")) blk: {
|
const hash_owned = if (std.mem.startsWith(u8, head_trimmed, "ref: ")) blk: {
|
||||||
const ref_path_rel = std.mem.trimLeft(u8, head_trimmed[5..], &std.ascii.whitespace);
|
const ref_path_rel = std.mem.trimStart(u8, head_trimmed[5..], &std.ascii.whitespace);
|
||||||
const ref_file = std.fmt.allocPrint(allocator, "{s}/.git/{s}", .{ build_root, ref_path_rel }) catch return "unknown";
|
const ref_file = std.fmt.allocPrint(allocator, "{s}/.git/{s}", .{ build_root, ref_path_rel }) catch return "unknown";
|
||||||
defer allocator.free(ref_file);
|
defer allocator.free(ref_file);
|
||||||
|
|
||||||
const ref_fd = std.fs.openFileAbsolute(ref_file, .{}) catch return "unknown";
|
const ref_data = std.Io.Dir.cwd().readFileAlloc(io, ref_file, allocator, .limited(1024)) catch return "unknown";
|
||||||
defer ref_fd.close();
|
defer allocator.free(ref_data);
|
||||||
|
|
||||||
var ref_buf: [1024]u8 = undefined;
|
|
||||||
const bytes_read = ref_fd.readAll(&ref_buf) catch return "unknown";
|
|
||||||
const ref_data = ref_buf[0..bytes_read];
|
|
||||||
|
|
||||||
const ref_trimmed = std.mem.trim(u8, ref_data, &std.ascii.whitespace);
|
const ref_trimmed = std.mem.trim(u8, ref_data, &std.ascii.whitespace);
|
||||||
break :blk allocator.dupe(u8, ref_trimmed) catch return "unknown";
|
break :blk allocator.dupe(u8, ref_trimmed) catch return "unknown";
|
||||||
|
|
@ -53,7 +51,7 @@ pub fn getVersion(b: *Build, options: Options) []const u8 {
|
||||||
|
|
||||||
// Check if dirty using simple heuristic:
|
// Check if dirty using simple heuristic:
|
||||||
// If any .zig files are newer than .git/index, mark as dirty
|
// If any .zig files are newer than .git/index, mark as dirty
|
||||||
const is_dirty = isDirty(allocator, build_root) catch return "unknown";
|
const is_dirty = isDirty(allocator, io, build_root) catch return "unknown";
|
||||||
|
|
||||||
if (is_dirty) {
|
if (is_dirty) {
|
||||||
return std.fmt.allocPrint(allocator, "{s}{s}", .{ short_hash, options.dirty_flag }) catch return "unknown";
|
return std.fmt.allocPrint(allocator, "{s}{s}", .{ short_hash, options.dirty_flag }) catch return "unknown";
|
||||||
|
|
@ -62,17 +60,17 @@ pub fn getVersion(b: *Build, options: Options) []const u8 {
|
||||||
return allocator.dupe(u8, short_hash) catch return "unknown";
|
return allocator.dupe(u8, short_hash) catch return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
fn findBuildRoot(allocator: std.mem.Allocator) ![]const u8 {
|
fn findBuildRoot(allocator: std.mem.Allocator, io: std.Io) ![]const u8 {
|
||||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
const start_cwd = try std.Io.Dir.cwd().realPathFileAlloc(io, ".", allocator);
|
||||||
const start_cwd = try std.fs.cwd().realpath(".", &buf);
|
defer allocator.free(start_cwd);
|
||||||
var cwd: []const u8 = start_cwd;
|
var cwd: []const u8 = start_cwd;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
// Check if build.zig exists in current directory
|
// Check if build.zig exists in current directory
|
||||||
var dir = std.fs.openDirAbsolute(cwd, .{}) catch break;
|
var dir = std.Io.Dir.cwd().openDir(io, cwd, .{}) catch break;
|
||||||
defer dir.close();
|
defer dir.close(io);
|
||||||
|
|
||||||
dir.access("build.zig", .{}) catch {
|
dir.access(io, "build.zig", .{}) catch {
|
||||||
// build.zig not found, try parent
|
// build.zig not found, try parent
|
||||||
const parent = std.fs.path.dirname(cwd) orelse break;
|
const parent = std.fs.path.dirname(cwd) orelse break;
|
||||||
if (std.mem.eql(u8, parent, cwd)) break; // Reached root
|
if (std.mem.eql(u8, parent, cwd)) break; // Reached root
|
||||||
|
|
@ -86,29 +84,29 @@ fn findBuildRoot(allocator: std.mem.Allocator) ![]const u8 {
|
||||||
return error.BuildRootNotFound;
|
return error.BuildRootNotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isDirty(allocator: std.mem.Allocator, build_root: []const u8) !bool {
|
fn isDirty(allocator: std.mem.Allocator, io: std.Io, build_root: []const u8) !bool {
|
||||||
// Get .git/index mtime
|
// Get .git/index mtime
|
||||||
const index_path = try std.fs.path.join(allocator, &[_][]const u8{ build_root, ".git", "index" });
|
const index_path = try std.fs.path.join(allocator, &[_][]const u8{ build_root, ".git", "index" });
|
||||||
defer allocator.free(index_path);
|
defer allocator.free(index_path);
|
||||||
|
|
||||||
const index_stat = std.fs.cwd().statFile(index_path) catch return error.CannotDetermineDirty;
|
const index_stat = std.Io.Dir.cwd().statFile(io, index_path, .{}) catch return error.CannotDetermineDirty;
|
||||||
const index_mtime = index_stat.mtime;
|
const index_mtime = index_stat.mtime;
|
||||||
|
|
||||||
// Read .gitignore
|
// Read .gitignore
|
||||||
const ignore_path = try std.fs.path.join(allocator, &[_][]const u8{ build_root, ".gitignore" });
|
const ignore_path = try std.fs.path.join(allocator, &[_][]const u8{ build_root, ".gitignore" });
|
||||||
defer allocator.free(ignore_path);
|
defer allocator.free(ignore_path);
|
||||||
const ignore_data = std.fs.cwd().readFileAlloc(allocator, ignore_path, 1024 * 1024) catch
|
const ignore_data = std.Io.Dir.cwd().readFileAlloc(io, ignore_path, allocator, .limited(1024 * 1024)) catch
|
||||||
try allocator.dupe(u8, "");
|
try allocator.dupe(u8, "");
|
||||||
defer allocator.free(ignore_data);
|
defer allocator.free(ignore_data);
|
||||||
|
|
||||||
// Walk source files in build root and check if any are newer
|
// Walk source files in build root and check if any are newer
|
||||||
var dir = std.fs.openDirAbsolute(build_root, .{ .iterate = true }) catch return error.CannotDetermineDirty;
|
var dir = std.Io.Dir.cwd().openDir(io, build_root, .{ .iterate = true }) catch return error.CannotDetermineDirty;
|
||||||
defer dir.close();
|
defer dir.close(io);
|
||||||
|
|
||||||
var walker = dir.walk(allocator) catch return error.CannotDetermineDirty;
|
var walker = dir.walk(allocator) catch return error.CannotDetermineDirty;
|
||||||
defer walker.deinit();
|
defer walker.deinit();
|
||||||
|
|
||||||
while (walker.next() catch return error.CannotDetermineDirty) |entry| {
|
while (walker.next(io) catch return error.CannotDetermineDirty) |entry| {
|
||||||
if (entry.kind != .file) continue;
|
if (entry.kind != .file) continue;
|
||||||
|
|
||||||
// Always ignore .git/
|
// Always ignore .git/
|
||||||
|
|
@ -129,8 +127,8 @@ fn isDirty(allocator: std.mem.Allocator, build_root: []const u8) !bool {
|
||||||
}
|
}
|
||||||
if (ignored) continue;
|
if (ignored) continue;
|
||||||
|
|
||||||
const stat = entry.dir.statFile(entry.basename) catch continue;
|
const stat = entry.dir.statFile(io, entry.basename, .{}) catch continue;
|
||||||
if (stat.mtime > index_mtime) {
|
if (stat.mtime.nanoseconds > index_mtime.nanoseconds) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
.name = .zfin_server,
|
.name = .zfin_server,
|
||||||
.version = "0.1.0",
|
.version = "0.1.0",
|
||||||
.fingerprint = 0xa060a7d1947dfe8f,
|
.fingerprint = 0xa060a7d1947dfe8f,
|
||||||
.minimum_zig_version = "0.15.2",
|
.minimum_zig_version = "0.16.0",
|
||||||
.paths = .{
|
.paths = .{
|
||||||
"build.zig",
|
"build.zig",
|
||||||
"build.zig.zon",
|
"build.zig.zon",
|
||||||
|
|
@ -10,12 +10,12 @@
|
||||||
},
|
},
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.httpz = .{
|
.httpz = .{
|
||||||
.url = "git+https://github.com/karlseguin/http.zig#844f8016e6616f00b05d4cc3c713307b0fe586c7",
|
.url = "git+https://github.com/karlseguin/http.zig?ref=master#03658c279937066201aba8c0a8f1936d821d0709",
|
||||||
.hash = "httpz-0.0.0-PNVzrBtMBwAPcQx3mNEgat3Xbsynw-eIC9SmOX5M9XtP",
|
.hash = "httpz-0.0.0-PNVzrLjJCAD37S0CcrXpsjSqr86hVjK0rsALTDJ98AAJ",
|
||||||
},
|
},
|
||||||
.zfin = .{
|
.zfin = .{
|
||||||
.url = "git+https://git.lerch.org/lobo/zfin#f9c7fa99e4d30ac613c45572bbe1c45cd3767b3d",
|
.url = "git+https://git.lerch.org/lobo/zfin#fe2894975751ab2bf9ea733e89396bf43c53532f",
|
||||||
.hash = "zfin-0.0.0-J-B21u7_IgD0BHz6KGXpZ4sWj7aIOHwcTKCOByLAUYKJ",
|
.hash = "zfin-0.0.0-J-B21tQtMQC0QPMuhUwnofeKdo2MU5XtCtxpvZeZr2kc",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
118
src/main.zig
118
src/main.zig
|
|
@ -17,14 +17,22 @@ const log = std.log.scoped(.@"zfin-server");
|
||||||
// ── App ──────────────────────────────────────────────────────
|
// ── App ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
const App = struct {
|
const App = struct {
|
||||||
|
io: std.Io,
|
||||||
|
environ: *const std.process.Environ.Map,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
config: zfin.Config,
|
config: zfin.Config,
|
||||||
svc: zfin.DataService,
|
svc: zfin.DataService,
|
||||||
|
|
||||||
fn init(allocator: std.mem.Allocator) App {
|
fn init(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process.Environ.Map) App {
|
||||||
const config = zfin.Config.fromEnv(allocator);
|
const config = zfin.Config.fromEnv(io, allocator, environ);
|
||||||
const svc = zfin.DataService.init(allocator, config);
|
const svc = zfin.DataService.init(io, allocator, config);
|
||||||
return .{ .allocator = allocator, .config = config, .svc = svc };
|
return .{
|
||||||
|
.io = io,
|
||||||
|
.environ = environ,
|
||||||
|
.allocator = allocator,
|
||||||
|
.config = config,
|
||||||
|
.svc = svc,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deinit(self: *App) void {
|
fn deinit(self: *App) void {
|
||||||
|
|
@ -60,6 +68,7 @@ fn handleHelp(_: *App, _: *httpz.Request, res: *httpz.Response) !void {
|
||||||
\\ GET /{SYMBOL}/candles Raw SRF cache file
|
\\ GET /{SYMBOL}/candles Raw SRF cache file
|
||||||
\\ GET /{SYMBOL}/candles_meta Candle freshness metadata (SRF)
|
\\ GET /{SYMBOL}/candles_meta Candle freshness metadata (SRF)
|
||||||
\\ GET /{SYMBOL}/dividends Raw SRF cache file
|
\\ GET /{SYMBOL}/dividends Raw SRF cache file
|
||||||
|
\\ GET /{SYMBOL}/splits Raw SRF cache file
|
||||||
\\ GET /{SYMBOL}/earnings Raw SRF cache file
|
\\ GET /{SYMBOL}/earnings Raw SRF cache file
|
||||||
\\ GET /{SYMBOL}/options Raw SRF cache file
|
\\ GET /{SYMBOL}/options Raw SRF cache file
|
||||||
\\ GET /symbols List of tracked symbols
|
\\ GET /symbols List of tracked symbols
|
||||||
|
|
@ -78,11 +87,11 @@ fn handleHelp(_: *App, _: *httpz.Request, res: *httpz.Response) !void {
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handleSymbols(_: *App, _: *httpz.Request, res: *httpz.Response) !void {
|
fn handleSymbols(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
|
||||||
const arena = res.arena;
|
const arena = res.arena;
|
||||||
const portfolio_path = std.posix.getenv("ZFIN_PORTFOLIO") orelse "portfolio.srf";
|
const portfolio_path = app.environ.get("ZFIN_PORTFOLIO") orelse "portfolio.srf";
|
||||||
|
|
||||||
const file_data = std.fs.cwd().readFileAlloc(arena, portfolio_path, 10 * 1024 * 1024) catch {
|
const file_data = std.Io.Dir.cwd().readFileAlloc(app.io, portfolio_path, arena, .limited(10 * 1024 * 1024)) catch {
|
||||||
res.content_type = httpz.ContentType.JSON;
|
res.content_type = httpz.ContentType.JSON;
|
||||||
res.body = "[]";
|
res.body = "[]";
|
||||||
return;
|
return;
|
||||||
|
|
@ -131,7 +140,7 @@ fn handleReturns(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
|
||||||
const q = try req.query();
|
const q = try req.query();
|
||||||
if (q.get("watch")) |w| {
|
if (q.get("watch")) |w| {
|
||||||
if (std.ascii.eqlIgnoreCase(w, "true")) {
|
if (std.ascii.eqlIgnoreCase(w, "true")) {
|
||||||
appendWatchSymbol(symbol) catch |err| {
|
appendWatchSymbol(app, symbol) catch |err| {
|
||||||
log.warn("failed to append watch symbol {s}: {}", .{ symbol, err });
|
log.warn("failed to append watch symbol {s}: {}", .{ symbol, err });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -156,15 +165,22 @@ fn handleReturns(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
|
||||||
|
|
||||||
const last_close = candles[candles.len - 1].close;
|
const last_close = candles[candles.len - 1].close;
|
||||||
var date_buf: [10]u8 = undefined;
|
var date_buf: [10]u8 = undefined;
|
||||||
const date_str = candles[candles.len - 1].date.format(&date_buf);
|
const date_str = try std.fmt.bufPrint(&date_buf, "{f}", .{candles[candles.len - 1].date});
|
||||||
|
|
||||||
// Price-only returns (from adjusted close)
|
// Price-only returns (split-adjusted, NOT dividend-adjusted —
|
||||||
|
// see analytics/performance.zig:trailingReturnsPriceOnly).
|
||||||
|
// Matches the "price return" numbers public sources publish
|
||||||
|
// (Yahoo chart-bar, FMP, Barchart, Fidelity stock pages).
|
||||||
const p1y = if (result.asof_price.one_year) |r| r.annualized_return else null;
|
const p1y = if (result.asof_price.one_year) |r| r.annualized_return else null;
|
||||||
const p3y = if (result.asof_price.three_year) |r| r.annualized_return else null;
|
const p3y = if (result.asof_price.three_year) |r| r.annualized_return else null;
|
||||||
const p5y = if (result.asof_price.five_year) |r| r.annualized_return else null;
|
const p5y = if (result.asof_price.five_year) |r| r.annualized_return else null;
|
||||||
const p10y = if (result.asof_price.ten_year) |r| r.annualized_return else null;
|
const p10y = if (result.asof_price.ten_year) |r| r.annualized_return else null;
|
||||||
|
|
||||||
// Total returns (best of adj_close and dividend reinvestment)
|
// Total returns (dividend reinvestment when dividends are
|
||||||
|
// available; falls back to adj_close-based total return when
|
||||||
|
// dividend records are missing). Matches Morningstar
|
||||||
|
// "Trailing Returns" / Yahoo "Performance Overview" / Koyfin
|
||||||
|
// "Total Return".
|
||||||
const total = result.asof_total orelse result.asof_price;
|
const total = result.asof_total orelse result.asof_price;
|
||||||
const t1y = if (total.one_year) |r| r.annualized_return else null;
|
const t1y = if (total.one_year) |r| r.annualized_return else null;
|
||||||
const t3y = if (total.three_year) |r| r.annualized_return else null;
|
const t3y = if (total.three_year) |r| r.annualized_return else null;
|
||||||
|
|
@ -285,7 +301,7 @@ fn handleSrfFile(app: *App, req: *httpz.Request, res: *httpz.Response, filename:
|
||||||
const symbol = try upperDupe(arena, raw_symbol);
|
const symbol = try upperDupe(arena, raw_symbol);
|
||||||
|
|
||||||
const path = try std.fs.path.join(arena, &.{ app.config.cache_dir, symbol, filename });
|
const path = try std.fs.path.join(arena, &.{ app.config.cache_dir, symbol, filename });
|
||||||
const content = std.fs.cwd().readFileAlloc(arena, path, 10 * 1024 * 1024) catch {
|
const content = std.Io.Dir.cwd().readFileAlloc(app.io, path, arena, .limited(10 * 1024 * 1024)) catch {
|
||||||
res.status = 404;
|
res.status = 404;
|
||||||
res.body = "Cache file not found";
|
res.body = "Cache file not found";
|
||||||
return;
|
return;
|
||||||
|
|
@ -323,6 +339,10 @@ fn handleDividends(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
|
||||||
return handleSrfFile(app, req, res, "dividends.srf");
|
return handleSrfFile(app, req, res, "dividends.srf");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handleSplits(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
|
||||||
|
return handleSrfFile(app, req, res, "splits.srf");
|
||||||
|
}
|
||||||
|
|
||||||
fn handleEarnings(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
|
fn handleEarnings(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
|
||||||
return handleSrfFile(app, req, res, "earnings.srf");
|
return handleSrfFile(app, req, res, "earnings.srf");
|
||||||
}
|
}
|
||||||
|
|
@ -362,13 +382,14 @@ fn fmtInt(arena: std.mem.Allocator, value: ?u8) []const u8 {
|
||||||
|
|
||||||
/// Append a watch lot for the given symbol to the portfolio SRF file,
|
/// Append a watch lot for the given symbol to the portfolio SRF file,
|
||||||
/// unless it already exists. Best-effort — errors are logged, not fatal.
|
/// unless it already exists. Best-effort — errors are logged, not fatal.
|
||||||
fn appendWatchSymbol(symbol: []const u8) !void {
|
fn appendWatchSymbol(app: *App, symbol: []const u8) !void {
|
||||||
const portfolio_path = std.posix.getenv("ZFIN_PORTFOLIO") orelse "portfolio.srf";
|
const portfolio_path = app.environ.get("ZFIN_PORTFOLIO") orelse "portfolio.srf";
|
||||||
const allocator = std.heap.page_allocator;
|
const allocator = app.allocator;
|
||||||
|
const io = app.io;
|
||||||
|
|
||||||
// Read and deserialize existing portfolio (or start empty)
|
// Read and deserialize existing portfolio (or start empty)
|
||||||
const file_data = std.fs.cwd().readFileAlloc(allocator, portfolio_path, 10 * 1024 * 1024) catch |err| {
|
const file_data = std.Io.Dir.cwd().readFileAlloc(io, portfolio_path, allocator, .limited(10 * 1024 * 1024)) catch |err| {
|
||||||
if (err == error.FileNotFound) return writeNewPortfolio(allocator, portfolio_path, symbol);
|
if (err == error.FileNotFound) return writeNewPortfolio(io, allocator, portfolio_path, symbol);
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
defer allocator.free(file_data);
|
defer allocator.free(file_data);
|
||||||
|
|
@ -397,14 +418,17 @@ fn appendWatchSymbol(symbol: []const u8) !void {
|
||||||
const output = try zfin.cache.serializePortfolio(allocator, new_lots);
|
const output = try zfin.cache.serializePortfolio(allocator, new_lots);
|
||||||
defer allocator.free(output);
|
defer allocator.free(output);
|
||||||
|
|
||||||
const file = try std.fs.cwd().createFile(portfolio_path, .{});
|
const file = try std.Io.Dir.cwd().createFile(io, portfolio_path, .{});
|
||||||
defer file.close();
|
defer file.close(io);
|
||||||
try file.writeAll(output);
|
var write_buf: [4096]u8 = undefined;
|
||||||
|
var fw = file.writer(io, &write_buf);
|
||||||
|
try fw.interface.writeAll(output);
|
||||||
|
try fw.interface.flush();
|
||||||
|
|
||||||
log.info("added watch symbol {s} to {s}", .{ symbol, portfolio_path });
|
log.info("added watch symbol {s} to {s}", .{ symbol, portfolio_path });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn writeNewPortfolio(allocator: std.mem.Allocator, path: []const u8, symbol: []const u8) !void {
|
fn writeNewPortfolio(io: std.Io, allocator: std.mem.Allocator, path: []const u8, symbol: []const u8) !void {
|
||||||
const lot = [_]zfin.Lot{.{
|
const lot = [_]zfin.Lot{.{
|
||||||
.symbol = symbol,
|
.symbol = symbol,
|
||||||
.shares = 0,
|
.shares = 0,
|
||||||
|
|
@ -415,24 +439,27 @@ fn writeNewPortfolio(allocator: std.mem.Allocator, path: []const u8, symbol: []c
|
||||||
const output = try zfin.cache.serializePortfolio(allocator, &lot);
|
const output = try zfin.cache.serializePortfolio(allocator, &lot);
|
||||||
defer allocator.free(output);
|
defer allocator.free(output);
|
||||||
|
|
||||||
const file = try std.fs.cwd().createFile(path, .{});
|
const file = try std.Io.Dir.cwd().createFile(io, path, .{});
|
||||||
defer file.close();
|
defer file.close(io);
|
||||||
try file.writeAll(output);
|
var write_buf: [4096]u8 = undefined;
|
||||||
|
var fw = file.writer(io, &write_buf);
|
||||||
|
try fw.interface.writeAll(output);
|
||||||
|
try fw.interface.flush();
|
||||||
|
|
||||||
log.info("created {s} with watch symbol {s}", .{ path, symbol });
|
log.info("created {s} with watch symbol {s}", .{ path, symbol });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Refresh command ──────────────────────────────────────────
|
// ── Refresh command ──────────────────────────────────────────
|
||||||
|
|
||||||
fn refresh(allocator: std.mem.Allocator) !void {
|
fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process.Environ.Map) !void {
|
||||||
var config = zfin.Config.fromEnv(allocator);
|
var config = zfin.Config.fromEnv(io, allocator, environ);
|
||||||
defer config.deinit();
|
defer config.deinit();
|
||||||
var svc = zfin.DataService.init(allocator, config);
|
var svc = zfin.DataService.init(io, allocator, config);
|
||||||
defer svc.deinit();
|
defer svc.deinit();
|
||||||
|
|
||||||
const portfolio_path = std.posix.getenv("ZFIN_PORTFOLIO") orelse "portfolio.srf";
|
const portfolio_path = environ.get("ZFIN_PORTFOLIO") orelse "portfolio.srf";
|
||||||
|
|
||||||
const data = std.fs.cwd().readFileAlloc(allocator, portfolio_path, 10 * 1024 * 1024) catch {
|
const data = std.Io.Dir.cwd().readFileAlloc(io, portfolio_path, allocator, .limited(10 * 1024 * 1024)) catch {
|
||||||
log.err("failed to read portfolio: {s}", .{portfolio_path});
|
log.err("failed to read portfolio: {s}", .{portfolio_path});
|
||||||
return error.ReadFailed;
|
return error.ReadFailed;
|
||||||
};
|
};
|
||||||
|
|
@ -455,9 +482,9 @@ fn refresh(allocator: std.mem.Allocator) !void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stdout_file = std.fs.File.stdout();
|
const stdout_file = std.Io.File.stdout();
|
||||||
var buf: [4096]u8 = undefined;
|
var buf: [4096]u8 = undefined;
|
||||||
var writer = stdout_file.writer(&buf);
|
var writer = stdout_file.writer(io, &buf);
|
||||||
const stdout = &writer.interface;
|
const stdout = &writer.interface;
|
||||||
|
|
||||||
try stdout.print("zfin-server {s}\n", .{version});
|
try stdout.print("zfin-server {s}\n", .{version});
|
||||||
|
|
@ -503,6 +530,16 @@ fn refresh(allocator: std.mem.Allocator) !void {
|
||||||
sym_ok = false;
|
sym_ok = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Splits
|
||||||
|
try printRateLimitWait(&svc, stdout);
|
||||||
|
if (svc.getSplits(sym)) |result| {
|
||||||
|
allocator.free(result.data);
|
||||||
|
try stdout.print(", splits ok", .{});
|
||||||
|
} else |err| {
|
||||||
|
try stdout.print(", splits FAILED ({s})", .{@errorName(err)});
|
||||||
|
sym_ok = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Earnings
|
// Earnings
|
||||||
try printRateLimitWait(&svc, stdout);
|
try printRateLimitWait(&svc, stdout);
|
||||||
if (svc.getEarnings(sym)) |result| {
|
if (svc.getEarnings(sym)) |result| {
|
||||||
|
|
@ -527,13 +564,13 @@ fn refresh(allocator: std.mem.Allocator) !void {
|
||||||
|
|
||||||
// ── Main ─────────────────────────────────────────────────────
|
// ── Main ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main(init: std.process.Init) !void {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
const allocator = init.gpa;
|
||||||
defer _ = gpa.deinit();
|
const io = init.io;
|
||||||
const allocator = gpa.allocator();
|
const environ = init.environ_map;
|
||||||
|
|
||||||
const args = try std.process.argsAlloc(allocator);
|
const args = try init.minimal.args.toSlice(allocator);
|
||||||
defer std.process.argsFree(allocator, args);
|
defer allocator.free(args);
|
||||||
|
|
||||||
if (args.len < 2) {
|
if (args.len < 2) {
|
||||||
printUsage();
|
printUsage();
|
||||||
|
|
@ -550,10 +587,10 @@ pub fn main() !void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var app = App.init(allocator);
|
var app = App.init(io, allocator, environ);
|
||||||
defer app.deinit();
|
defer app.deinit();
|
||||||
|
|
||||||
var server = try httpz.Server(*App).init(allocator, .{
|
var server = try httpz.Server(*App).init(io, allocator, .{
|
||||||
.address = .all(port),
|
.address = .all(port),
|
||||||
}, &app);
|
}, &app);
|
||||||
defer {
|
defer {
|
||||||
|
|
@ -574,6 +611,7 @@ pub fn main() !void {
|
||||||
router.get("/:symbol/candles", handleCandles, .{});
|
router.get("/:symbol/candles", handleCandles, .{});
|
||||||
router.get("/:symbol/candles_meta", handleCandlesMeta, .{});
|
router.get("/:symbol/candles_meta", handleCandlesMeta, .{});
|
||||||
router.get("/:symbol/dividends", handleDividends, .{});
|
router.get("/:symbol/dividends", handleDividends, .{});
|
||||||
|
router.get("/:symbol/splits", handleSplits, .{});
|
||||||
router.get("/:symbol/earnings", handleEarnings, .{});
|
router.get("/:symbol/earnings", handleEarnings, .{});
|
||||||
router.get("/:symbol/options", handleOptions, .{});
|
router.get("/:symbol/options", handleOptions, .{});
|
||||||
|
|
||||||
|
|
@ -581,7 +619,7 @@ pub fn main() !void {
|
||||||
log.info("listening on port {d}", .{port});
|
log.info("listening on port {d}", .{port});
|
||||||
try server.listen();
|
try server.listen();
|
||||||
} else if (std.mem.eql(u8, command, "refresh")) {
|
} else if (std.mem.eql(u8, command, "refresh")) {
|
||||||
try refresh(allocator);
|
try refresh(io, allocator, environ);
|
||||||
} else {
|
} else {
|
||||||
printUsage();
|
printUsage();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue