some cleaning and color matching
This commit is contained in:
parent
dee910c33a
commit
181c164394
3 changed files with 184 additions and 214 deletions
|
|
@ -1,3 +1,5 @@
|
||||||
[tools]
|
[tools]
|
||||||
|
prek = "0.3.1"
|
||||||
zig = "0.15.2"
|
zig = "0.15.2"
|
||||||
zls = "0.15.1"
|
zls = "0.15.1"
|
||||||
|
"ubi:DonIsaac/zlint" = "0.7.9"
|
||||||
|
|
|
||||||
35
.pre-commit-config.yaml
Normal file
35
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-added-large-files
|
||||||
|
- repo: https://github.com/batmac/pre-commit-zig
|
||||||
|
rev: v0.3.0
|
||||||
|
hooks:
|
||||||
|
- id: zig-fmt
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: zlint
|
||||||
|
name: Run zlint
|
||||||
|
entry: zlint
|
||||||
|
args: ["--deny-warnings", "--fix"]
|
||||||
|
language: system
|
||||||
|
types: [zig]
|
||||||
|
- repo: https://github.com/batmac/pre-commit-zig
|
||||||
|
rev: v0.3.0
|
||||||
|
hooks:
|
||||||
|
- id: zig-build
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: test
|
||||||
|
name: Run zig build test
|
||||||
|
entry: zig
|
||||||
|
args: ["build", "coverage", "-Dcoverage-threshold=80"]
|
||||||
|
language: system
|
||||||
|
types: [file]
|
||||||
|
pass_filenames: false
|
||||||
361
src/cli/main.zig
361
src/cli/main.zig
|
|
@ -58,13 +58,13 @@ const usage =
|
||||||
\\
|
\\
|
||||||
;
|
;
|
||||||
|
|
||||||
// ── Default CLI colors (match TUI default theme) ─────────────
|
// ── Default CLI colors (match TUI default Monokai theme) ─────
|
||||||
const CLR_GREEN = [3]u8{ 166, 227, 161 }; // positive
|
const CLR_POSITIVE = [3]u8{ 0x7f, 0xd8, 0x8f }; // gains (TUI .positive)
|
||||||
const CLR_RED = [3]u8{ 243, 139, 168 }; // negative
|
const CLR_NEGATIVE = [3]u8{ 0xe0, 0x6c, 0x75 }; // losses (TUI .negative)
|
||||||
const CLR_MUTED = [3]u8{ 128, 128, 128 }; // muted/dim
|
const CLR_MUTED = [3]u8{ 0x80, 0x80, 0x80 }; // dim/secondary text (TUI .text_muted)
|
||||||
const CLR_HEADER = [3]u8{ 205, 214, 244 }; // headers
|
const CLR_HEADER = [3]u8{ 0x9d, 0x7c, 0xd8 }; // section headers (TUI .accent)
|
||||||
const CLR_ACCENT = [3]u8{ 137, 180, 250 }; // info/accent
|
const CLR_ACCENT = [3]u8{ 0x89, 0xb4, 0xfa }; // info highlights, bar fills (TUI .bar_fill)
|
||||||
const CLR_YELLOW = [3]u8{ 249, 226, 175 }; // stale/manual price indicator
|
const CLR_WARNING = [3]u8{ 0xe5, 0xc0, 0x7b }; // stale/manual price indicator (TUI .warning)
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
|
@ -74,8 +74,14 @@ pub fn main() !void {
|
||||||
const args = try std.process.argsAlloc(allocator);
|
const args = try std.process.argsAlloc(allocator);
|
||||||
defer std.process.argsFree(allocator, args);
|
defer std.process.argsFree(allocator, args);
|
||||||
|
|
||||||
|
// Single buffered writer for all stdout output
|
||||||
|
var stdout_buf: [4096]u8 = undefined;
|
||||||
|
var stdout_writer = std.fs.File.stdout().writer(&stdout_buf);
|
||||||
|
const out: *std.Io.Writer = &stdout_writer.interface;
|
||||||
|
|
||||||
if (args.len < 2) {
|
if (args.len < 2) {
|
||||||
try stdout_print(usage);
|
try out.writeAll(usage);
|
||||||
|
try out.flush();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,12 +97,14 @@ pub fn main() !void {
|
||||||
const command = args[1];
|
const command = args[1];
|
||||||
|
|
||||||
if (std.mem.eql(u8, command, "help") or std.mem.eql(u8, command, "--help") or std.mem.eql(u8, command, "-h")) {
|
if (std.mem.eql(u8, command, "help") or std.mem.eql(u8, command, "--help") or std.mem.eql(u8, command, "-h")) {
|
||||||
try stdout_print(usage);
|
try out.writeAll(usage);
|
||||||
|
try out.flush();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interactive TUI -- delegates to the TUI module (owns its own DataService)
|
// Interactive TUI -- delegates to the TUI module (owns its own DataService)
|
||||||
if (std.mem.eql(u8, command, "interactive") or std.mem.eql(u8, command, "i")) {
|
if (std.mem.eql(u8, command, "interactive") or std.mem.eql(u8, command, "i")) {
|
||||||
|
try out.flush();
|
||||||
try tui.run(allocator, config, args);
|
try tui.run(allocator, config, args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -105,22 +113,22 @@ pub fn main() !void {
|
||||||
defer svc.deinit();
|
defer svc.deinit();
|
||||||
|
|
||||||
if (std.mem.eql(u8, command, "perf")) {
|
if (std.mem.eql(u8, command, "perf")) {
|
||||||
if (args.len < 3) return try stderr_print("Error: 'perf' requires a symbol argument\n");
|
if (args.len < 3) return try stderrPrint("Error: 'perf' requires a symbol argument\n");
|
||||||
try cmdPerf(allocator, &svc, args[2], color);
|
try cmdPerf(allocator, &svc, args[2], color, out);
|
||||||
} else if (std.mem.eql(u8, command, "quote")) {
|
} else if (std.mem.eql(u8, command, "quote")) {
|
||||||
if (args.len < 3) return try stderr_print("Error: 'quote' requires a symbol argument\n");
|
if (args.len < 3) return try stderrPrint("Error: 'quote' requires a symbol argument\n");
|
||||||
try cmdQuote(allocator, config, &svc, args[2], color);
|
try cmdQuote(allocator, config, &svc, args[2], color, out);
|
||||||
} else if (std.mem.eql(u8, command, "history")) {
|
} else if (std.mem.eql(u8, command, "history")) {
|
||||||
if (args.len < 3) return try stderr_print("Error: 'history' requires a symbol argument\n");
|
if (args.len < 3) return try stderrPrint("Error: 'history' requires a symbol argument\n");
|
||||||
try cmdHistory(allocator, &svc, args[2], color);
|
try cmdHistory(allocator, &svc, args[2], color, out);
|
||||||
} else if (std.mem.eql(u8, command, "divs")) {
|
} else if (std.mem.eql(u8, command, "divs")) {
|
||||||
if (args.len < 3) return try stderr_print("Error: 'divs' requires a symbol argument\n");
|
if (args.len < 3) return try stderrPrint("Error: 'divs' requires a symbol argument\n");
|
||||||
try cmdDivs(allocator, &svc, config, args[2], color);
|
try cmdDivs(allocator, &svc, config, args[2], color, out);
|
||||||
} else if (std.mem.eql(u8, command, "splits")) {
|
} else if (std.mem.eql(u8, command, "splits")) {
|
||||||
if (args.len < 3) return try stderr_print("Error: 'splits' requires a symbol argument\n");
|
if (args.len < 3) return try stderrPrint("Error: 'splits' requires a symbol argument\n");
|
||||||
try cmdSplits(allocator, &svc, args[2], color);
|
try cmdSplits(allocator, &svc, args[2], color, out);
|
||||||
} else if (std.mem.eql(u8, command, "options")) {
|
} else if (std.mem.eql(u8, command, "options")) {
|
||||||
if (args.len < 3) return try stderr_print("Error: 'options' requires a symbol argument\n");
|
if (args.len < 3) return try stderrPrint("Error: 'options' requires a symbol argument\n");
|
||||||
// Parse --ntm flag
|
// Parse --ntm flag
|
||||||
var ntm: usize = 8;
|
var ntm: usize = 8;
|
||||||
var ai: usize = 3;
|
var ai: usize = 3;
|
||||||
|
|
@ -130,13 +138,13 @@ pub fn main() !void {
|
||||||
ntm = std.fmt.parseInt(usize, args[ai], 10) catch 8;
|
ntm = std.fmt.parseInt(usize, args[ai], 10) catch 8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try cmdOptions(allocator, &svc, args[2], ntm, color);
|
try cmdOptions(allocator, &svc, args[2], ntm, color, out);
|
||||||
} else if (std.mem.eql(u8, command, "earnings")) {
|
} else if (std.mem.eql(u8, command, "earnings")) {
|
||||||
if (args.len < 3) return try stderr_print("Error: 'earnings' requires a symbol argument\n");
|
if (args.len < 3) return try stderrPrint("Error: 'earnings' requires a symbol argument\n");
|
||||||
try cmdEarnings(allocator, &svc, args[2], color);
|
try cmdEarnings(allocator, &svc, args[2], color, out);
|
||||||
} else if (std.mem.eql(u8, command, "etf")) {
|
} else if (std.mem.eql(u8, command, "etf")) {
|
||||||
if (args.len < 3) return try stderr_print("Error: 'etf' requires a symbol argument\n");
|
if (args.len < 3) return try stderrPrint("Error: 'etf' requires a symbol argument\n");
|
||||||
try cmdEtf(allocator, &svc, args[2], color);
|
try cmdEtf(allocator, &svc, args[2], color, out);
|
||||||
} else if (std.mem.eql(u8, command, "portfolio")) {
|
} else if (std.mem.eql(u8, command, "portfolio")) {
|
||||||
// Parse -w/--watchlist and --refresh flags; file path is first non-flag arg (default: portfolio.srf)
|
// Parse -w/--watchlist and --refresh flags; file path is first non-flag arg (default: portfolio.srf)
|
||||||
var watchlist_path: ?[]const u8 = null;
|
var watchlist_path: ?[]const u8 = null;
|
||||||
|
|
@ -155,16 +163,16 @@ pub fn main() !void {
|
||||||
file_path = args[pi];
|
file_path = args[pi];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try cmdPortfolio(allocator, config, &svc, file_path, watchlist_path, force_refresh, color);
|
try cmdPortfolio(allocator, config, &svc, file_path, watchlist_path, force_refresh, color, out);
|
||||||
} else if (std.mem.eql(u8, command, "lookup")) {
|
} else if (std.mem.eql(u8, command, "lookup")) {
|
||||||
if (args.len < 3) return try stderr_print("Error: 'lookup' requires a CUSIP argument\n");
|
if (args.len < 3) return try stderrPrint("Error: 'lookup' requires a CUSIP argument\n");
|
||||||
try cmdLookup(allocator, &svc, args[2], color);
|
try cmdLookup(allocator, &svc, args[2], color, out);
|
||||||
} else if (std.mem.eql(u8, command, "cache")) {
|
} else if (std.mem.eql(u8, command, "cache")) {
|
||||||
if (args.len < 3) return try stderr_print("Error: 'cache' requires a subcommand (stats, clear)\n");
|
if (args.len < 3) return try stderrPrint("Error: 'cache' requires a subcommand (stats, clear)\n");
|
||||||
try cmdCache(allocator, config, args[2]);
|
try cmdCache(allocator, config, args[2], out);
|
||||||
} else if (std.mem.eql(u8, command, "enrich")) {
|
} else if (std.mem.eql(u8, command, "enrich")) {
|
||||||
if (args.len < 3) return try stderr_print("Error: 'enrich' requires a portfolio file path or symbol\n");
|
if (args.len < 3) return try stderrPrint("Error: 'enrich' requires a portfolio file path or symbol\n");
|
||||||
try cmdEnrich(allocator, config, args[2]);
|
try cmdEnrich(allocator, config, args[2], out);
|
||||||
} else if (std.mem.eql(u8, command, "analysis")) {
|
} else if (std.mem.eql(u8, command, "analysis")) {
|
||||||
// File path is first non-flag arg (default: portfolio.srf)
|
// File path is first non-flag arg (default: portfolio.srf)
|
||||||
var analysis_file: []const u8 = "portfolio.srf";
|
var analysis_file: []const u8 = "portfolio.srf";
|
||||||
|
|
@ -174,62 +182,61 @@ pub fn main() !void {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try cmdAnalysis(allocator, config, &svc, analysis_file, color);
|
try cmdAnalysis(allocator, config, &svc, analysis_file, color, out);
|
||||||
} else {
|
} else {
|
||||||
try stderr_print("Unknown command. Run 'zfin help' for usage.\n");
|
try stderrPrint("Unknown command. Run 'zfin help' for usage.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Single flush for all stdout output
|
||||||
|
try out.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ANSI color helpers ───────────────────────────────────────
|
// ── ANSI color helpers ───────────────────────────────────────
|
||||||
|
|
||||||
fn setFg(out: anytype, c: bool, rgb: [3]u8) !void {
|
fn setFg(out: *std.Io.Writer, c: bool, rgb: [3]u8) !void {
|
||||||
if (c) try fmt.ansiSetFg(out, rgb[0], rgb[1], rgb[2]);
|
if (c) try fmt.ansiSetFg(out, rgb[0], rgb[1], rgb[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setBold(out: anytype, c: bool) !void {
|
fn setBold(out: *std.Io.Writer, c: bool) !void {
|
||||||
if (c) try fmt.ansiBold(out);
|
if (c) try fmt.ansiBold(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(out: anytype, c: bool) !void {
|
fn reset(out: *std.Io.Writer, c: bool) !void {
|
||||||
if (c) try fmt.ansiReset(out);
|
if (c) try fmt.ansiReset(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setGainLoss(out: anytype, c: bool, value: f64) !void {
|
fn setGainLoss(out: *std.Io.Writer, c: bool, value: f64) !void {
|
||||||
if (c) {
|
if (c) {
|
||||||
if (value >= 0)
|
if (value >= 0)
|
||||||
try fmt.ansiSetFg(out, CLR_GREEN[0], CLR_GREEN[1], CLR_GREEN[2])
|
try fmt.ansiSetFg(out, CLR_POSITIVE[0], CLR_POSITIVE[1], CLR_POSITIVE[2])
|
||||||
else
|
else
|
||||||
try fmt.ansiSetFg(out, CLR_RED[0], CLR_RED[1], CLR_RED[2]);
|
try fmt.ansiSetFg(out, CLR_NEGATIVE[0], CLR_NEGATIVE[1], CLR_NEGATIVE[2]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Commands ─────────────────────────────────────────────────
|
// ── Commands ─────────────────────────────────────────────────
|
||||||
|
|
||||||
fn cmdPerf(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool) !void {
|
fn cmdPerf(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||||
const result = svc.getTrailingReturns(symbol) catch |err| switch (err) {
|
const result = svc.getTrailingReturns(symbol) catch |err| switch (err) {
|
||||||
zfin.DataError.NoApiKey => {
|
zfin.DataError.NoApiKey => {
|
||||||
try stderr_print("Error: TWELVEDATA_API_KEY not set. Get a free key at https://twelvedata.com\n");
|
try stderrPrint("Error: TWELVEDATA_API_KEY not set. Get a free key at https://twelvedata.com\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
try stderr_print("Error fetching data.\n");
|
try stderrPrint("Error fetching data.\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
defer allocator.free(result.candles);
|
defer allocator.free(result.candles);
|
||||||
defer if (result.dividends) |d| allocator.free(d);
|
defer if (result.dividends) |d| allocator.free(d);
|
||||||
|
|
||||||
if (result.source == .cached) try stderr_print("(using cached data)\n");
|
if (result.source == .cached) try stderrPrint("(using cached data)\n");
|
||||||
|
|
||||||
const c = result.candles;
|
const c = result.candles;
|
||||||
const end_date = c[c.len - 1].date;
|
const end_date = c[c.len - 1].date;
|
||||||
const today = fmt.todayDate();
|
const today = fmt.todayDate();
|
||||||
const month_end = today.lastDayOfPriorMonth();
|
const month_end = today.lastDayOfPriorMonth();
|
||||||
|
|
||||||
var buf: [8192]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
|
|
||||||
try setBold(out, color);
|
try setBold(out, color);
|
||||||
try out.print("\nTrailing Returns for {s}\n", .{symbol});
|
try out.print("\nTrailing Returns for {s}\n", .{symbol});
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
|
|
@ -275,11 +282,10 @@ fn cmdPerf(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
}
|
}
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
try out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn printReturnsTable(
|
fn printReturnsTable(
|
||||||
out: anytype,
|
out: *std.Io.Writer,
|
||||||
price: zfin.performance.TrailingReturns,
|
price: zfin.performance.TrailingReturns,
|
||||||
total: ?zfin.performance.TrailingReturns,
|
total: ?zfin.performance.TrailingReturns,
|
||||||
color: bool,
|
color: bool,
|
||||||
|
|
@ -350,15 +356,15 @@ fn printReturnsTable(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmdQuote(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataService, symbol: []const u8, color: bool) !void {
|
fn cmdQuote(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||||
// Fetch candle data for chart and history
|
// Fetch candle data for chart and history
|
||||||
const candle_result = svc.getCandles(symbol) catch |err| switch (err) {
|
const candle_result = svc.getCandles(symbol) catch |err| switch (err) {
|
||||||
zfin.DataError.NoApiKey => {
|
zfin.DataError.NoApiKey => {
|
||||||
try stderr_print("Error: TWELVEDATA_API_KEY not set.\n");
|
try stderrPrint("Error: TWELVEDATA_API_KEY not set.\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
try stderr_print("Error fetching candle data.\n");
|
try stderrPrint("Error fetching candle data.\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -394,10 +400,6 @@ fn cmdQuote(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSe
|
||||||
} else |_| {}
|
} else |_| {}
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf: [16384]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
try setBold(out, color);
|
try setBold(out, color);
|
||||||
if (has_quote) {
|
if (has_quote) {
|
||||||
|
|
@ -449,7 +451,7 @@ fn cmdQuote(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSe
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
const chart_days: usize = @min(candles.len, 60);
|
const chart_days: usize = @min(candles.len, 60);
|
||||||
const chart_data = candles[candles.len - chart_days ..];
|
const chart_data = candles[candles.len - chart_days ..];
|
||||||
var chart = fmt.computeBrailleChart(allocator, chart_data, 60, 10, CLR_GREEN, CLR_RED) catch null;
|
var chart = fmt.computeBrailleChart(allocator, chart_data, 60, 10, CLR_POSITIVE, CLR_NEGATIVE) catch null;
|
||||||
if (chart) |*ch| {
|
if (chart) |*ch| {
|
||||||
defer ch.deinit(allocator);
|
defer ch.deinit(allocator);
|
||||||
try fmt.writeBrailleAnsi(out, ch, color, CLR_MUTED);
|
try fmt.writeBrailleAnsi(out, ch, color, CLR_MUTED);
|
||||||
|
|
@ -483,35 +485,30 @@ fn cmdQuote(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSe
|
||||||
}
|
}
|
||||||
|
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
try out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmdHistory(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool) !void {
|
fn cmdHistory(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||||
const result = svc.getCandles(symbol) catch |err| switch (err) {
|
const result = svc.getCandles(symbol) catch |err| switch (err) {
|
||||||
zfin.DataError.NoApiKey => {
|
zfin.DataError.NoApiKey => {
|
||||||
try stderr_print("Error: TWELVEDATA_API_KEY not set.\n");
|
try stderrPrint("Error: TWELVEDATA_API_KEY not set.\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
try stderr_print("Error fetching data.\n");
|
try stderrPrint("Error fetching data.\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
defer allocator.free(result.data);
|
defer allocator.free(result.data);
|
||||||
|
|
||||||
if (result.source == .cached) try stderr_print("(using cached data)\n");
|
if (result.source == .cached) try stderrPrint("(using cached data)\n");
|
||||||
|
|
||||||
const all = result.data;
|
const all = result.data;
|
||||||
if (all.len == 0) return try stderr_print("No data available.\n");
|
if (all.len == 0) return try stderrPrint("No data available.\n");
|
||||||
|
|
||||||
const today = fmt.todayDate();
|
const today = fmt.todayDate();
|
||||||
const one_month_ago = today.addDays(-30);
|
const one_month_ago = today.addDays(-30);
|
||||||
const c = fmt.filterCandlesFrom(all, one_month_ago);
|
const c = fmt.filterCandlesFrom(all, one_month_ago);
|
||||||
if (c.len == 0) return try stderr_print("No data available.\n");
|
if (c.len == 0) return try stderrPrint("No data available.\n");
|
||||||
|
|
||||||
var buf: [8192]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
|
|
||||||
try setBold(out, color);
|
try setBold(out, color);
|
||||||
try out.print("\nPrice History for {s} (last 30 days)\n", .{symbol});
|
try out.print("\nPrice History for {s} (last 30 days)\n", .{symbol});
|
||||||
|
|
@ -536,23 +533,22 @@ fn cmdHistory(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []co
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
}
|
}
|
||||||
try out.print("\n{d} trading days\n\n", .{c.len});
|
try out.print("\n{d} trading days\n\n", .{c.len});
|
||||||
try out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmdDivs(allocator: std.mem.Allocator, svc: *zfin.DataService, config: zfin.Config, symbol: []const u8, color: bool) !void {
|
fn cmdDivs(allocator: std.mem.Allocator, svc: *zfin.DataService, config: zfin.Config, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||||
const result = svc.getDividends(symbol) catch |err| switch (err) {
|
const result = svc.getDividends(symbol) catch |err| switch (err) {
|
||||||
zfin.DataError.NoApiKey => {
|
zfin.DataError.NoApiKey => {
|
||||||
try stderr_print("Error: POLYGON_API_KEY not set. Get a free key at https://polygon.io\n");
|
try stderrPrint("Error: POLYGON_API_KEY not set. Get a free key at https://polygon.io\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
try stderr_print("Error fetching dividend data.\n");
|
try stderrPrint("Error fetching dividend data.\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
defer allocator.free(result.data);
|
defer allocator.free(result.data);
|
||||||
|
|
||||||
if (result.source == .cached) try stderr_print("(using cached dividend data)\n");
|
if (result.source == .cached) try stderrPrint("(using cached dividend data)\n");
|
||||||
|
|
||||||
const d = result.data;
|
const d = result.data;
|
||||||
|
|
||||||
|
|
@ -572,10 +568,6 @@ fn cmdDivs(allocator: std.mem.Allocator, svc: *zfin.DataService, config: zfin.Co
|
||||||
} else |_| {}
|
} else |_| {}
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf: [8192]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
|
|
||||||
try setBold(out, color);
|
try setBold(out, color);
|
||||||
try out.print("\nDividend History for {s}\n", .{symbol});
|
try out.print("\nDividend History for {s}\n", .{symbol});
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
|
|
@ -585,7 +577,6 @@ fn cmdDivs(allocator: std.mem.Allocator, svc: *zfin.DataService, config: zfin.Co
|
||||||
try setFg(out, color, CLR_MUTED);
|
try setFg(out, color, CLR_MUTED);
|
||||||
try out.print(" No dividends found.\n\n", .{});
|
try out.print(" No dividends found.\n\n", .{});
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
try out.flush();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -633,30 +624,25 @@ fn cmdDivs(allocator: std.mem.Allocator, svc: *zfin.DataService, config: zfin.Co
|
||||||
}
|
}
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
try out.print("\n\n", .{});
|
try out.print("\n\n", .{});
|
||||||
try out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmdSplits(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool) !void {
|
fn cmdSplits(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||||
const result = svc.getSplits(symbol) catch |err| switch (err) {
|
const result = svc.getSplits(symbol) catch |err| switch (err) {
|
||||||
zfin.DataError.NoApiKey => {
|
zfin.DataError.NoApiKey => {
|
||||||
try stderr_print("Error: POLYGON_API_KEY not set. Get a free key at https://polygon.io\n");
|
try stderrPrint("Error: POLYGON_API_KEY not set. Get a free key at https://polygon.io\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
try stderr_print("Error fetching split data.\n");
|
try stderrPrint("Error fetching split data.\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
defer allocator.free(result.data);
|
defer allocator.free(result.data);
|
||||||
|
|
||||||
if (result.source == .cached) try stderr_print("(using cached split data)\n");
|
if (result.source == .cached) try stderrPrint("(using cached split data)\n");
|
||||||
|
|
||||||
const sp = result.data;
|
const sp = result.data;
|
||||||
|
|
||||||
var buf: [4096]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
|
|
||||||
try setBold(out, color);
|
try setBold(out, color);
|
||||||
try out.print("\nSplit History for {s}\n", .{symbol});
|
try out.print("\nSplit History for {s}\n", .{symbol});
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
|
|
@ -666,7 +652,6 @@ fn cmdSplits(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []con
|
||||||
try setFg(out, color, CLR_MUTED);
|
try setFg(out, color, CLR_MUTED);
|
||||||
try out.print(" No splits found.\n\n", .{});
|
try out.print(" No splits found.\n\n", .{});
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
try out.flush();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -680,17 +665,16 @@ fn cmdSplits(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []con
|
||||||
try out.print("{s:>12} {d:.0}:{d:.0}\n", .{ s.date.format(&db), s.numerator, s.denominator });
|
try out.print("{s:>12} {d:.0}:{d:.0}\n", .{ s.date.format(&db), s.numerator, s.denominator });
|
||||||
}
|
}
|
||||||
try out.print("\n{d} split(s)\n\n", .{sp.len});
|
try out.print("\n{d} split(s)\n\n", .{sp.len});
|
||||||
try out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmdOptions(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, ntm: usize, color: bool) !void {
|
fn cmdOptions(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, ntm: usize, color: bool, out: *std.Io.Writer) !void {
|
||||||
const result = svc.getOptions(symbol) catch |err| switch (err) {
|
const result = svc.getOptions(symbol) catch |err| switch (err) {
|
||||||
zfin.DataError.FetchFailed => {
|
zfin.DataError.FetchFailed => {
|
||||||
try stderr_print("Error fetching options data from CBOE.\n");
|
try stderrPrint("Error fetching options data from CBOE.\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
try stderr_print("Error loading options data.\n");
|
try stderrPrint("Error loading options data.\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -704,17 +688,13 @@ fn cmdOptions(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []co
|
||||||
allocator.free(ch);
|
allocator.free(ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.source == .cached) try stderr_print("(using cached options data)\n");
|
if (result.source == .cached) try stderrPrint("(using cached options data)\n");
|
||||||
|
|
||||||
if (ch.len == 0) {
|
if (ch.len == 0) {
|
||||||
try stderr_print("No options data found.\n");
|
try stderrPrint("No options data found.\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf: [32768]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
|
|
||||||
try setBold(out, color);
|
try setBold(out, color);
|
||||||
try out.print("\nOptions Chain for {s}\n", .{symbol});
|
try out.print("\nOptions Chain for {s}\n", .{symbol});
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
|
|
@ -772,11 +752,10 @@ fn cmdOptions(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []co
|
||||||
}
|
}
|
||||||
|
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
try out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn printOptionsSection(
|
fn printOptionsSection(
|
||||||
out: anytype,
|
out: *std.Io.Writer,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
label: []const u8,
|
label: []const u8,
|
||||||
contracts: []const zfin.OptionContract,
|
contracts: []const zfin.OptionContract,
|
||||||
|
|
@ -807,27 +786,23 @@ fn printOptionsSection(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmdEarnings(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool) !void {
|
fn cmdEarnings(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||||
const result = svc.getEarnings(symbol) catch |err| switch (err) {
|
const result = svc.getEarnings(symbol) catch |err| switch (err) {
|
||||||
zfin.DataError.NoApiKey => {
|
zfin.DataError.NoApiKey => {
|
||||||
try stderr_print("Error: FINNHUB_API_KEY not set. Get a free key at https://finnhub.io\n");
|
try stderrPrint("Error: FINNHUB_API_KEY not set. Get a free key at https://finnhub.io\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
try stderr_print("Error fetching earnings data.\n");
|
try stderrPrint("Error fetching earnings data.\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
defer allocator.free(result.data);
|
defer allocator.free(result.data);
|
||||||
|
|
||||||
if (result.source == .cached) try stderr_print("(using cached earnings data)\n");
|
if (result.source == .cached) try stderrPrint("(using cached earnings data)\n");
|
||||||
|
|
||||||
const ev = result.data;
|
const ev = result.data;
|
||||||
|
|
||||||
var buf: [8192]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
|
|
||||||
try setBold(out, color);
|
try setBold(out, color);
|
||||||
try out.print("\nEarnings History for {s}\n", .{symbol});
|
try out.print("\nEarnings History for {s}\n", .{symbol});
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
|
|
@ -837,7 +812,6 @@ fn cmdEarnings(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []c
|
||||||
try setFg(out, color, CLR_MUTED);
|
try setFg(out, color, CLR_MUTED);
|
||||||
try out.print(" No earnings data found.\n\n", .{});
|
try out.print(" No earnings data found.\n\n", .{});
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
try out.flush();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -858,9 +832,9 @@ fn cmdEarnings(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []c
|
||||||
if (is_future) {
|
if (is_future) {
|
||||||
try setFg(out, color, CLR_MUTED);
|
try setFg(out, color, CLR_MUTED);
|
||||||
} else if (surprise_positive) {
|
} else if (surprise_positive) {
|
||||||
try setFg(out, color, CLR_GREEN);
|
try setFg(out, color, CLR_POSITIVE);
|
||||||
} else {
|
} else {
|
||||||
try setFg(out, color, CLR_RED);
|
try setFg(out, color, CLR_NEGATIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
try out.print("{s:>12}", .{e.date.format(&db)});
|
try out.print("{s:>12}", .{e.date.format(&db)});
|
||||||
|
|
@ -887,7 +861,6 @@ fn cmdEarnings(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []c
|
||||||
}
|
}
|
||||||
|
|
||||||
try out.print("\n{d} earnings event(s)\n\n", .{ev.len});
|
try out.print("\n{d} earnings event(s)\n\n", .{ev.len});
|
||||||
try out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fmtEps(val: f64) [12]u8 {
|
fn fmtEps(val: f64) [12]u8 {
|
||||||
|
|
@ -896,14 +869,14 @@ fn fmtEps(val: f64) [12]u8 {
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmdEtf(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool) !void {
|
fn cmdEtf(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||||
const result = svc.getEtfProfile(symbol) catch |err| switch (err) {
|
const result = svc.getEtfProfile(symbol) catch |err| switch (err) {
|
||||||
zfin.DataError.NoApiKey => {
|
zfin.DataError.NoApiKey => {
|
||||||
try stderr_print("Error: ALPHAVANTAGE_API_KEY not set. Get a free key at https://alphavantage.co\n");
|
try stderrPrint("Error: ALPHAVANTAGE_API_KEY not set. Get a free key at https://alphavantage.co\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
try stderr_print("Error fetching ETF profile.\n");
|
try stderrPrint("Error fetching ETF profile.\n");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -923,16 +896,12 @@ fn cmdEtf(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.source == .cached) try stderr_print("(using cached ETF profile)\n");
|
if (result.source == .cached) try stderrPrint("(using cached ETF profile)\n");
|
||||||
|
|
||||||
try printEtfProfile(profile, symbol, color);
|
try printEtfProfile(profile, symbol, color, out);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn printEtfProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool) !void {
|
fn printEtfProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||||
var buf: [16384]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
|
|
||||||
try setBold(out, color);
|
try setBold(out, color);
|
||||||
try out.print("\nETF Profile: {s}\n", .{symbol});
|
try out.print("\nETF Profile: {s}\n", .{symbol});
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
|
|
@ -955,7 +924,7 @@ fn printEtfProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool) !v
|
||||||
try out.print(" Inception Date: {s}\n", .{d.format(&db)});
|
try out.print(" Inception Date: {s}\n", .{d.format(&db)});
|
||||||
}
|
}
|
||||||
if (profile.leveraged) {
|
if (profile.leveraged) {
|
||||||
try setFg(out, color, CLR_RED);
|
try setFg(out, color, CLR_NEGATIVE);
|
||||||
try out.print(" Leveraged: YES\n", .{});
|
try out.print(" Leveraged: YES\n", .{});
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
}
|
}
|
||||||
|
|
@ -1002,27 +971,26 @@ fn printEtfProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool) !v
|
||||||
}
|
}
|
||||||
|
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
try out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataService, file_path: []const u8, watchlist_path: ?[]const u8, force_refresh: bool, color: bool) !void {
|
fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataService, file_path: []const u8, watchlist_path: ?[]const u8, force_refresh: bool, color: bool, out: *std.Io.Writer) !void {
|
||||||
// Load portfolio from SRF file
|
// Load portfolio from SRF file
|
||||||
const data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch |err| {
|
const data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch |err| {
|
||||||
try stderr_print("Error reading portfolio file: ");
|
try stderrPrint("Error reading portfolio file: ");
|
||||||
try stderr_print(@errorName(err));
|
try stderrPrint(@errorName(err));
|
||||||
try stderr_print("\n");
|
try stderrPrint("\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer allocator.free(data);
|
defer allocator.free(data);
|
||||||
|
|
||||||
var portfolio = zfin.cache.deserializePortfolio(allocator, data) catch {
|
var portfolio = zfin.cache.deserializePortfolio(allocator, data) catch {
|
||||||
try stderr_print("Error parsing portfolio file.\n");
|
try stderrPrint("Error parsing portfolio file.\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer portfolio.deinit();
|
defer portfolio.deinit();
|
||||||
|
|
||||||
if (portfolio.lots.len == 0) {
|
if (portfolio.lots.len == 0) {
|
||||||
try stderr_print("Portfolio is empty.\n");
|
try stderrPrint("Portfolio is empty.\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1059,7 +1027,7 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
|
||||||
|
|
||||||
if (all_syms_count > 0) {
|
if (all_syms_count > 0) {
|
||||||
if (config.twelvedata_key == null) {
|
if (config.twelvedata_key == null) {
|
||||||
try stderr_print("Warning: TWELVEDATA_API_KEY not set. Cannot fetch current prices.\n");
|
try stderrPrint("Warning: TWELVEDATA_API_KEY not set. Cannot fetch current prices.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
var loaded_count: usize = 0;
|
var loaded_count: usize = 0;
|
||||||
|
|
@ -1145,11 +1113,11 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
|
||||||
var msg_buf: [256]u8 = undefined;
|
var msg_buf: [256]u8 = undefined;
|
||||||
if (cached_count == all_syms_count) {
|
if (cached_count == all_syms_count) {
|
||||||
const msg = std.fmt.bufPrint(&msg_buf, "All {d} symbols loaded from cache\n", .{all_syms_count}) catch "Loaded from cache\n";
|
const msg = std.fmt.bufPrint(&msg_buf, "All {d} symbols loaded from cache\n", .{all_syms_count}) catch "Loaded from cache\n";
|
||||||
try stderr_print(msg);
|
try stderrPrint(msg);
|
||||||
} else {
|
} else {
|
||||||
const fetched_count = all_syms_count - cached_count - fail_count;
|
const fetched_count = all_syms_count - cached_count - fail_count;
|
||||||
const msg = std.fmt.bufPrint(&msg_buf, "Loaded {d} symbols ({d} cached, {d} fetched, {d} failed)\n", .{ all_syms_count, cached_count, fetched_count, fail_count }) catch "Done loading\n";
|
const msg = std.fmt.bufPrint(&msg_buf, "Loaded {d} symbols ({d} cached, {d} fetched, {d} failed)\n", .{ all_syms_count, cached_count, fetched_count, fail_count }) catch "Done loading\n";
|
||||||
try stderr_print(msg);
|
try stderrPrint(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1180,7 +1148,7 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
|
||||||
}
|
}
|
||||||
|
|
||||||
var summary = zfin.risk.portfolioSummary(allocator, positions, prices, manual_price_set) catch {
|
var summary = zfin.risk.portfolioSummary(allocator, positions, prices, manual_price_set) catch {
|
||||||
try stderr_print("Error computing portfolio summary.\n");
|
try stderrPrint("Error computing portfolio summary.\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer summary.deinit(allocator);
|
defer summary.deinit(allocator);
|
||||||
|
|
@ -1209,10 +1177,6 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf: [32768]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
|
|
||||||
// Header with summary
|
// Header with summary
|
||||||
try setBold(out, color);
|
try setBold(out, color);
|
||||||
try out.print("\nPortfolio Summary ({s})\n", .{file_path});
|
try out.print("\nPortfolio Summary ({s})\n", .{file_path});
|
||||||
|
|
@ -1337,16 +1301,19 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
|
||||||
date_col_len = written.len;
|
date_col_len = written.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (a.is_manual_price) try setFg(out, color, CLR_WARNING);
|
||||||
try out.print(" " ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} ", .{
|
try out.print(" " ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} ", .{
|
||||||
a.display_symbol, a.shares, fmt.fmtMoney2(&cost_buf2, a.avg_cost),
|
a.display_symbol, a.shares, fmt.fmtMoney2(&cost_buf2, a.avg_cost),
|
||||||
});
|
});
|
||||||
if (a.is_manual_price) try setFg(out, color, CLR_YELLOW);
|
|
||||||
try out.print("{s:>10}", .{fmt.fmtMoney2(&price_buf2, a.current_price)});
|
try out.print("{s:>10}", .{fmt.fmtMoney2(&price_buf2, a.current_price)});
|
||||||
if (a.is_manual_price) try reset(out, color);
|
|
||||||
try out.print(" {s:>16} ", .{fmt.fmtMoney(&mv_buf, a.market_value)});
|
try out.print(" {s:>16} ", .{fmt.fmtMoney(&mv_buf, a.market_value)});
|
||||||
try setGainLoss(out, color, a.unrealized_pnl);
|
try setGainLoss(out, color, a.unrealized_pnl);
|
||||||
try out.print("{s}{s:>13}", .{ sign, gl_money });
|
try out.print("{s}{s:>13}", .{ sign, gl_money });
|
||||||
try reset(out, color);
|
if (a.is_manual_price) {
|
||||||
|
try setFg(out, color, CLR_WARNING);
|
||||||
|
} else {
|
||||||
|
try reset(out, color);
|
||||||
|
}
|
||||||
try out.print(" {d:>7.1}%", .{a.weight * 100.0});
|
try out.print(" {d:>7.1}%", .{a.weight * 100.0});
|
||||||
if (date_col_len > 0) {
|
if (date_col_len > 0) {
|
||||||
try out.print(" {s}", .{date_col[0..date_col_len]});
|
try out.print(" {s}", .{date_col[0..date_col_len]});
|
||||||
|
|
@ -1357,6 +1324,7 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
|
||||||
try out.print(" {s}", .{acct});
|
try out.print(" {s}", .{acct});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (a.is_manual_price) try reset(out, color);
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1365,19 +1333,22 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
|
||||||
// Check if any lots are DRIP
|
// Check if any lots are DRIP
|
||||||
var has_drip = false;
|
var has_drip = false;
|
||||||
for (lots_for_sym.items) |lot| {
|
for (lots_for_sym.items) |lot| {
|
||||||
if (lot.drip) { has_drip = true; break; }
|
if (lot.drip) {
|
||||||
|
has_drip = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!has_drip) {
|
if (!has_drip) {
|
||||||
// No DRIP: show all individually
|
// No DRIP: show all individually
|
||||||
for (lots_for_sym.items) |lot| {
|
for (lots_for_sym.items) |lot| {
|
||||||
try printCliLotRow(out, color, lot, a.current_price);
|
try printLotRow(out, color, lot, a.current_price);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Show non-DRIP lots individually
|
// Show non-DRIP lots individually
|
||||||
for (lots_for_sym.items) |lot| {
|
for (lots_for_sym.items) |lot| {
|
||||||
if (!lot.drip) {
|
if (!lot.drip) {
|
||||||
try printCliLotRow(out, color, lot, a.current_price);
|
try printLotRow(out, color, lot, a.current_price);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1673,7 +1644,7 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
|
||||||
|
|
||||||
// Helper to render a watch symbol
|
// Helper to render a watch symbol
|
||||||
const renderWatch = struct {
|
const renderWatch = struct {
|
||||||
fn f(o: anytype, c: bool, s: *zfin.DataService, a2: std.mem.Allocator, sym: []const u8, any: *bool) !void {
|
fn f(o: *std.Io.Writer, c: bool, s: *zfin.DataService, a2: std.mem.Allocator, sym: []const u8, any: *bool) !void {
|
||||||
if (!any.*) {
|
if (!any.*) {
|
||||||
try o.print("\n", .{});
|
try o.print("\n", .{});
|
||||||
try setBold(o, c);
|
try setBold(o, c);
|
||||||
|
|
@ -1752,7 +1723,7 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
|
||||||
try out.print(" {s:>6} {d:>9.1}% {d:>8.2} ", .{
|
try out.print(" {s:>6} {d:>9.1}% {d:>8.2} ", .{
|
||||||
a.symbol, metrics.volatility * 100.0, metrics.sharpe,
|
a.symbol, metrics.volatility * 100.0, metrics.sharpe,
|
||||||
});
|
});
|
||||||
try setFg(out, color, CLR_RED);
|
try setFg(out, color, CLR_NEGATIVE);
|
||||||
try out.print("{d:>9.1}%", .{metrics.max_drawdown * 100.0});
|
try out.print("{d:>9.1}%", .{metrics.max_drawdown * 100.0});
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
if (metrics.drawdown_trough) |dt| {
|
if (metrics.drawdown_trough) |dt| {
|
||||||
|
|
@ -1768,10 +1739,9 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
|
||||||
}
|
}
|
||||||
|
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
try out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn printCliLotRow(out: anytype, color: bool, lot: zfin.Lot, current_price: f64) !void {
|
fn printLotRow(out: *std.Io.Writer, color: bool, lot: zfin.Lot, current_price: f64) !void {
|
||||||
var lot_price_buf: [24]u8 = undefined;
|
var lot_price_buf: [24]u8 = undefined;
|
||||||
var lot_date_buf: [10]u8 = undefined;
|
var lot_date_buf: [10]u8 = undefined;
|
||||||
const date_str = lot.open_date.format(&lot_date_buf);
|
const date_str = lot.open_date.format(&lot_date_buf);
|
||||||
|
|
@ -1799,22 +1769,18 @@ fn printCliLotRow(out: anytype, color: bool, lot: zfin.Lot, current_price: f64)
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmdLookup(allocator: std.mem.Allocator, svc: *zfin.DataService, cusip: []const u8, color: bool) !void {
|
fn cmdLookup(allocator: std.mem.Allocator, svc: *zfin.DataService, cusip: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||||
var buf: [4096]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
|
|
||||||
if (!zfin.OpenFigi.isCusipLike(cusip)) {
|
if (!zfin.OpenFigi.isCusipLike(cusip)) {
|
||||||
try setFg(out, color, CLR_MUTED);
|
try setFg(out, color, CLR_MUTED);
|
||||||
try out.print("Note: '{s}' doesn't look like a CUSIP (expected 9 alphanumeric chars with digits)\n", .{cusip});
|
try out.print("Note: '{s}' doesn't look like a CUSIP (expected 9 alphanumeric chars with digits)\n", .{cusip});
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
try stderr_print("Looking up via OpenFIGI...\n");
|
try stderrPrint("Looking up via OpenFIGI...\n");
|
||||||
|
|
||||||
// Try full batch lookup for richer output
|
// Try full batch lookup for richer output
|
||||||
const results = zfin.OpenFigi.lookupCusips(allocator, &.{cusip}, svc.config.openfigi_key) catch {
|
const results = zfin.OpenFigi.lookupCusips(allocator, &.{cusip}, svc.config.openfigi_key) catch {
|
||||||
try stderr_print("Error: OpenFIGI request failed (network error)\n");
|
try stderrPrint("Error: OpenFIGI request failed (network error)\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer {
|
defer {
|
||||||
|
|
@ -1828,7 +1794,6 @@ fn cmdLookup(allocator: std.mem.Allocator, svc: *zfin.DataService, cusip: []cons
|
||||||
|
|
||||||
if (results.len == 0 or !results[0].found) {
|
if (results.len == 0 or !results[0].found) {
|
||||||
try out.print("No result from OpenFIGI for '{s}'\n", .{cusip});
|
try out.print("No result from OpenFIGI for '{s}'\n", .{cusip});
|
||||||
try out.flush();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1868,24 +1833,17 @@ fn cmdLookup(allocator: std.mem.Allocator, svc: *zfin.DataService, cusip: []cons
|
||||||
try out.print("\n Tip: For mutual funds, OpenFIGI often has no coverage.\n", .{});
|
try out.print("\n Tip: For mutual funds, OpenFIGI often has no coverage.\n", .{});
|
||||||
try out.print(" Add manually: symbol::{s},ticker::XXXX,...\n", .{cusip});
|
try out.print(" Add manually: symbol::{s},ticker::XXXX,...\n", .{cusip});
|
||||||
}
|
}
|
||||||
|
|
||||||
try out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmdCache(allocator: std.mem.Allocator, config: zfin.Config, subcommand: []const u8) !void {
|
fn cmdCache(allocator: std.mem.Allocator, config: zfin.Config, subcommand: []const u8, out: *std.Io.Writer) !void {
|
||||||
if (std.mem.eql(u8, subcommand, "stats")) {
|
if (std.mem.eql(u8, subcommand, "stats")) {
|
||||||
var buf: [4096]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
try out.print("Cache directory: {s}\n", .{config.cache_dir});
|
try out.print("Cache directory: {s}\n", .{config.cache_dir});
|
||||||
std.fs.cwd().access(config.cache_dir, .{}) catch {
|
std.fs.cwd().access(config.cache_dir, .{}) catch {
|
||||||
try out.print(" (empty -- no cached data)\n", .{});
|
try out.print(" (empty -- no cached data)\n", .{});
|
||||||
try out.flush();
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
var dir = std.fs.cwd().openDir(config.cache_dir, .{ .iterate = true }) catch {
|
var dir = std.fs.cwd().openDir(config.cache_dir, .{ .iterate = true }) catch {
|
||||||
try out.print(" (empty -- no cached data)\n", .{});
|
try out.print(" (empty -- no cached data)\n", .{});
|
||||||
try out.flush();
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer dir.close();
|
defer dir.close();
|
||||||
|
|
@ -1902,29 +1860,28 @@ fn cmdCache(allocator: std.mem.Allocator, config: zfin.Config, subcommand: []con
|
||||||
} else {
|
} else {
|
||||||
try out.print("\n {d} symbol(s) cached\n", .{count});
|
try out.print("\n {d} symbol(s) cached\n", .{count});
|
||||||
}
|
}
|
||||||
try out.flush();
|
|
||||||
} else if (std.mem.eql(u8, subcommand, "clear")) {
|
} else if (std.mem.eql(u8, subcommand, "clear")) {
|
||||||
var store = zfin.cache.Store.init(allocator, config.cache_dir);
|
var store = zfin.cache.Store.init(allocator, config.cache_dir);
|
||||||
try store.clearAll();
|
try store.clearAll();
|
||||||
try stdout_print("Cache cleared.\n");
|
try out.writeAll("Cache cleared.\n");
|
||||||
} else {
|
} else {
|
||||||
try stderr_print("Unknown cache subcommand. Use 'stats' or 'clear'.\n");
|
try stderrPrint("Unknown cache subcommand. Use 'stats' or 'clear'.\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CLI `analysis` command: show portfolio analysis breakdowns.
|
/// CLI `analysis` command: show portfolio analysis breakdowns.
|
||||||
fn cmdAnalysis(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataService, file_path: []const u8, color: bool) !void {
|
fn cmdAnalysis(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataService, file_path: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||||
_ = config;
|
_ = config;
|
||||||
|
|
||||||
// Load portfolio
|
// Load portfolio
|
||||||
const file_data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch {
|
const file_data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch {
|
||||||
try stderr_print("Error: Cannot read portfolio file\n");
|
try stderrPrint("Error: Cannot read portfolio file\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer allocator.free(file_data);
|
defer allocator.free(file_data);
|
||||||
|
|
||||||
var portfolio = zfin.cache.deserializePortfolio(allocator, file_data) catch {
|
var portfolio = zfin.cache.deserializePortfolio(allocator, file_data) catch {
|
||||||
try stderr_print("Error: Cannot parse portfolio file\n");
|
try stderrPrint("Error: Cannot parse portfolio file\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer portfolio.deinit();
|
defer portfolio.deinit();
|
||||||
|
|
@ -1969,7 +1926,7 @@ fn cmdAnalysis(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Dat
|
||||||
}
|
}
|
||||||
|
|
||||||
var summary = zfin.risk.portfolioSummary(allocator, positions, prices, manual_price_set) catch {
|
var summary = zfin.risk.portfolioSummary(allocator, positions, prices, manual_price_set) catch {
|
||||||
try stderr_print("Error computing portfolio summary.\n");
|
try stderrPrint("Error computing portfolio summary.\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer summary.deinit(allocator);
|
defer summary.deinit(allocator);
|
||||||
|
|
@ -1987,13 +1944,13 @@ fn cmdAnalysis(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Dat
|
||||||
defer allocator.free(meta_path);
|
defer allocator.free(meta_path);
|
||||||
|
|
||||||
const meta_data = std.fs.cwd().readFileAlloc(allocator, meta_path, 1024 * 1024) catch {
|
const meta_data = std.fs.cwd().readFileAlloc(allocator, meta_path, 1024 * 1024) catch {
|
||||||
try stderr_print("Error: No metadata.srf found. Run: zfin enrich <portfolio.srf> > metadata.srf\n");
|
try stderrPrint("Error: No metadata.srf found. Run: zfin enrich <portfolio.srf> > metadata.srf\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer allocator.free(meta_data);
|
defer allocator.free(meta_data);
|
||||||
|
|
||||||
var cm = zfin.classification.parseClassificationFile(allocator, meta_data) catch {
|
var cm = zfin.classification.parseClassificationFile(allocator, meta_data) catch {
|
||||||
try stderr_print("Error: Cannot parse metadata.srf\n");
|
try stderrPrint("Error: Cannot parse metadata.srf\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer cm.deinit();
|
defer cm.deinit();
|
||||||
|
|
@ -2018,16 +1975,12 @@ fn cmdAnalysis(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Dat
|
||||||
summary.total_value,
|
summary.total_value,
|
||||||
acct_map_opt,
|
acct_map_opt,
|
||||||
) catch {
|
) catch {
|
||||||
try stderr_print("Error computing analysis.\n");
|
try stderrPrint("Error computing analysis.\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer result.deinit(allocator);
|
defer result.deinit(allocator);
|
||||||
|
|
||||||
// Output
|
// Output
|
||||||
var buf: [32768]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
|
|
||||||
const label_width: usize = 24;
|
const label_width: usize = 24;
|
||||||
const bar_width: usize = 30;
|
const bar_width: usize = 30;
|
||||||
|
|
||||||
|
|
@ -2086,7 +2039,7 @@ fn cmdAnalysis(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Dat
|
||||||
// Unclassified
|
// Unclassified
|
||||||
if (result.unclassified.len > 0) {
|
if (result.unclassified.len > 0) {
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
try setFg(out, color, CLR_YELLOW);
|
try setFg(out, color, CLR_WARNING);
|
||||||
try out.print(" Unclassified (not in metadata.srf)\n", .{});
|
try out.print(" Unclassified (not in metadata.srf)\n", .{});
|
||||||
try reset(out, color);
|
try reset(out, color);
|
||||||
for (result.unclassified) |sym| {
|
for (result.unclassified) |sym| {
|
||||||
|
|
@ -2097,11 +2050,10 @@ fn cmdAnalysis(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Dat
|
||||||
}
|
}
|
||||||
|
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
try out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print a breakdown section with block-element bar charts to the CLI output.
|
/// Print a breakdown section with block-element bar charts to the CLI output.
|
||||||
fn printBreakdownSection(out: anytype, items: []const zfin.analysis.BreakdownItem, label_width: usize, bar_width: usize, color: bool) !void {
|
fn printBreakdownSection(out: *std.Io.Writer, items: []const zfin.analysis.BreakdownItem, label_width: usize, bar_width: usize, color: bool) !void {
|
||||||
// Unicode block elements: U+2588 full, U+2589..U+258F partials (7/8..1/8)
|
// Unicode block elements: U+2588 full, U+2589..U+258F partials (7/8..1/8)
|
||||||
const full_block = "\xE2\x96\x88";
|
const full_block = "\xE2\x96\x88";
|
||||||
// partial_blocks[0]=7/8, [1]=3/4, ..., [6]=1/8
|
// partial_blocks[0]=7/8, [1]=3/4, ..., [6]=1/8
|
||||||
|
|
@ -2150,10 +2102,10 @@ fn printBreakdownSection(out: anytype, items: []const zfin.analysis.BreakdownIte
|
||||||
/// Reads the portfolio, extracts stock symbols, fetches sector/industry/country for each,
|
/// Reads the portfolio, extracts stock symbols, fetches sector/industry/country for each,
|
||||||
/// and outputs a metadata SRF file to stdout.
|
/// and outputs a metadata SRF file to stdout.
|
||||||
/// If the argument looks like a symbol (no path separators, no .srf extension), enrich just that symbol.
|
/// If the argument looks like a symbol (no path separators, no .srf extension), enrich just that symbol.
|
||||||
fn cmdEnrich(allocator: std.mem.Allocator, config: zfin.Config, arg: []const u8) !void {
|
fn cmdEnrich(allocator: std.mem.Allocator, config: zfin.Config, arg: []const u8, out: *std.Io.Writer) !void {
|
||||||
// Check for Alpha Vantage API key
|
// Check for Alpha Vantage API key
|
||||||
const av_key = config.alphavantage_key orelse {
|
const av_key = config.alphavantage_key orelse {
|
||||||
try stderr_print("Error: ALPHAVANTAGE_API_KEY not set. Add it to .env\n");
|
try stderrPrint("Error: ALPHAVANTAGE_API_KEY not set. Add it to .env\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2164,35 +2116,30 @@ fn cmdEnrich(allocator: std.mem.Allocator, config: zfin.Config, arg: []const u8)
|
||||||
|
|
||||||
if (!is_file) {
|
if (!is_file) {
|
||||||
// Single symbol mode: enrich one symbol, output appendable SRF (no header)
|
// Single symbol mode: enrich one symbol, output appendable SRF (no header)
|
||||||
try cmdEnrichSymbol(allocator, av_key, arg);
|
try cmdEnrichSymbol(allocator, av_key, arg, out);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Portfolio file mode: enrich all symbols
|
// Portfolio file mode: enrich all symbols
|
||||||
try cmdEnrichPortfolio(allocator, config, av_key, arg);
|
try cmdEnrichPortfolio(allocator, config, av_key, arg, out);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enrich a single symbol and output appendable SRF lines to stdout.
|
/// Enrich a single symbol and output appendable SRF lines to stdout.
|
||||||
fn cmdEnrichSymbol(allocator: std.mem.Allocator, av_key: []const u8, sym: []const u8) !void {
|
fn cmdEnrichSymbol(allocator: std.mem.Allocator, av_key: []const u8, sym: []const u8, out: *std.Io.Writer) !void {
|
||||||
const AV = @import("zfin").AlphaVantage;
|
const AV = @import("zfin").AlphaVantage;
|
||||||
var av = AV.init(allocator, av_key);
|
var av = AV.init(allocator, av_key);
|
||||||
defer av.deinit();
|
defer av.deinit();
|
||||||
|
|
||||||
var buf: [32768]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
|
|
||||||
{
|
{
|
||||||
var msg_buf: [128]u8 = undefined;
|
var msg_buf: [128]u8 = undefined;
|
||||||
const msg = std.fmt.bufPrint(&msg_buf, " Fetching {s}...\n", .{sym}) catch " ...\n";
|
const msg = std.fmt.bufPrint(&msg_buf, " Fetching {s}...\n", .{sym}) catch " ...\n";
|
||||||
try stderr_print(msg);
|
try stderrPrint(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
const overview = av.fetchCompanyOverview(allocator, sym) catch {
|
const overview = av.fetchCompanyOverview(allocator, sym) catch {
|
||||||
try stderr_print("Error: Failed to fetch data for symbol\n");
|
try stderrPrint("Error: Failed to fetch data for symbol\n");
|
||||||
try out.print("# {s} -- fetch failed\n", .{sym});
|
try out.print("# {s} -- fetch failed\n", .{sym});
|
||||||
try out.print("# symbol::{s},sector::TODO,geo::TODO,asset_class::TODO\n", .{sym});
|
try out.print("# symbol::{s},sector::TODO,geo::TODO,asset_class::TODO\n", .{sym});
|
||||||
try out.flush();
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer {
|
defer {
|
||||||
|
|
@ -2228,23 +2175,22 @@ fn cmdEnrichSymbol(allocator: std.mem.Allocator, av_key: []const u8, sym: []cons
|
||||||
try out.print("symbol::{s},sector::{s},geo::{s},asset_class::{s}\n", .{
|
try out.print("symbol::{s},sector::{s},geo::{s},asset_class::{s}\n", .{
|
||||||
sym, sector_str, geo_str, asset_class_str,
|
sym, sector_str, geo_str, asset_class_str,
|
||||||
});
|
});
|
||||||
try out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enrich all symbols from a portfolio file.
|
/// Enrich all symbols from a portfolio file.
|
||||||
fn cmdEnrichPortfolio(allocator: std.mem.Allocator, config: zfin.Config, av_key: []const u8, file_path: []const u8) !void {
|
fn cmdEnrichPortfolio(allocator: std.mem.Allocator, config: zfin.Config, av_key: []const u8, file_path: []const u8, out: *std.Io.Writer) !void {
|
||||||
const AV = @import("zfin").AlphaVantage;
|
const AV = @import("zfin").AlphaVantage;
|
||||||
_ = config;
|
_ = config;
|
||||||
|
|
||||||
// Load portfolio
|
// Load portfolio
|
||||||
const file_data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch {
|
const file_data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch {
|
||||||
try stderr_print("Error: Cannot read portfolio file\n");
|
try stderrPrint("Error: Cannot read portfolio file\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer allocator.free(file_data);
|
defer allocator.free(file_data);
|
||||||
|
|
||||||
var portfolio = zfin.cache.deserializePortfolio(allocator, file_data) catch {
|
var portfolio = zfin.cache.deserializePortfolio(allocator, file_data) catch {
|
||||||
try stderr_print("Error: Cannot parse portfolio file\n");
|
try stderrPrint("Error: Cannot parse portfolio file\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer portfolio.deinit();
|
defer portfolio.deinit();
|
||||||
|
|
@ -2260,10 +2206,6 @@ fn cmdEnrichPortfolio(allocator: std.mem.Allocator, config: zfin.Config, av_key:
|
||||||
var av = AV.init(allocator, av_key);
|
var av = AV.init(allocator, av_key);
|
||||||
defer av.deinit();
|
defer av.deinit();
|
||||||
|
|
||||||
var buf: [32768]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
|
|
||||||
try out.print("#!srfv1\n", .{});
|
try out.print("#!srfv1\n", .{});
|
||||||
try out.print("# Portfolio classification metadata\n", .{});
|
try out.print("# Portfolio classification metadata\n", .{});
|
||||||
try out.print("# Generated from Alpha Vantage OVERVIEW data\n", .{});
|
try out.print("# Generated from Alpha Vantage OVERVIEW data\n", .{});
|
||||||
|
|
@ -2303,7 +2245,7 @@ fn cmdEnrichPortfolio(allocator: std.mem.Allocator, config: zfin.Config, av_key:
|
||||||
{
|
{
|
||||||
var msg_buf: [128]u8 = undefined;
|
var msg_buf: [128]u8 = undefined;
|
||||||
const msg = std.fmt.bufPrint(&msg_buf, " [{d}/{d}] {s}...\n", .{ i + 1, syms.len, sym }) catch " ...\n";
|
const msg = std.fmt.bufPrint(&msg_buf, " [{d}/{d}] {s}...\n", .{ i + 1, syms.len, sym }) catch " ...\n";
|
||||||
try stderr_print(msg);
|
try stderrPrint(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
const overview = av.fetchCompanyOverview(allocator, sym) catch {
|
const overview = av.fetchCompanyOverview(allocator, sym) catch {
|
||||||
|
|
@ -2358,19 +2300,10 @@ fn cmdEnrichPortfolio(allocator: std.mem.Allocator, config: zfin.Config, av_key:
|
||||||
syms.len, success, skipped, failed,
|
syms.len, success, skipped, failed,
|
||||||
});
|
});
|
||||||
try out.print("# Review and edit this file, then save as metadata.srf\n", .{});
|
try out.print("# Review and edit this file, then save as metadata.srf\n", .{});
|
||||||
try out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Output helpers ───────────────────────────────────────────
|
// ── Output helpers ───────────────────────────────────────────
|
||||||
|
|
||||||
fn stdout_print(msg: []const u8) !void {
|
|
||||||
var buf: [4096]u8 = undefined;
|
|
||||||
var writer = std.fs.File.stdout().writer(&buf);
|
|
||||||
const out = &writer.interface;
|
|
||||||
try out.writeAll(msg);
|
|
||||||
try out.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Print progress line to stderr: " [N/M] SYMBOL (status)"
|
/// Print progress line to stderr: " [N/M] SYMBOL (status)"
|
||||||
fn stderrProgress(symbol: []const u8, status: []const u8, current: usize, total: usize, color: bool) !void {
|
fn stderrProgress(symbol: []const u8, status: []const u8, current: usize, total: usize, color: bool) !void {
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
|
|
@ -2391,7 +2324,7 @@ fn stderrRateLimitWait(wait_seconds: u64, color: bool) !void {
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
var writer = std.fs.File.stderr().writer(&buf);
|
var writer = std.fs.File.stderr().writer(&buf);
|
||||||
const out = &writer.interface;
|
const out = &writer.interface;
|
||||||
if (color) try fmt.ansiSetFg(out, CLR_RED[0], CLR_RED[1], CLR_RED[2]);
|
if (color) try fmt.ansiSetFg(out, CLR_NEGATIVE[0], CLR_NEGATIVE[1], CLR_NEGATIVE[2]);
|
||||||
if (wait_seconds >= 60) {
|
if (wait_seconds >= 60) {
|
||||||
const mins = wait_seconds / 60;
|
const mins = wait_seconds / 60;
|
||||||
const secs = wait_seconds % 60;
|
const secs = wait_seconds % 60;
|
||||||
|
|
@ -2407,7 +2340,7 @@ fn stderrRateLimitWait(wait_seconds: u64, color: bool) !void {
|
||||||
try out.flush();
|
try out.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stderr_print(msg: []const u8) !void {
|
fn stderrPrint(msg: []const u8) !void {
|
||||||
var buf: [1024]u8 = undefined;
|
var buf: [1024]u8 = undefined;
|
||||||
var writer = std.fs.File.stderr().writer(&buf);
|
var writer = std.fs.File.stderr().writer(&buf);
|
||||||
const out = &writer.interface;
|
const out = &writer.interface;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue