root.zig review
This commit is contained in:
parent
3e24d37040
commit
f936531721
7 changed files with 135 additions and 97 deletions
|
|
@ -3,7 +3,7 @@ const zfin = @import("../root.zig");
|
|||
const cli = @import("common.zig");
|
||||
const fmt = cli.fmt;
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, config: zfin.Config, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||
const result = svc.getDividends(symbol) catch |err| switch (err) {
|
||||
zfin.DataError.NoApiKey => {
|
||||
try cli.stderrPrint("Error: POLYGON_API_KEY not set. Get a free key at https://polygon.io\n");
|
||||
|
|
@ -18,21 +18,11 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, config: zfin.Co
|
|||
|
||||
if (result.source == .cached) try cli.stderrPrint("(using cached dividend data)\n");
|
||||
|
||||
// Fetch current price for yield calculation
|
||||
// Fetch current price for yield calculation via DataService
|
||||
var current_price: ?f64 = null;
|
||||
if (config.twelvedata_key) |td_key| {
|
||||
var td = zfin.TwelveData.init(allocator, td_key);
|
||||
defer td.deinit();
|
||||
if (td.fetchQuote(allocator, symbol)) |qr_val| {
|
||||
var qr = qr_val;
|
||||
defer qr.deinit();
|
||||
if (qr.parse(allocator)) |q_val| {
|
||||
var q = q_val;
|
||||
defer q.deinit();
|
||||
current_price = q.close();
|
||||
if (svc.getQuote(symbol)) |q| {
|
||||
current_price = q.close;
|
||||
} else |_| {}
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
try display(result.data, symbol, current_price, color, out);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,7 @@ const cli = @import("common.zig");
|
|||
/// Reads the portfolio, extracts stock symbols, fetches sector/industry/country for each,
|
||||
/// 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.
|
||||
pub fn run(allocator: std.mem.Allocator, config: zfin.Config, arg: []const u8, out: *std.Io.Writer) !void {
|
||||
// Check for Alpha Vantage API key
|
||||
const av_key = config.alphavantage_key orelse {
|
||||
try cli.stderrPrint("Error: ALPHAVANTAGE_API_KEY not set. Add it to .env\n");
|
||||
return;
|
||||
};
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, arg: []const u8, out: *std.Io.Writer) !void {
|
||||
// Determine if arg is a symbol or a file path
|
||||
const is_file = std.mem.endsWith(u8, arg, ".srf") or
|
||||
std.mem.indexOfScalar(u8, arg, '/') != null or
|
||||
|
|
@ -20,27 +14,27 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, arg: []const u8, o
|
|||
|
||||
if (!is_file) {
|
||||
// Single symbol mode: enrich one symbol, output appendable SRF (no header)
|
||||
try enrichSymbol(allocator, av_key, arg, out);
|
||||
try enrichSymbol(allocator, svc, arg, out);
|
||||
return;
|
||||
}
|
||||
|
||||
// Portfolio file mode: enrich all symbols
|
||||
try enrichPortfolio(allocator, av_key, arg, out);
|
||||
try enrichPortfolio(allocator, svc, arg, out);
|
||||
}
|
||||
|
||||
/// Enrich a single symbol and output appendable SRF lines to stdout.
|
||||
fn enrichSymbol(allocator: std.mem.Allocator, av_key: []const u8, sym: []const u8, out: *std.Io.Writer) !void {
|
||||
const AV = zfin.AlphaVantage;
|
||||
var av = AV.init(allocator, av_key);
|
||||
defer av.deinit();
|
||||
|
||||
fn enrichSymbol(allocator: std.mem.Allocator, svc: *zfin.DataService, sym: []const u8, out: *std.Io.Writer) !void {
|
||||
{
|
||||
var msg_buf: [128]u8 = undefined;
|
||||
const msg = std.fmt.bufPrint(&msg_buf, " Fetching {s}...\n", .{sym}) catch " ...\n";
|
||||
try cli.stderrPrint(msg);
|
||||
}
|
||||
|
||||
const overview = av.fetchCompanyOverview(allocator, sym) catch {
|
||||
const overview = svc.getCompanyOverview(sym) catch |err| {
|
||||
if (err == zfin.DataError.NoApiKey) {
|
||||
try cli.stderrPrint("Error: ALPHAVANTAGE_API_KEY not set. Add it to .env\n");
|
||||
return;
|
||||
}
|
||||
try cli.stderrPrint("Error: Failed to fetch data for symbol\n");
|
||||
try out.print("# {s} -- fetch failed\n", .{sym});
|
||||
try out.print("# symbol::{s},sector::TODO,geo::TODO,asset_class::TODO\n", .{sym});
|
||||
|
|
@ -84,8 +78,7 @@ fn enrichSymbol(allocator: std.mem.Allocator, av_key: []const u8, sym: []const u
|
|||
}
|
||||
|
||||
/// Enrich all symbols from a portfolio file.
|
||||
fn enrichPortfolio(allocator: std.mem.Allocator, av_key: []const u8, file_path: []const u8, out: *std.Io.Writer) !void {
|
||||
const AV = zfin.AlphaVantage;
|
||||
fn enrichPortfolio(allocator: std.mem.Allocator, svc: *zfin.DataService, file_path: []const u8, out: *std.Io.Writer) !void {
|
||||
|
||||
// Load portfolio
|
||||
const file_data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch {
|
||||
|
|
@ -108,9 +101,6 @@ fn enrichPortfolio(allocator: std.mem.Allocator, av_key: []const u8, file_path:
|
|||
const syms = try portfolio.stockSymbols(allocator);
|
||||
defer allocator.free(syms);
|
||||
|
||||
var av = AV.init(allocator, av_key);
|
||||
defer av.deinit();
|
||||
|
||||
try out.print("#!srfv1\n", .{});
|
||||
try out.print("# Portfolio classification metadata\n", .{});
|
||||
try out.print("# Generated from Alpha Vantage OVERVIEW data\n", .{});
|
||||
|
|
@ -152,7 +142,7 @@ fn enrichPortfolio(allocator: std.mem.Allocator, av_key: []const u8, file_path:
|
|||
try cli.stderrPrint(msg);
|
||||
}
|
||||
|
||||
const overview = av.fetchCompanyOverview(allocator, sym) catch {
|
||||
const overview = svc.getCompanyOverview(sym) catch {
|
||||
try out.print("# {s} -- fetch failed\n", .{sym});
|
||||
try out.print("# symbol::{s},sector::TODO,geo::TODO,asset_class::TODO\n\n", .{sym});
|
||||
failed += 1;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, cusip: []const
|
|||
try cli.stderrPrint("Looking up via OpenFIGI...\n");
|
||||
|
||||
// Try full batch lookup for richer output
|
||||
const results = zfin.OpenFigi.lookupCusips(allocator, &.{cusip}, svc.config.openfigi_key) catch {
|
||||
const results = svc.lookupCusips(&.{cusip}) catch {
|
||||
try cli.stderrPrint("Error: OpenFIGI request failed (network error)\n");
|
||||
return;
|
||||
};
|
||||
|
|
@ -38,7 +38,7 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, cusip: []const
|
|||
}
|
||||
}
|
||||
|
||||
pub fn display(result: zfin.OpenFigi.FigiResult, cusip: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||
pub fn display(result: zfin.CusipResult, cusip: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||
if (result.ticker) |ticker| {
|
||||
try cli.setBold(out, color);
|
||||
try out.print("{s}", .{cusip});
|
||||
|
|
@ -78,7 +78,7 @@ pub fn display(result: zfin.OpenFigi.FigiResult, cusip: []const u8, color: bool,
|
|||
test "display shows ticker mapping" {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
const result: zfin.OpenFigi.FigiResult = .{
|
||||
const result: zfin.CusipResult = .{
|
||||
.ticker = "AAPL",
|
||||
.name = "Apple Inc",
|
||||
.security_type = "Common Stock",
|
||||
|
|
@ -96,7 +96,7 @@ test "display shows ticker mapping" {
|
|||
test "display shows no-ticker message" {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
const result: zfin.OpenFigi.FigiResult = .{
|
||||
const result: zfin.CusipResult = .{
|
||||
.ticker = null,
|
||||
.name = "Some Fund",
|
||||
.security_type = null,
|
||||
|
|
@ -112,7 +112,7 @@ test "display shows no-ticker message" {
|
|||
test "display no ANSI without color" {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
const result: zfin.OpenFigi.FigiResult = .{
|
||||
const result: zfin.CusipResult = .{
|
||||
.ticker = "MSFT",
|
||||
.name = null,
|
||||
.security_type = null,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ pub const QuoteData = struct {
|
|||
date: zfin.Date,
|
||||
};
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
|
||||
// Fetch candle data for chart and history
|
||||
const candle_result = svc.getCandles(symbol) catch |err| switch (err) {
|
||||
zfin.DataError.NoApiKey => {
|
||||
|
|
@ -29,30 +29,19 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
|
|||
defer allocator.free(candle_result.data);
|
||||
const candles = candle_result.data;
|
||||
|
||||
// Fetch real-time quote
|
||||
// Fetch real-time quote via DataService
|
||||
var quote: ?QuoteData = null;
|
||||
|
||||
if (config.twelvedata_key) |key| {
|
||||
var td = zfin.TwelveData.init(allocator, key);
|
||||
defer td.deinit();
|
||||
if (td.fetchQuote(allocator, symbol)) |qr_val| {
|
||||
var qr = qr_val;
|
||||
defer qr.deinit();
|
||||
if (qr.parse(allocator)) |q_val| {
|
||||
var q = q_val;
|
||||
defer q.deinit();
|
||||
if (svc.getQuote(symbol)) |q| {
|
||||
quote = .{
|
||||
.price = q.close(),
|
||||
.open = q.open(),
|
||||
.high = q.high(),
|
||||
.low = q.low(),
|
||||
.volume = q.volume(),
|
||||
.prev_close = q.previous_close(),
|
||||
.price = q.close,
|
||||
.open = q.open,
|
||||
.high = q.high,
|
||||
.low = q.low,
|
||||
.volume = q.volume,
|
||||
.prev_close = q.previous_close,
|
||||
.date = if (candles.len > 0) candles[candles.len - 1].date else fmt.todayDate(),
|
||||
};
|
||||
} else |_| {}
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
try display(allocator, candles, quote, symbol, color, out);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ pub fn main() !u8 {
|
|||
try cli.stderrPrint("Error: 'quote' requires a symbol argument\n");
|
||||
return 1;
|
||||
}
|
||||
try commands.quote.run(allocator, config, &svc, args[2], color, out);
|
||||
try commands.quote.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "history")) {
|
||||
if (args.len < 3) {
|
||||
try cli.stderrPrint("Error: 'history' requires a symbol argument\n");
|
||||
|
|
@ -126,7 +126,7 @@ pub fn main() !u8 {
|
|||
try cli.stderrPrint("Error: 'divs' requires a symbol argument\n");
|
||||
return 1;
|
||||
}
|
||||
try commands.divs.run(allocator, &svc, config, args[2], color, out);
|
||||
try commands.divs.run(allocator, &svc, args[2], color, out);
|
||||
} else if (std.mem.eql(u8, command, "splits")) {
|
||||
if (args.len < 3) {
|
||||
try cli.stderrPrint("Error: 'splits' requires a symbol argument\n");
|
||||
|
|
@ -196,7 +196,7 @@ pub fn main() !u8 {
|
|||
try cli.stderrPrint("Error: 'enrich' requires a portfolio file path or symbol\n");
|
||||
return 1;
|
||||
}
|
||||
try commands.enrich.run(allocator, config, args[2], out);
|
||||
try commands.enrich.run(allocator, &svc, args[2], out);
|
||||
} else if (std.mem.eql(u8, command, "analysis")) {
|
||||
// File path is first non-flag arg (default: portfolio.srf)
|
||||
var analysis_file: []const u8 = "portfolio.srf";
|
||||
|
|
|
|||
104
src/root.zig
104
src/root.zig
|
|
@ -3,62 +3,108 @@
|
|||
//! Fetches, caches, and analyzes US equity/ETF financial data from
|
||||
//! multiple free-tier API providers (Twelve Data, Polygon, Finnhub,
|
||||
//! Alpha Vantage). Includes Morningstar-style performance calculations.
|
||||
//!
|
||||
//! ## Getting Started
|
||||
//!
|
||||
//! Most consumers should start with `DataService`, which orchestrates
|
||||
//! fetching, caching, and provider selection. Pair it with a `Config`
|
||||
//! (populated from environment variables / .env) to get going:
|
||||
//!
|
||||
//! ```
|
||||
//! const config = try zfin.Config.load(allocator);
|
||||
//! var svc = zfin.DataService.init(allocator, config);
|
||||
//! const result = try svc.getCandles("AAPL");
|
||||
//! ```
|
||||
//!
|
||||
//! For portfolio workflows, load a Portfolio from an SRF file and pass
|
||||
//! it through `risk` and `performance` for analytics.
|
||||
//!
|
||||
//! The `format` module contains shared rendering helpers used by both
|
||||
//! the CLI commands and TUI.
|
||||
|
||||
// -- Data models --
|
||||
// ── Data Models ──────────────────────────────────────────────
|
||||
|
||||
/// Calendar date with financial-market helpers (trading days, expiry rules).
|
||||
pub const Date = @import("models/date.zig").Date;
|
||||
|
||||
/// OHLCV price bar (open, high, low, close, volume) for a single trading day.
|
||||
pub const Candle = @import("models/candle.zig").Candle;
|
||||
|
||||
/// Cash dividend payment with ex-date, pay-date, and amount.
|
||||
pub const Dividend = @import("models/dividend.zig").Dividend;
|
||||
pub const DividendType = @import("models/dividend.zig").DividendType;
|
||||
|
||||
/// Stock split event (ratio + effective date).
|
||||
pub const Split = @import("models/split.zig").Split;
|
||||
|
||||
/// Single options contract (strike, expiry, greeks, bid/ask).
|
||||
pub const OptionContract = @import("models/option.zig").OptionContract;
|
||||
|
||||
/// Full options chain for a symbol (calls + puts grouped by expiry).
|
||||
pub const OptionsChain = @import("models/option.zig").OptionsChain;
|
||||
pub const ContractType = @import("models/option.zig").ContractType;
|
||||
|
||||
/// Quarterly earnings event with EPS estimate, actual, and surprise.
|
||||
pub const EarningsEvent = @import("models/earnings.zig").EarningsEvent;
|
||||
pub const ReportTime = @import("models/earnings.zig").ReportTime;
|
||||
|
||||
/// ETF profile: expense ratio, AUM, top holdings, sector weights.
|
||||
pub const EtfProfile = @import("models/etf_profile.zig").EtfProfile;
|
||||
pub const Holding = @import("models/etf_profile.zig").Holding;
|
||||
pub const SectorWeight = @import("models/etf_profile.zig").SectorWeight;
|
||||
pub const TickerInfo = @import("models/ticker_info.zig").TickerInfo;
|
||||
pub const SecurityType = @import("models/ticker_info.zig").SecurityType;
|
||||
|
||||
/// A single position lot (shares, cost basis, purchase date, type).
|
||||
pub const Lot = @import("models/portfolio.zig").Lot;
|
||||
pub const LotType = @import("models/portfolio.zig").LotType;
|
||||
|
||||
/// Aggregated position: symbol + all its lots.
|
||||
pub const Position = @import("models/portfolio.zig").Position;
|
||||
|
||||
/// Parsed portfolio: positions, cash, CDs, options, metadata.
|
||||
pub const Portfolio = @import("models/portfolio.zig").Portfolio;
|
||||
|
||||
/// Real-time or delayed price quote (last, bid, ask, volume).
|
||||
pub const Quote = @import("models/quote.zig").Quote;
|
||||
|
||||
// -- Infrastructure --
|
||||
pub const Config = @import("config.zig").Config;
|
||||
pub const RateLimiter = @import("net/rate_limiter.zig").RateLimiter;
|
||||
pub const http = @import("net/http.zig");
|
||||
// ── Infrastructure ───────────────────────────────────────────
|
||||
|
||||
// -- Cache --
|
||||
/// Runtime configuration loaded from environment / .env file (API keys, paths).
|
||||
pub const Config = @import("config.zig").Config;
|
||||
|
||||
// ── Cache ────────────────────────────────────────────────────
|
||||
|
||||
/// SRF-backed file cache for candles, dividends, earnings, and other fetched data.
|
||||
pub const cache = @import("cache/store.zig");
|
||||
|
||||
// -- Analytics --
|
||||
// ── Analytics ────────────────────────────────────────────────
|
||||
|
||||
/// Morningstar-style holding-period return calculations (total + annualized).
|
||||
pub const performance = @import("analytics/performance.zig");
|
||||
|
||||
/// Portfolio risk metrics: sector weights, fallback prices, DRIP aggregation.
|
||||
pub const risk = @import("analytics/risk.zig");
|
||||
|
||||
/// Technical indicators: SMA, EMA, Bollinger Bands, RSI, MACD.
|
||||
pub const indicators = @import("analytics/indicators.zig");
|
||||
|
||||
/// Fundamental analysis: valuation, momentum, quality, and yield scoring.
|
||||
pub const analysis = @import("analytics/analysis.zig");
|
||||
|
||||
// -- Classification --
|
||||
// ── Classification ───────────────────────────────────────────
|
||||
|
||||
/// Sector/industry/country classification for enriched securities.
|
||||
pub const classification = @import("models/classification.zig");
|
||||
|
||||
// -- Formatting (shared between CLI and TUI) --
|
||||
// ── Formatting ───────────────────────────────────────────────
|
||||
|
||||
/// Shared rendering helpers (money formatting, charts, earnings rows, bars)
|
||||
/// used by both CLI commands and TUI.
|
||||
pub const format = @import("format.zig");
|
||||
|
||||
// -- Service layer --
|
||||
// ── Service Layer ────────────────────────────────────────────
|
||||
|
||||
/// High-level data service: orchestrates providers, caching, and fallback logic.
|
||||
pub const DataService = @import("service.zig").DataService;
|
||||
|
||||
/// Errors returned by DataService (NoApiKey, RateLimited, etc.).
|
||||
pub const DataError = @import("service.zig").DataError;
|
||||
pub const DataSource = @import("service.zig").Source;
|
||||
|
||||
// -- Providers --
|
||||
pub const Provider = @import("providers/provider.zig").Provider;
|
||||
pub const TwelveData = @import("providers/twelvedata.zig").TwelveData;
|
||||
pub const Polygon = @import("providers/polygon.zig").Polygon;
|
||||
pub const Finnhub = @import("providers/finnhub.zig").Finnhub;
|
||||
pub const Cboe = @import("providers/cboe.zig").Cboe;
|
||||
pub const AlphaVantage = @import("providers/alphavantage.zig").AlphaVantage;
|
||||
pub const OpenFigi = @import("providers/openfigi.zig");
|
||||
/// Company overview data (sector, industry, country, market cap) from Alpha Vantage.
|
||||
pub const CompanyOverview = @import("service.zig").CompanyOverview;
|
||||
|
||||
// -- Re-export SRF for portfolio file loading --
|
||||
pub const srf = @import("srf");
|
||||
/// Result of a CUSIP-to-ticker lookup (ticker, name, security type).
|
||||
pub const CusipResult = @import("service.zig").CusipResult;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const Polygon = @import("providers/polygon.zig").Polygon;
|
|||
const Finnhub = @import("providers/finnhub.zig").Finnhub;
|
||||
const Cboe = @import("providers/cboe.zig").Cboe;
|
||||
const AlphaVantage = @import("providers/alphavantage.zig").AlphaVantage;
|
||||
const alphavantage = @import("providers/alphavantage.zig");
|
||||
const OpenFigi = @import("providers/openfigi.zig");
|
||||
const performance = @import("analytics/performance.zig");
|
||||
|
||||
|
|
@ -34,6 +35,12 @@ pub const DataError = error{
|
|||
OutOfMemory,
|
||||
};
|
||||
|
||||
/// Re-exported provider types needed by commands via DataService.
|
||||
pub const CompanyOverview = alphavantage.CompanyOverview;
|
||||
|
||||
/// Result of a CUSIP-to-ticker lookup (provider-agnostic).
|
||||
pub const CusipResult = OpenFigi.FigiResult;
|
||||
|
||||
/// Indicates whether the returned data came from cache or was freshly fetched.
|
||||
pub const Source = enum {
|
||||
cached,
|
||||
|
|
@ -333,6 +340,14 @@ pub const DataService = struct {
|
|||
};
|
||||
}
|
||||
|
||||
/// Fetch company overview (sector, industry, country, market cap) from Alpha Vantage.
|
||||
/// No cache -- always fetches fresh. Caller must free the returned string fields.
|
||||
pub fn getCompanyOverview(self: *DataService, symbol: []const u8) DataError!CompanyOverview {
|
||||
var av = try self.getAlphaVantage();
|
||||
return av.fetchCompanyOverview(self.allocator, symbol) catch
|
||||
return DataError.FetchFailed;
|
||||
}
|
||||
|
||||
/// Compute trailing returns for a symbol (fetches candles + dividends).
|
||||
/// Returns both as-of-date and month-end trailing returns.
|
||||
/// As-of-date: end = latest close. Matches Morningstar "Trailing Returns" page.
|
||||
|
|
@ -565,6 +580,14 @@ pub const DataService = struct {
|
|||
|
||||
// ── CUSIP Resolution ──────────────────────────────────────────
|
||||
|
||||
/// Look up multiple CUSIPs in a single batch request via OpenFIGI.
|
||||
/// Results array is parallel to the input cusips array (same length, same order).
|
||||
/// Caller owns the returned slice and all strings within each CusipResult.
|
||||
pub fn lookupCusips(self: *DataService, cusips: []const []const u8) DataError![]CusipResult {
|
||||
return OpenFigi.lookupCusips(self.allocator, cusips, self.config.openfigi_key) catch
|
||||
return DataError.FetchFailed;
|
||||
}
|
||||
|
||||
/// Look up a CUSIP via OpenFIGI API. Returns the ticker if found, null otherwise.
|
||||
/// Results are cached in {cache_dir}/cusip_tickers.srf.
|
||||
/// Caller owns the returned string.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue