utilize srf library from/to most concrete types
This commit is contained in:
parent
fcd85aa64e
commit
c65ebb9384
14 changed files with 111 additions and 247 deletions
|
|
@ -13,8 +13,8 @@
|
||||||
.hash = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ",
|
.hash = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ",
|
||||||
},
|
},
|
||||||
.srf = .{
|
.srf = .{
|
||||||
.url = "git+https://git.lerch.org/lobo/srf.git#c4a59cfbd3bb8a0157cdd6a49e1a1ef24439460e",
|
.url = "git+https://git.lerch.org/lobo/srf.git#18a4558b47acb3d62701351820cd70dcc2c56c5a",
|
||||||
.hash = "srf-0.0.0-qZj572f9AADFQvNOFZ6Ls2C_7i53CJj4kjYZ4CY9wiJ0",
|
.hash = "srf-0.0.0-qZj579ZBAQCJdGtTnPQlxlGPw3g2gmTmwer8V6yyo-ga",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
.paths = .{
|
.paths = .{
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
/// Takes portfolio allocations (with market values) and classification metadata,
|
/// Takes portfolio allocations (with market values) and classification metadata,
|
||||||
/// produces breakdowns by asset class, sector, geographic region, account, and tax type.
|
/// produces breakdowns by asset class, sector, geographic region, account, and tax type.
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const srf = @import("srf");
|
||||||
const Allocation = @import("risk.zig").Allocation;
|
const Allocation = @import("risk.zig").Allocation;
|
||||||
const ClassificationEntry = @import("../models/classification.zig").ClassificationEntry;
|
const ClassificationEntry = @import("../models/classification.zig").ClassificationEntry;
|
||||||
const ClassificationMap = @import("../models/classification.zig").ClassificationMap;
|
const ClassificationMap = @import("../models/classification.zig").ClassificationMap;
|
||||||
|
|
@ -56,7 +57,7 @@ fn taxTypeLabel(raw: []const u8) []const u8 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an accounts.srf file into an AccountMap.
|
/// Parse an accounts.srf file into an AccountMap.
|
||||||
/// Format: account::<NAME>,tax_type::<TYPE>
|
/// Each record has: account::<NAME>,tax_type::<TYPE>
|
||||||
pub fn parseAccountsFile(allocator: std.mem.Allocator, data: []const u8) !AccountMap {
|
pub fn parseAccountsFile(allocator: std.mem.Allocator, data: []const u8) !AccountMap {
|
||||||
var entries = std.ArrayList(AccountTaxEntry).empty;
|
var entries = std.ArrayList(AccountTaxEntry).empty;
|
||||||
errdefer {
|
errdefer {
|
||||||
|
|
@ -67,30 +68,15 @@ pub fn parseAccountsFile(allocator: std.mem.Allocator, data: []const u8) !Accoun
|
||||||
entries.deinit(allocator);
|
entries.deinit(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
var line_iter = std.mem.splitScalar(u8, data, '\n');
|
var reader = std.Io.Reader.fixed(data);
|
||||||
while (line_iter.next()) |line| {
|
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
||||||
const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace);
|
defer parsed.deinit();
|
||||||
if (trimmed.len == 0 or trimmed[0] == '#') continue;
|
|
||||||
if (std.mem.startsWith(u8, trimmed, "#!")) continue;
|
|
||||||
|
|
||||||
var account: ?[]const u8 = null;
|
for (parsed.records.items) |record| {
|
||||||
var tax_type: ?[]const u8 = null;
|
const entry = record.to(AccountTaxEntry) catch continue;
|
||||||
|
|
||||||
var field_iter = std.mem.splitScalar(u8, trimmed, ',');
|
|
||||||
while (field_iter.next()) |field| {
|
|
||||||
const f = std.mem.trim(u8, field, &std.ascii.whitespace);
|
|
||||||
if (std.mem.startsWith(u8, f, "account::")) {
|
|
||||||
account = f["account::".len..];
|
|
||||||
} else if (std.mem.startsWith(u8, f, "tax_type::")) {
|
|
||||||
tax_type = f["tax_type::".len..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const acct = account orelse continue;
|
|
||||||
const tt = tax_type orelse continue;
|
|
||||||
try entries.append(allocator, .{
|
try entries.append(allocator, .{
|
||||||
.account = try allocator.dupe(u8, acct),
|
.account = try allocator.dupe(u8, entry.account),
|
||||||
.tax_type = try allocator.dupe(u8, tt),
|
.tax_type = try allocator.dupe(u8, entry.tax_type),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,7 +187,7 @@ pub fn analyzePortfolio(
|
||||||
for (portfolio.lots) |lot| {
|
for (portfolio.lots) |lot| {
|
||||||
if (!lot.isOpen()) continue;
|
if (!lot.isOpen()) continue;
|
||||||
const acct = lot.account orelse continue;
|
const acct = lot.account orelse continue;
|
||||||
const value: f64 = switch (lot.lot_type) {
|
const value: f64 = switch (lot.security_type) {
|
||||||
.stock => blk: {
|
.stock => blk: {
|
||||||
const price = price_lookup.get(lot.priceSymbol()) orelse lot.open_price;
|
const price = price_lookup.get(lot.priceSymbol()) orelse lot.open_price;
|
||||||
break :blk lot.shares * price;
|
break :blk lot.shares * price;
|
||||||
|
|
@ -342,15 +328,20 @@ test "mapToSortedBreakdown empty" {
|
||||||
|
|
||||||
test "parseAccountsFile empty" {
|
test "parseAccountsFile empty" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var am = try parseAccountsFile(allocator, "");
|
var am = try parseAccountsFile(allocator, "#!srfv1\n");
|
||||||
defer am.deinit();
|
defer am.deinit();
|
||||||
try std.testing.expectEqual(@as(usize, 0), am.entries.len);
|
try std.testing.expectEqual(@as(usize, 0), am.entries.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseAccountsFile missing fields" {
|
test "parseAccountsFile missing fields" {
|
||||||
|
// Line with only account but no tax_type -> skipped via Record.to() error.
|
||||||
|
// Override log level to suppress expected srf log.err output that
|
||||||
|
// would otherwise cause the test runner to report failure.
|
||||||
|
const prev_level = std.testing.log_level;
|
||||||
|
std.testing.log_level = .err;
|
||||||
|
defer std.testing.log_level = prev_level;
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
// Line with only account but no tax_type -> skipped
|
var am = try parseAccountsFile(allocator, "#!srfv1\naccount::Test Account\n# comment\n");
|
||||||
var am = try parseAccountsFile(allocator, "account::Test Account\n# comment\n");
|
|
||||||
defer am.deinit();
|
defer am.deinit();
|
||||||
try std.testing.expectEqual(@as(usize, 0), am.entries.len);
|
try std.testing.expectEqual(@as(usize, 0), am.entries.len);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ pub fn buildFallbackPrices(
|
||||||
errdefer manual_price_set.deinit();
|
errdefer manual_price_set.deinit();
|
||||||
// First pass: manual price:: overrides
|
// First pass: manual price:: overrides
|
||||||
for (lots) |lot| {
|
for (lots) |lot| {
|
||||||
if (lot.lot_type != .stock) continue;
|
if (lot.security_type != .stock) continue;
|
||||||
const sym = lot.priceSymbol();
|
const sym = lot.priceSymbol();
|
||||||
if (lot.price) |p| {
|
if (lot.price) |p| {
|
||||||
if (!prices.contains(sym)) {
|
if (!prices.contains(sym)) {
|
||||||
|
|
@ -558,9 +558,9 @@ test "adjustForNonStockAssets" {
|
||||||
const Lot = @import("../models/portfolio.zig").Lot;
|
const Lot = @import("../models/portfolio.zig").Lot;
|
||||||
var lots = [_]Lot{
|
var lots = [_]Lot{
|
||||||
.{ .symbol = "VTI", .shares = 10, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 200 },
|
.{ .symbol = "VTI", .shares = 10, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 200 },
|
||||||
.{ .symbol = "Cash", .shares = 5000, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .lot_type = .cash },
|
.{ .symbol = "Cash", .shares = 5000, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .security_type = .cash },
|
||||||
.{ .symbol = "CD1", .shares = 10000, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .lot_type = .cd },
|
.{ .symbol = "CD1", .shares = 10000, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .security_type = .cd },
|
||||||
.{ .symbol = "OPT1", .shares = 2, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 5.0, .lot_type = .option },
|
.{ .symbol = "OPT1", .shares = 2, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 5.0, .security_type = .option },
|
||||||
};
|
};
|
||||||
const pf = Portfolio{ .lots = &lots, .allocator = std.testing.allocator };
|
const pf = Portfolio{ .lots = &lots, .allocator = std.testing.allocator };
|
||||||
var allocs = [_]Allocation{
|
var allocs = [_]Allocation{
|
||||||
|
|
|
||||||
133
src/cache/store.zig
vendored
133
src/cache/store.zig
vendored
|
|
@ -263,17 +263,7 @@ pub const Store = struct {
|
||||||
var buf: std.ArrayList(u8) = .empty;
|
var buf: std.ArrayList(u8) = .empty;
|
||||||
errdefer buf.deinit(allocator);
|
errdefer buf.deinit(allocator);
|
||||||
const writer = buf.writer(allocator);
|
const writer = buf.writer(allocator);
|
||||||
|
try writer.print("{f}", .{srf.fmtFrom(Candle, allocator, candles, .{})});
|
||||||
try writer.writeAll("#!srfv1\n");
|
|
||||||
for (candles) |c| {
|
|
||||||
var date_buf: [10]u8 = undefined;
|
|
||||||
const date_str = c.date.format(&date_buf);
|
|
||||||
try writer.print(
|
|
||||||
"date::{s},open:num:{d},high:num:{d},low:num:{d},close:num:{d},adj_close:num:{d},volume:num:{d}\n",
|
|
||||||
.{ date_str, c.open, c.high, c.low, c.close, c.adj_close, c.volume },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.toOwnedSlice(allocator);
|
return buf.toOwnedSlice(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -330,22 +320,7 @@ pub const Store = struct {
|
||||||
var buf: std.ArrayList(u8) = .empty;
|
var buf: std.ArrayList(u8) = .empty;
|
||||||
errdefer buf.deinit(allocator);
|
errdefer buf.deinit(allocator);
|
||||||
const writer = buf.writer(allocator);
|
const writer = buf.writer(allocator);
|
||||||
|
try writer.print("{f}", .{srf.fmtFrom(Dividend, allocator, dividends, .{})});
|
||||||
try writer.writeAll("#!srfv1\n");
|
|
||||||
for (dividends) |d| {
|
|
||||||
var ex_buf: [10]u8 = undefined;
|
|
||||||
const ex_str = d.ex_date.format(&ex_buf);
|
|
||||||
try writer.print("ex_date::{s},amount:num:{d}", .{ ex_str, d.amount });
|
|
||||||
if (d.pay_date) |pd| {
|
|
||||||
var pay_buf: [10]u8 = undefined;
|
|
||||||
try writer.print(",pay_date::{s}", .{pd.format(&pay_buf)});
|
|
||||||
}
|
|
||||||
if (d.frequency) |f| {
|
|
||||||
try writer.print(",frequency:num:{d}", .{f});
|
|
||||||
}
|
|
||||||
try writer.print(",type::{s}\n", .{@tagName(d.distribution_type)});
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.toOwnedSlice(allocator);
|
return buf.toOwnedSlice(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,16 +329,7 @@ pub const Store = struct {
|
||||||
var buf: std.ArrayList(u8) = .empty;
|
var buf: std.ArrayList(u8) = .empty;
|
||||||
errdefer buf.deinit(allocator);
|
errdefer buf.deinit(allocator);
|
||||||
const writer = buf.writer(allocator);
|
const writer = buf.writer(allocator);
|
||||||
|
try writer.print("{f}", .{srf.fmtFrom(Split, allocator, splits, .{})});
|
||||||
try writer.writeAll("#!srfv1\n");
|
|
||||||
for (splits) |s| {
|
|
||||||
var date_buf: [10]u8 = undefined;
|
|
||||||
const date_str = s.date.format(&date_buf);
|
|
||||||
try writer.print("date::{s},numerator:num:{d},denominator:num:{d}\n", .{
|
|
||||||
date_str, s.numerator, s.denominator,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.toOwnedSlice(allocator);
|
return buf.toOwnedSlice(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,7 +377,7 @@ pub const Store = struct {
|
||||||
.string => |s| s,
|
.string => |s| s,
|
||||||
else => continue,
|
else => continue,
|
||||||
};
|
};
|
||||||
div.distribution_type = parseDividendTypeTag(str);
|
div.type = parseDividendTypeTag(str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -462,21 +428,7 @@ pub const Store = struct {
|
||||||
var buf: std.ArrayList(u8) = .empty;
|
var buf: std.ArrayList(u8) = .empty;
|
||||||
errdefer buf.deinit(allocator);
|
errdefer buf.deinit(allocator);
|
||||||
const writer = buf.writer(allocator);
|
const writer = buf.writer(allocator);
|
||||||
|
try writer.print("{f}", .{srf.fmtFrom(EarningsEvent, allocator, events, .{})});
|
||||||
try writer.writeAll("#!srfv1\n");
|
|
||||||
for (events) |e| {
|
|
||||||
var date_buf: [10]u8 = undefined;
|
|
||||||
const date_str = e.date.format(&date_buf);
|
|
||||||
try writer.print("date::{s}", .{date_str});
|
|
||||||
if (e.estimate) |est| try writer.print(",estimate:num:{d}", .{est});
|
|
||||||
if (e.actual) |act| try writer.print(",actual:num:{d}", .{act});
|
|
||||||
if (e.quarter) |q| try writer.print(",quarter:num:{d}", .{q});
|
|
||||||
if (e.fiscal_year) |fy| try writer.print(",fiscal_year:num:{d}", .{fy});
|
|
||||||
if (e.revenue_actual) |ra| try writer.print(",revenue_actual:num:{d}", .{ra});
|
|
||||||
if (e.revenue_estimate) |re| try writer.print(",revenue_estimate:num:{d}", .{re});
|
|
||||||
try writer.print(",report_time::{s}\n", .{@tagName(e.report_time)});
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.toOwnedSlice(allocator);
|
return buf.toOwnedSlice(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -949,67 +901,7 @@ pub fn serializePortfolio(allocator: std.mem.Allocator, lots: []const Lot) ![]co
|
||||||
var buf: std.ArrayList(u8) = .empty;
|
var buf: std.ArrayList(u8) = .empty;
|
||||||
errdefer buf.deinit(allocator);
|
errdefer buf.deinit(allocator);
|
||||||
const writer = buf.writer(allocator);
|
const writer = buf.writer(allocator);
|
||||||
|
try writer.print("{f}", .{srf.fmtFrom(Lot, allocator, lots, .{})});
|
||||||
try writer.writeAll("#!srfv1\n");
|
|
||||||
for (lots) |lot| {
|
|
||||||
var od_buf: [10]u8 = undefined;
|
|
||||||
// Write security_type if not stock (stock is the default)
|
|
||||||
if (lot.lot_type != .stock) {
|
|
||||||
const type_str: []const u8 = switch (lot.lot_type) {
|
|
||||||
.option => "option",
|
|
||||||
.cd => "cd",
|
|
||||||
.cash => "cash",
|
|
||||||
.illiquid => "illiquid",
|
|
||||||
.watch => "watch",
|
|
||||||
.stock => unreachable,
|
|
||||||
};
|
|
||||||
try writer.print("security_type::{s},", .{type_str});
|
|
||||||
}
|
|
||||||
// Watch lots only need a symbol
|
|
||||||
if (lot.lot_type == .watch) {
|
|
||||||
try writer.print("symbol::{s}\n", .{lot.symbol});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try writer.print("symbol::{s}", .{lot.symbol});
|
|
||||||
if (lot.ticker) |t| {
|
|
||||||
try writer.print(",ticker::{s}", .{t});
|
|
||||||
}
|
|
||||||
try writer.print(",shares:num:{d},open_date::{s},open_price:num:{d}", .{
|
|
||||||
lot.shares, lot.open_date.format(&od_buf), lot.open_price,
|
|
||||||
});
|
|
||||||
if (lot.close_date) |cd| {
|
|
||||||
var cd_buf: [10]u8 = undefined;
|
|
||||||
try writer.print(",close_date::{s}", .{cd.format(&cd_buf)});
|
|
||||||
}
|
|
||||||
if (lot.close_price) |cp| {
|
|
||||||
try writer.print(",close_price:num:{d}", .{cp});
|
|
||||||
}
|
|
||||||
if (lot.note) |n| {
|
|
||||||
try writer.print(",note::{s}", .{n});
|
|
||||||
}
|
|
||||||
if (lot.account) |a| {
|
|
||||||
try writer.print(",account::{s}", .{a});
|
|
||||||
}
|
|
||||||
if (lot.maturity_date) |md| {
|
|
||||||
var md_buf: [10]u8 = undefined;
|
|
||||||
try writer.print(",maturity_date::{s}", .{md.format(&md_buf)});
|
|
||||||
}
|
|
||||||
if (lot.rate) |r| {
|
|
||||||
try writer.print(",rate:num:{d}", .{r});
|
|
||||||
}
|
|
||||||
if (lot.drip) {
|
|
||||||
try writer.writeAll(",drip::true");
|
|
||||||
}
|
|
||||||
if (lot.price) |p| {
|
|
||||||
try writer.print(",price:num:{d}", .{p});
|
|
||||||
}
|
|
||||||
if (lot.price_date) |pd| {
|
|
||||||
var pd_buf: [10]u8 = undefined;
|
|
||||||
try writer.print(",price_date::{s}", .{pd.format(&pd_buf)});
|
|
||||||
}
|
|
||||||
try writer.writeAll("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.toOwnedSlice(allocator);
|
return buf.toOwnedSlice(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1105,6 +997,7 @@ pub fn deserializePortfolio(allocator: std.mem.Allocator, data: []const u8) !Por
|
||||||
switch (v) {
|
switch (v) {
|
||||||
.string => |s| lot.drip = std.mem.eql(u8, s, "true") or std.mem.eql(u8, s, "1"),
|
.string => |s| lot.drip = std.mem.eql(u8, s, "true") or std.mem.eql(u8, s, "1"),
|
||||||
.number => |n| lot.drip = n > 0,
|
.number => |n| lot.drip = n > 0,
|
||||||
|
.boolean => |b| lot.drip = b,
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1131,11 +1024,11 @@ pub fn deserializePortfolio(allocator: std.mem.Allocator, data: []const u8) !Por
|
||||||
|
|
||||||
// Determine lot type
|
// Determine lot type
|
||||||
if (sec_type_raw) |st| {
|
if (sec_type_raw) |st| {
|
||||||
lot.lot_type = LotType.fromString(st);
|
lot.security_type = LotType.fromString(st);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cash lots don't require a symbol -- generate a placeholder
|
// Cash lots don't require a symbol -- generate a placeholder
|
||||||
if (lot.lot_type == .cash) {
|
if (lot.security_type == .cash) {
|
||||||
if (sym_raw == null) {
|
if (sym_raw == null) {
|
||||||
lot.symbol = try allocator.dupe(u8, "CASH");
|
lot.symbol = try allocator.dupe(u8, "CASH");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1169,8 +1062,8 @@ pub fn deserializePortfolio(allocator: std.mem.Allocator, data: []const u8) !Por
|
||||||
test "dividend serialize/deserialize round-trip" {
|
test "dividend serialize/deserialize round-trip" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
const divs = [_]Dividend{
|
const divs = [_]Dividend{
|
||||||
.{ .ex_date = Date.fromYmd(2024, 3, 15), .amount = 0.8325, .pay_date = Date.fromYmd(2024, 3, 28), .frequency = 4, .distribution_type = .regular },
|
.{ .ex_date = Date.fromYmd(2024, 3, 15), .amount = 0.8325, .pay_date = Date.fromYmd(2024, 3, 28), .frequency = 4, .type = .regular },
|
||||||
.{ .ex_date = Date.fromYmd(2024, 6, 14), .amount = 0.9148, .distribution_type = .special },
|
.{ .ex_date = Date.fromYmd(2024, 6, 14), .amount = 0.9148, .type = .special },
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = try Store.serializeDividends(allocator, &divs);
|
const data = try Store.serializeDividends(allocator, &divs);
|
||||||
|
|
@ -1186,12 +1079,12 @@ test "dividend serialize/deserialize round-trip" {
|
||||||
try std.testing.expect(parsed[0].pay_date != null);
|
try std.testing.expect(parsed[0].pay_date != null);
|
||||||
try std.testing.expect(parsed[0].pay_date.?.eql(Date.fromYmd(2024, 3, 28)));
|
try std.testing.expect(parsed[0].pay_date.?.eql(Date.fromYmd(2024, 3, 28)));
|
||||||
try std.testing.expectEqual(@as(?u8, 4), parsed[0].frequency);
|
try std.testing.expectEqual(@as(?u8, 4), parsed[0].frequency);
|
||||||
try std.testing.expectEqual(DividendType.regular, parsed[0].distribution_type);
|
try std.testing.expectEqual(DividendType.regular, parsed[0].type);
|
||||||
|
|
||||||
try std.testing.expect(parsed[1].ex_date.eql(Date.fromYmd(2024, 6, 14)));
|
try std.testing.expect(parsed[1].ex_date.eql(Date.fromYmd(2024, 6, 14)));
|
||||||
try std.testing.expectApproxEqAbs(@as(f64, 0.9148), parsed[1].amount, 0.0001);
|
try std.testing.expectApproxEqAbs(@as(f64, 0.9148), parsed[1].amount, 0.0001);
|
||||||
try std.testing.expect(parsed[1].pay_date == null);
|
try std.testing.expect(parsed[1].pay_date == null);
|
||||||
try std.testing.expectEqual(DividendType.special, parsed[1].distribution_type);
|
try std.testing.expectEqual(DividendType.special, parsed[1].type);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "split serialize/deserialize round-trip" {
|
test "split serialize/deserialize round-trip" {
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ pub fn display(dividends: []const zfin.Dividend, symbol: []const u8, current_pri
|
||||||
} else {
|
} else {
|
||||||
try out.print(" {s:>6}", .{"--"});
|
try out.print(" {s:>6}", .{"--"});
|
||||||
}
|
}
|
||||||
try out.print(" {s:>10}\n", .{@tagName(div.distribution_type)});
|
try out.print(" {s:>10}\n", .{@tagName(div.type)});
|
||||||
total += div.amount;
|
total += div.amount;
|
||||||
if (!div.ex_date.lessThan(one_year_ago)) ttm += div.amount;
|
if (!div.ex_date.lessThan(one_year_ago)) ttm += div.amount;
|
||||||
}
|
}
|
||||||
|
|
@ -92,8 +92,8 @@ test "display shows dividend data with yield" {
|
||||||
var buf: [4096]u8 = undefined;
|
var buf: [4096]u8 = undefined;
|
||||||
var w: std.Io.Writer = .fixed(&buf);
|
var w: std.Io.Writer = .fixed(&buf);
|
||||||
const divs = [_]zfin.Dividend{
|
const divs = [_]zfin.Dividend{
|
||||||
.{ .ex_date = .{ .days = 20000 }, .amount = 0.88, .distribution_type = .regular },
|
.{ .ex_date = .{ .days = 20000 }, .amount = 0.88, .type = .regular },
|
||||||
.{ .ex_date = .{ .days = 19900 }, .amount = 0.88, .distribution_type = .regular },
|
.{ .ex_date = .{ .days = 19900 }, .amount = 0.88, .type = .regular },
|
||||||
};
|
};
|
||||||
try display(&divs, "VTI", 250.0, false, &w);
|
try display(&divs, "VTI", 250.0, false, &w);
|
||||||
const out = w.buffered();
|
const out = w.buffered();
|
||||||
|
|
@ -117,7 +117,7 @@ test "display without price omits yield" {
|
||||||
var buf: [4096]u8 = undefined;
|
var buf: [4096]u8 = undefined;
|
||||||
var w: std.Io.Writer = .fixed(&buf);
|
var w: std.Io.Writer = .fixed(&buf);
|
||||||
const divs = [_]zfin.Dividend{
|
const divs = [_]zfin.Dividend{
|
||||||
.{ .ex_date = .{ .days = 20000 }, .amount = 1.50, .distribution_type = .regular },
|
.{ .ex_date = .{ .days = 20000 }, .amount = 1.50, .type = .regular },
|
||||||
};
|
};
|
||||||
try display(&divs, "T", null, false, &w);
|
try display(&divs, "T", null, false, &w);
|
||||||
const out = w.buffered();
|
const out = w.buffered();
|
||||||
|
|
@ -129,7 +129,7 @@ test "display no ANSI without color" {
|
||||||
var buf: [4096]u8 = undefined;
|
var buf: [4096]u8 = undefined;
|
||||||
var w: std.Io.Writer = .fixed(&buf);
|
var w: std.Io.Writer = .fixed(&buf);
|
||||||
const divs = [_]zfin.Dividend{
|
const divs = [_]zfin.Dividend{
|
||||||
.{ .ex_date = .{ .days = 20000 }, .amount = 0.50, .distribution_type = .regular },
|
.{ .ex_date = .{ .days = 20000 }, .amount = 0.50, .type = .regular },
|
||||||
};
|
};
|
||||||
try display(&divs, "SPY", 500.0, false, &w);
|
try display(&divs, "SPY", 500.0, false, &w);
|
||||||
const out = w.buffered();
|
const out = w.buffered();
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
|
||||||
defer seen.deinit();
|
defer seen.deinit();
|
||||||
for (syms) |s| try seen.put(s, {});
|
for (syms) |s| try seen.put(s, {});
|
||||||
for (portfolio.lots) |lot| {
|
for (portfolio.lots) |lot| {
|
||||||
if (lot.lot_type == .watch and !seen.contains(lot.priceSymbol())) {
|
if (lot.security_type == .watch and !seen.contains(lot.priceSymbol())) {
|
||||||
try seen.put(lot.priceSymbol(), {});
|
try seen.put(lot.priceSymbol(), {});
|
||||||
try watch_syms.append(allocator, lot.priceSymbol());
|
try watch_syms.append(allocator, lot.priceSymbol());
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +155,7 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
|
||||||
|
|
||||||
// Watch lots from portfolio
|
// Watch lots from portfolio
|
||||||
for (portfolio.lots) |lot| {
|
for (portfolio.lots) |lot| {
|
||||||
if (lot.lot_type == .watch) {
|
if (lot.security_type == .watch) {
|
||||||
const sym = lot.priceSymbol();
|
const sym = lot.priceSymbol();
|
||||||
if (watch_seen.contains(sym)) continue;
|
if (watch_seen.contains(sym)) continue;
|
||||||
try watch_seen.put(sym, {});
|
try watch_seen.put(sym, {});
|
||||||
|
|
@ -249,7 +249,7 @@ pub fn display(
|
||||||
var open_lots: u32 = 0;
|
var open_lots: u32 = 0;
|
||||||
var closed_lots: u32 = 0;
|
var closed_lots: u32 = 0;
|
||||||
for (portfolio.lots) |lot| {
|
for (portfolio.lots) |lot| {
|
||||||
if (lot.lot_type != .stock) continue;
|
if (lot.security_type != .stock) continue;
|
||||||
if (lot.isOpen()) open_lots += 1 else closed_lots += 1;
|
if (lot.isOpen()) open_lots += 1 else closed_lots += 1;
|
||||||
}
|
}
|
||||||
try cli.setFg(out, color, cli.CLR_MUTED);
|
try cli.setFg(out, color, cli.CLR_MUTED);
|
||||||
|
|
@ -297,7 +297,7 @@ pub fn display(
|
||||||
var lots_for_sym: std.ArrayList(zfin.Lot) = .empty;
|
var lots_for_sym: std.ArrayList(zfin.Lot) = .empty;
|
||||||
defer lots_for_sym.deinit(allocator);
|
defer lots_for_sym.deinit(allocator);
|
||||||
for (portfolio.lots) |lot| {
|
for (portfolio.lots) |lot| {
|
||||||
if (lot.lot_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) {
|
if (lot.security_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) {
|
||||||
try lots_for_sym.append(allocator, lot);
|
try lots_for_sym.append(allocator, lot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -464,7 +464,7 @@ pub fn display(
|
||||||
|
|
||||||
var opt_total_cost: f64 = 0;
|
var opt_total_cost: f64 = 0;
|
||||||
for (portfolio.lots) |lot| {
|
for (portfolio.lots) |lot| {
|
||||||
if (lot.lot_type != .option) continue;
|
if (lot.security_type != .option) continue;
|
||||||
const qty = lot.shares;
|
const qty = lot.shares;
|
||||||
const cost_per = lot.open_price;
|
const cost_per = lot.open_price;
|
||||||
const total_cost_opt = @abs(qty) * cost_per;
|
const total_cost_opt = @abs(qty) * cost_per;
|
||||||
|
|
@ -509,7 +509,7 @@ pub fn display(
|
||||||
var cd_lots: std.ArrayList(zfin.Lot) = .empty;
|
var cd_lots: std.ArrayList(zfin.Lot) = .empty;
|
||||||
defer cd_lots.deinit(allocator);
|
defer cd_lots.deinit(allocator);
|
||||||
for (portfolio.lots) |lot| {
|
for (portfolio.lots) |lot| {
|
||||||
if (lot.lot_type == .cd) {
|
if (lot.security_type == .cd) {
|
||||||
try cd_lots.append(allocator, lot);
|
try cd_lots.append(allocator, lot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -560,7 +560,7 @@ pub fn display(
|
||||||
try cli.reset(out, color);
|
try cli.reset(out, color);
|
||||||
|
|
||||||
for (portfolio.lots) |lot| {
|
for (portfolio.lots) |lot| {
|
||||||
if (lot.lot_type != .cash) continue;
|
if (lot.security_type != .cash) continue;
|
||||||
const acct2: []const u8 = lot.account orelse "Unknown";
|
const acct2: []const u8 = lot.account orelse "Unknown";
|
||||||
var row_buf: [160]u8 = undefined;
|
var row_buf: [160]u8 = undefined;
|
||||||
try out.print("{s}\n", .{fmt.fmtCashRow(&row_buf, acct2, lot.shares, lot.note)});
|
try out.print("{s}\n", .{fmt.fmtCashRow(&row_buf, acct2, lot.shares, lot.note)});
|
||||||
|
|
@ -590,7 +590,7 @@ pub fn display(
|
||||||
try cli.reset(out, color);
|
try cli.reset(out, color);
|
||||||
|
|
||||||
for (portfolio.lots) |lot| {
|
for (portfolio.lots) |lot| {
|
||||||
if (lot.lot_type != .illiquid) continue;
|
if (lot.security_type != .illiquid) continue;
|
||||||
var il_row_buf: [160]u8 = undefined;
|
var il_row_buf: [160]u8 = undefined;
|
||||||
try out.print("{s}\n", .{fmt.fmtIlliquidRow(&il_row_buf, lot.symbol, lot.shares, lot.note)});
|
try out.print("{s}\n", .{fmt.fmtIlliquidRow(&il_row_buf, lot.symbol, lot.shares, lot.note)});
|
||||||
}
|
}
|
||||||
|
|
@ -845,7 +845,7 @@ test "display with options section" {
|
||||||
|
|
||||||
var lots = [_]zfin.Lot{
|
var lots = [_]zfin.Lot{
|
||||||
.{ .symbol = "SPY", .shares = 50, .open_date = zfin.Date.fromYmd(2023, 1, 1), .open_price = 400.0 },
|
.{ .symbol = "SPY", .shares = 50, .open_date = zfin.Date.fromYmd(2023, 1, 1), .open_price = 400.0 },
|
||||||
.{ .symbol = "SPY 240119C00450000", .shares = 2, .open_date = zfin.Date.fromYmd(2023, 6, 1), .open_price = 5.50, .lot_type = .option },
|
.{ .symbol = "SPY 240119C00450000", .shares = 2, .open_date = zfin.Date.fromYmd(2023, 6, 1), .open_price = 5.50, .security_type = .option },
|
||||||
};
|
};
|
||||||
var portfolio = testPortfolio(&lots);
|
var portfolio = testPortfolio(&lots);
|
||||||
|
|
||||||
|
|
@ -886,8 +886,8 @@ test "display with CDs and cash" {
|
||||||
|
|
||||||
var lots = [_]zfin.Lot{
|
var lots = [_]zfin.Lot{
|
||||||
.{ .symbol = "VTI", .shares = 10, .open_date = zfin.Date.fromYmd(2023, 1, 1), .open_price = 200.0 },
|
.{ .symbol = "VTI", .shares = 10, .open_date = zfin.Date.fromYmd(2023, 1, 1), .open_price = 200.0 },
|
||||||
.{ .symbol = "912828ZT0", .shares = 10000, .open_date = zfin.Date.fromYmd(2023, 1, 1), .open_price = 100.0, .lot_type = .cd, .rate = 4.5, .maturity_date = zfin.Date.fromYmd(2025, 6, 15) },
|
.{ .symbol = "912828ZT0", .shares = 10000, .open_date = zfin.Date.fromYmd(2023, 1, 1), .open_price = 100.0, .security_type = .cd, .rate = 4.5, .maturity_date = zfin.Date.fromYmd(2025, 6, 15) },
|
||||||
.{ .symbol = "CASH", .shares = 5000, .open_date = zfin.Date.fromYmd(2023, 1, 1), .open_price = 0, .lot_type = .cash, .account = "Brokerage" },
|
.{ .symbol = "CASH", .shares = 5000, .open_date = zfin.Date.fromYmd(2023, 1, 1), .open_price = 0, .security_type = .cash, .account = "Brokerage" },
|
||||||
};
|
};
|
||||||
var portfolio = testPortfolio(&lots);
|
var portfolio = testPortfolio(&lots);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1133,7 +1133,7 @@ test "lotMaturitySortFn" {
|
||||||
.shares = 10000,
|
.shares = 10000,
|
||||||
.open_date = Date.fromYmd(2024, 1, 1),
|
.open_date = Date.fromYmd(2024, 1, 1),
|
||||||
.open_price = 100,
|
.open_price = 100,
|
||||||
.lot_type = .cd,
|
.security_type = .cd,
|
||||||
.maturity_date = Date.fromYmd(2025, 6, 15),
|
.maturity_date = Date.fromYmd(2025, 6, 15),
|
||||||
};
|
};
|
||||||
const later_maturity = Lot{
|
const later_maturity = Lot{
|
||||||
|
|
@ -1141,7 +1141,7 @@ test "lotMaturitySortFn" {
|
||||||
.shares = 10000,
|
.shares = 10000,
|
||||||
.open_date = Date.fromYmd(2024, 1, 1),
|
.open_date = Date.fromYmd(2024, 1, 1),
|
||||||
.open_price = 100,
|
.open_price = 100,
|
||||||
.lot_type = .cd,
|
.security_type = .cd,
|
||||||
.maturity_date = Date.fromYmd(2026, 1, 1),
|
.maturity_date = Date.fromYmd(2026, 1, 1),
|
||||||
};
|
};
|
||||||
const no_maturity = Lot{
|
const no_maturity = Lot{
|
||||||
|
|
@ -1149,7 +1149,7 @@ test "lotMaturitySortFn" {
|
||||||
.shares = 10000,
|
.shares = 10000,
|
||||||
.open_date = Date.fromYmd(2024, 1, 1),
|
.open_date = Date.fromYmd(2024, 1, 1),
|
||||||
.open_price = 100,
|
.open_price = 100,
|
||||||
.lot_type = .cd,
|
.security_type = .cd,
|
||||||
};
|
};
|
||||||
// Earlier maturity sorts first
|
// Earlier maturity sorts first
|
||||||
try std.testing.expect(lotMaturitySortFn({}, with_maturity, later_maturity));
|
try std.testing.expect(lotMaturitySortFn({}, with_maturity, later_maturity));
|
||||||
|
|
|
||||||
|
|
@ -61,33 +61,13 @@ pub fn parseClassificationFile(allocator: std.mem.Allocator, data: []const u8) !
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
|
|
||||||
for (parsed.records.items) |record| {
|
for (parsed.records.items) |record| {
|
||||||
var symbol: ?[]const u8 = null;
|
const entry = record.to(ClassificationEntry) catch continue;
|
||||||
var sector: ?[]const u8 = null;
|
|
||||||
var geo: ?[]const u8 = null;
|
|
||||||
var asset_class: ?[]const u8 = null;
|
|
||||||
var pct: f64 = 100.0;
|
|
||||||
|
|
||||||
for (record.fields) |field| {
|
|
||||||
if (std.mem.eql(u8, field.key, "symbol")) {
|
|
||||||
if (field.value) |v| symbol = strVal(v);
|
|
||||||
} else if (std.mem.eql(u8, field.key, "sector")) {
|
|
||||||
if (field.value) |v| sector = strVal(v);
|
|
||||||
} else if (std.mem.eql(u8, field.key, "geo")) {
|
|
||||||
if (field.value) |v| geo = strVal(v);
|
|
||||||
} else if (std.mem.eql(u8, field.key, "asset_class")) {
|
|
||||||
if (field.value) |v| asset_class = strVal(v);
|
|
||||||
} else if (std.mem.eql(u8, field.key, "pct")) {
|
|
||||||
if (field.value) |v| pct = numVal(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sym = symbol orelse continue;
|
|
||||||
try entries.append(allocator, .{
|
try entries.append(allocator, .{
|
||||||
.symbol = try allocator.dupe(u8, sym),
|
.symbol = try allocator.dupe(u8, entry.symbol),
|
||||||
.sector = if (sector) |s| try allocator.dupe(u8, s) else null,
|
.sector = if (entry.sector) |s| try allocator.dupe(u8, s) else null,
|
||||||
.geo = if (geo) |g| try allocator.dupe(u8, g) else null,
|
.geo = if (entry.geo) |g| try allocator.dupe(u8, g) else null,
|
||||||
.asset_class = if (asset_class) |a| try allocator.dupe(u8, a) else null,
|
.asset_class = if (entry.asset_class) |a| try allocator.dupe(u8, a) else null,
|
||||||
.pct = pct,
|
.pct = entry.pct,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,20 +77,6 @@ pub fn parseClassificationFile(allocator: std.mem.Allocator, data: []const u8) !
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strVal(v: srf.Value) ?[]const u8 {
|
|
||||||
return switch (v) {
|
|
||||||
.string => |s| s,
|
|
||||||
else => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn numVal(v: srf.Value) f64 {
|
|
||||||
return switch (v) {
|
|
||||||
.number => |n| n,
|
|
||||||
else => 100.0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parse classification file" {
|
test "parse classification file" {
|
||||||
const data =
|
const data =
|
||||||
\\#!srfv1
|
\\#!srfv1
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,19 @@ pub const Date = struct {
|
||||||
return fromYmd(y, m, d);
|
return fromYmd(y, m, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hook for srf Record.to(T) coercion.
|
||||||
|
pub fn srfParse(str: []const u8) !Date {
|
||||||
|
return parse(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hook for srf Record.from(T) serialization.
|
||||||
|
pub fn srfFormat(self: Date, allocator: std.mem.Allocator, comptime field_name: []const u8) !srf.Value {
|
||||||
|
_ = field_name;
|
||||||
|
const buf = try allocator.alloc(u8, 10);
|
||||||
|
_ = self.format(buf[0..10]);
|
||||||
|
return .{ .string = buf };
|
||||||
|
}
|
||||||
|
|
||||||
/// Format as "YYYY-MM-DD"
|
/// Format as "YYYY-MM-DD"
|
||||||
pub fn format(self: Date, buf: *[10]u8) []const u8 {
|
pub fn format(self: Date, buf: *[10]u8) []const u8 {
|
||||||
const ymd = epochDaysToYmd(self.days);
|
const ymd = epochDaysToYmd(self.days);
|
||||||
|
|
@ -149,6 +162,7 @@ fn ymdToEpochDays(y: i16, m: u8, d: u8) i32 {
|
||||||
}
|
}
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const srf = @import("srf");
|
||||||
|
|
||||||
test "date roundtrip" {
|
test "date roundtrip" {
|
||||||
const d = Date.fromYmd(2024, 6, 15);
|
const d = Date.fromYmd(2024, 6, 15);
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ pub const Dividend = struct {
|
||||||
/// How many times per year this dividend is expected
|
/// How many times per year this dividend is expected
|
||||||
frequency: ?u8 = null,
|
frequency: ?u8 = null,
|
||||||
/// Classification of the dividend
|
/// Classification of the dividend
|
||||||
distribution_type: DividendType = .unknown,
|
type: DividendType = .unknown,
|
||||||
/// Currency code (e.g., "USD")
|
/// Currency code (e.g., "USD")
|
||||||
currency: ?[]const u8 = null,
|
currency: ?[]const u8 = null,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ pub const ReportTime = enum {
|
||||||
|
|
||||||
/// An earnings event (historical or upcoming).
|
/// An earnings event (historical or upcoming).
|
||||||
pub const EarningsEvent = struct {
|
pub const EarningsEvent = struct {
|
||||||
symbol: []const u8,
|
symbol: []const u8 = "",
|
||||||
date: Date,
|
date: Date,
|
||||||
/// Estimated EPS (analyst consensus)
|
/// Estimated EPS (analyst consensus)
|
||||||
estimate: ?f64 = null,
|
estimate: ?f64 = null,
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ pub const Lot = struct {
|
||||||
/// Optional account identifier (e.g. "Roth IRA", "Brokerage")
|
/// Optional account identifier (e.g. "Roth IRA", "Brokerage")
|
||||||
account: ?[]const u8 = null,
|
account: ?[]const u8 = null,
|
||||||
/// Type of holding (stock, option, cd, cash)
|
/// Type of holding (stock, option, cd, cash)
|
||||||
lot_type: LotType = .stock,
|
security_type: LotType = .stock,
|
||||||
/// Maturity date (for CDs)
|
/// Maturity date (for CDs)
|
||||||
maturity_date: ?Date = null,
|
maturity_date: ?Date = null,
|
||||||
/// Interest rate (for CDs, as percentage e.g. 3.8 = 3.8%)
|
/// Interest rate (for CDs, as percentage e.g. 3.8 = 3.8%)
|
||||||
|
|
@ -163,7 +163,7 @@ pub const Portfolio = struct {
|
||||||
defer seen.deinit();
|
defer seen.deinit();
|
||||||
|
|
||||||
for (self.lots) |lot| {
|
for (self.lots) |lot| {
|
||||||
if (lot.lot_type == .stock) {
|
if (lot.security_type == .stock) {
|
||||||
// Skip lots that have a manual price but no ticker alias —
|
// Skip lots that have a manual price but no ticker alias —
|
||||||
// these are securities without API coverage (e.g. 401k CIT shares).
|
// these are securities without API coverage (e.g. 401k CIT shares).
|
||||||
if (lot.price != null and lot.ticker == null) continue;
|
if (lot.price != null and lot.ticker == null) continue;
|
||||||
|
|
@ -200,7 +200,7 @@ pub const Portfolio = struct {
|
||||||
errdefer result.deinit(allocator);
|
errdefer result.deinit(allocator);
|
||||||
|
|
||||||
for (self.lots) |lot| {
|
for (self.lots) |lot| {
|
||||||
if (lot.lot_type == sec_type) {
|
if (lot.security_type == sec_type) {
|
||||||
try result.append(allocator, lot);
|
try result.append(allocator, lot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -214,7 +214,7 @@ pub const Portfolio = struct {
|
||||||
defer map.deinit();
|
defer map.deinit();
|
||||||
|
|
||||||
for (self.lots) |lot| {
|
for (self.lots) |lot| {
|
||||||
if (lot.lot_type != .stock) continue;
|
if (lot.security_type != .stock) continue;
|
||||||
const sym = lot.priceSymbol();
|
const sym = lot.priceSymbol();
|
||||||
const entry = try map.getOrPut(sym);
|
const entry = try map.getOrPut(sym);
|
||||||
if (!entry.found_existing) {
|
if (!entry.found_existing) {
|
||||||
|
|
@ -269,7 +269,7 @@ pub const Portfolio = struct {
|
||||||
pub fn totalCostBasis(self: Portfolio) f64 {
|
pub fn totalCostBasis(self: Portfolio) f64 {
|
||||||
var total: f64 = 0;
|
var total: f64 = 0;
|
||||||
for (self.lots) |lot| {
|
for (self.lots) |lot| {
|
||||||
if (lot.isOpen() and lot.lot_type == .stock) total += lot.costBasis();
|
if (lot.isOpen() and lot.security_type == .stock) total += lot.costBasis();
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
@ -278,7 +278,7 @@ pub const Portfolio = struct {
|
||||||
pub fn totalRealizedGainLoss(self: Portfolio) f64 {
|
pub fn totalRealizedGainLoss(self: Portfolio) f64 {
|
||||||
var total: f64 = 0;
|
var total: f64 = 0;
|
||||||
for (self.lots) |lot| {
|
for (self.lots) |lot| {
|
||||||
if (lot.lot_type == .stock) {
|
if (lot.security_type == .stock) {
|
||||||
if (lot.realizedGainLoss()) |pnl| total += pnl;
|
if (lot.realizedGainLoss()) |pnl| total += pnl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -289,7 +289,7 @@ pub const Portfolio = struct {
|
||||||
pub fn totalCash(self: Portfolio) f64 {
|
pub fn totalCash(self: Portfolio) f64 {
|
||||||
var total: f64 = 0;
|
var total: f64 = 0;
|
||||||
for (self.lots) |lot| {
|
for (self.lots) |lot| {
|
||||||
if (lot.lot_type == .cash) total += lot.shares;
|
if (lot.security_type == .cash) total += lot.shares;
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
@ -298,7 +298,7 @@ pub const Portfolio = struct {
|
||||||
pub fn totalIlliquid(self: Portfolio) f64 {
|
pub fn totalIlliquid(self: Portfolio) f64 {
|
||||||
var total: f64 = 0;
|
var total: f64 = 0;
|
||||||
for (self.lots) |lot| {
|
for (self.lots) |lot| {
|
||||||
if (lot.lot_type == .illiquid) total += lot.shares;
|
if (lot.security_type == .illiquid) total += lot.shares;
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
@ -307,7 +307,7 @@ pub const Portfolio = struct {
|
||||||
pub fn totalCdFaceValue(self: Portfolio) f64 {
|
pub fn totalCdFaceValue(self: Portfolio) f64 {
|
||||||
var total: f64 = 0;
|
var total: f64 = 0;
|
||||||
for (self.lots) |lot| {
|
for (self.lots) |lot| {
|
||||||
if (lot.lot_type == .cd) total += lot.shares;
|
if (lot.security_type == .cd) total += lot.shares;
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
@ -316,7 +316,7 @@ pub const Portfolio = struct {
|
||||||
pub fn totalOptionCost(self: Portfolio) f64 {
|
pub fn totalOptionCost(self: Portfolio) f64 {
|
||||||
var total: f64 = 0;
|
var total: f64 = 0;
|
||||||
for (self.lots) |lot| {
|
for (self.lots) |lot| {
|
||||||
if (lot.lot_type == .option) {
|
if (lot.security_type == .option) {
|
||||||
// shares can be negative (short), open_price is per-contract cost
|
// shares can be negative (short), open_price is per-contract cost
|
||||||
total += @abs(lot.shares) * lot.open_price;
|
total += @abs(lot.shares) * lot.open_price;
|
||||||
}
|
}
|
||||||
|
|
@ -327,7 +327,7 @@ pub const Portfolio = struct {
|
||||||
/// Check if portfolio has any lots of a given type.
|
/// Check if portfolio has any lots of a given type.
|
||||||
pub fn hasType(self: Portfolio, sec_type: LotType) bool {
|
pub fn hasType(self: Portfolio, sec_type: LotType) bool {
|
||||||
for (self.lots) |lot| {
|
for (self.lots) |lot| {
|
||||||
if (lot.lot_type == sec_type) return true;
|
if (lot.security_type == sec_type) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -338,7 +338,7 @@ pub const Portfolio = struct {
|
||||||
errdefer result.deinit(allocator);
|
errdefer result.deinit(allocator);
|
||||||
|
|
||||||
for (self.lots) |lot| {
|
for (self.lots) |lot| {
|
||||||
if (lot.lot_type == .watch) {
|
if (lot.security_type == .watch) {
|
||||||
try result.append(allocator, lot.symbol);
|
try result.append(allocator, lot.symbol);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -442,13 +442,13 @@ test "Lot.returnPct" {
|
||||||
|
|
||||||
test "Portfolio totals" {
|
test "Portfolio totals" {
|
||||||
var lots = [_]Lot{
|
var lots = [_]Lot{
|
||||||
.{ .symbol = "AAPL", .shares = 10, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 150, .lot_type = .stock },
|
.{ .symbol = "AAPL", .shares = 10, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 150, .security_type = .stock },
|
||||||
.{ .symbol = "AAPL", .shares = 5, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 140, .lot_type = .stock, .close_date = Date.fromYmd(2024, 6, 1), .close_price = 160 },
|
.{ .symbol = "AAPL", .shares = 5, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 140, .security_type = .stock, .close_date = Date.fromYmd(2024, 6, 1), .close_price = 160 },
|
||||||
.{ .symbol = "Savings", .shares = 50000, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .lot_type = .cash },
|
.{ .symbol = "Savings", .shares = 50000, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .security_type = .cash },
|
||||||
.{ .symbol = "CD-1Y", .shares = 10000, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .lot_type = .cd },
|
.{ .symbol = "CD-1Y", .shares = 10000, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .security_type = .cd },
|
||||||
.{ .symbol = "House", .shares = 500000, .open_date = Date.fromYmd(2020, 1, 1), .open_price = 0, .lot_type = .illiquid },
|
.{ .symbol = "House", .shares = 500000, .open_date = Date.fromYmd(2020, 1, 1), .open_price = 0, .security_type = .illiquid },
|
||||||
.{ .symbol = "SPY_CALL", .shares = 2, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 5.50, .lot_type = .option },
|
.{ .symbol = "SPY_CALL", .shares = 2, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 5.50, .security_type = .option },
|
||||||
.{ .symbol = "TSLA", .shares = 0, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .lot_type = .watch },
|
.{ .symbol = "TSLA", .shares = 0, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .security_type = .watch },
|
||||||
};
|
};
|
||||||
const portfolio = Portfolio{ .lots = &lots, .allocator = std.testing.allocator };
|
const portfolio = Portfolio{ .lots = &lots, .allocator = std.testing.allocator };
|
||||||
|
|
||||||
|
|
@ -477,8 +477,8 @@ test "Portfolio watchSymbols" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var lots = [_]Lot{
|
var lots = [_]Lot{
|
||||||
.{ .symbol = "AAPL", .shares = 10, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 150 },
|
.{ .symbol = "AAPL", .shares = 10, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 150 },
|
||||||
.{ .symbol = "TSLA", .shares = 0, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .lot_type = .watch },
|
.{ .symbol = "TSLA", .shares = 0, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .security_type = .watch },
|
||||||
.{ .symbol = "NVDA", .shares = 0, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .lot_type = .watch },
|
.{ .symbol = "NVDA", .shares = 0, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 0, .security_type = .watch },
|
||||||
};
|
};
|
||||||
const portfolio = Portfolio{ .lots = &lots, .allocator = allocator };
|
const portfolio = Portfolio{ .lots = &lots, .allocator = allocator };
|
||||||
const watch = try portfolio.watchSymbols(allocator);
|
const watch = try portfolio.watchSymbols(allocator);
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ fn parseDividendsPage(
|
||||||
.pay_date = parseDateField(obj, "pay_date"),
|
.pay_date = parseDateField(obj, "pay_date"),
|
||||||
.record_date = parseDateField(obj, "record_date"),
|
.record_date = parseDateField(obj, "record_date"),
|
||||||
.frequency = parseFrequency(obj),
|
.frequency = parseFrequency(obj),
|
||||||
.distribution_type = parseDividendType(obj),
|
.type = parseDividendType(obj),
|
||||||
.currency = jsonStr(obj.get("currency")),
|
.currency = jsonStr(obj.get("currency")),
|
||||||
}) catch return provider.ProviderError.OutOfMemory;
|
}) catch return provider.ProviderError.OutOfMemory;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
src/tui.zig
22
src/tui.zig
|
|
@ -1082,7 +1082,7 @@ const App = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (pf.lots) |lot| {
|
for (pf.lots) |lot| {
|
||||||
if (lot.lot_type == .watch) {
|
if (lot.security_type == .watch) {
|
||||||
const sym = lot.priceSymbol();
|
const sym = lot.priceSymbol();
|
||||||
const result = self.svc.getCandles(sym) catch continue;
|
const result = self.svc.getCandles(sym) catch continue;
|
||||||
defer self.allocator.free(result.data);
|
defer self.allocator.free(result.data);
|
||||||
|
|
@ -1263,7 +1263,7 @@ const App = struct {
|
||||||
var lcount: usize = 0;
|
var lcount: usize = 0;
|
||||||
if (self.portfolio) |pf| {
|
if (self.portfolio) |pf| {
|
||||||
for (pf.lots) |lot| {
|
for (pf.lots) |lot| {
|
||||||
if (lot.lot_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) lcount += 1;
|
if (lot.security_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) lcount += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1281,7 +1281,7 @@ const App = struct {
|
||||||
var matching: std.ArrayList(zfin.Lot) = .empty;
|
var matching: std.ArrayList(zfin.Lot) = .empty;
|
||||||
defer matching.deinit(self.allocator);
|
defer matching.deinit(self.allocator);
|
||||||
for (pf.lots) |lot| {
|
for (pf.lots) |lot| {
|
||||||
if (lot.lot_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) {
|
if (lot.security_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) {
|
||||||
matching.append(self.allocator, lot) catch continue;
|
matching.append(self.allocator, lot) catch continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1369,7 +1369,7 @@ const App = struct {
|
||||||
// Watch lots from portfolio file
|
// Watch lots from portfolio file
|
||||||
if (self.portfolio) |pf| {
|
if (self.portfolio) |pf| {
|
||||||
for (pf.lots) |lot| {
|
for (pf.lots) |lot| {
|
||||||
if (lot.lot_type == .watch) {
|
if (lot.security_type == .watch) {
|
||||||
if (watch_seen.contains(lot.priceSymbol())) continue;
|
if (watch_seen.contains(lot.priceSymbol())) continue;
|
||||||
watch_seen.put(lot.priceSymbol(), {}) catch {};
|
watch_seen.put(lot.priceSymbol(), {}) catch {};
|
||||||
self.portfolio_rows.append(self.allocator, .{
|
self.portfolio_rows.append(self.allocator, .{
|
||||||
|
|
@ -1400,7 +1400,7 @@ const App = struct {
|
||||||
.symbol = "Options",
|
.symbol = "Options",
|
||||||
}) catch {};
|
}) catch {};
|
||||||
for (pf.lots) |lot| {
|
for (pf.lots) |lot| {
|
||||||
if (lot.lot_type == .option) {
|
if (lot.security_type == .option) {
|
||||||
self.portfolio_rows.append(self.allocator, .{
|
self.portfolio_rows.append(self.allocator, .{
|
||||||
.kind = .option_row,
|
.kind = .option_row,
|
||||||
.symbol = lot.symbol,
|
.symbol = lot.symbol,
|
||||||
|
|
@ -1419,7 +1419,7 @@ const App = struct {
|
||||||
var cd_lots: std.ArrayList(zfin.Lot) = .empty;
|
var cd_lots: std.ArrayList(zfin.Lot) = .empty;
|
||||||
defer cd_lots.deinit(self.allocator);
|
defer cd_lots.deinit(self.allocator);
|
||||||
for (pf.lots) |lot| {
|
for (pf.lots) |lot| {
|
||||||
if (lot.lot_type == .cd) {
|
if (lot.security_type == .cd) {
|
||||||
cd_lots.append(self.allocator, lot) catch continue;
|
cd_lots.append(self.allocator, lot) catch continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1447,7 +1447,7 @@ const App = struct {
|
||||||
// Per-account cash rows (expanded when cash_total is toggled)
|
// Per-account cash rows (expanded when cash_total is toggled)
|
||||||
if (self.cash_expanded) {
|
if (self.cash_expanded) {
|
||||||
for (pf.lots) |lot| {
|
for (pf.lots) |lot| {
|
||||||
if (lot.lot_type == .cash) {
|
if (lot.security_type == .cash) {
|
||||||
self.portfolio_rows.append(self.allocator, .{
|
self.portfolio_rows.append(self.allocator, .{
|
||||||
.kind = .cash_row,
|
.kind = .cash_row,
|
||||||
.symbol = lot.account orelse "Unknown",
|
.symbol = lot.account orelse "Unknown",
|
||||||
|
|
@ -1472,7 +1472,7 @@ const App = struct {
|
||||||
// Per-asset rows (expanded when illiquid_total is toggled)
|
// Per-asset rows (expanded when illiquid_total is toggled)
|
||||||
if (self.illiquid_expanded) {
|
if (self.illiquid_expanded) {
|
||||||
for (pf.lots) |lot| {
|
for (pf.lots) |lot| {
|
||||||
if (lot.lot_type == .illiquid) {
|
if (lot.security_type == .illiquid) {
|
||||||
self.portfolio_rows.append(self.allocator, .{
|
self.portfolio_rows.append(self.allocator, .{
|
||||||
.kind = .illiquid_row,
|
.kind = .illiquid_row,
|
||||||
.symbol = lot.symbol,
|
.symbol = lot.symbol,
|
||||||
|
|
@ -2160,7 +2160,7 @@ const App = struct {
|
||||||
if (!is_multi) {
|
if (!is_multi) {
|
||||||
if (self.portfolio) |pf| {
|
if (self.portfolio) |pf| {
|
||||||
for (pf.lots) |lot| {
|
for (pf.lots) |lot| {
|
||||||
if (lot.lot_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) {
|
if (lot.security_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) {
|
||||||
const ds = lot.open_date.format(&pos_date_buf);
|
const ds = lot.open_date.format(&pos_date_buf);
|
||||||
const indicator = fmt.capitalGainsIndicator(lot.open_date);
|
const indicator = fmt.capitalGainsIndicator(lot.open_date);
|
||||||
date_col = std.fmt.allocPrint(arena, "{s} {s}", .{ ds, indicator }) catch ds;
|
date_col = std.fmt.allocPrint(arena, "{s} {s}", .{ ds, indicator }) catch ds;
|
||||||
|
|
@ -2175,7 +2175,7 @@ const App = struct {
|
||||||
var common_acct: ?[]const u8 = null;
|
var common_acct: ?[]const u8 = null;
|
||||||
var mixed = false;
|
var mixed = false;
|
||||||
for (pf.lots) |lot| {
|
for (pf.lots) |lot| {
|
||||||
if (lot.lot_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) {
|
if (lot.security_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) {
|
||||||
if (common_acct) |ca| {
|
if (common_acct) |ca| {
|
||||||
const la = lot.account orelse "";
|
const la = lot.account orelse "";
|
||||||
if (!std.mem.eql(u8, ca, la)) {
|
if (!std.mem.eql(u8, ca, la)) {
|
||||||
|
|
@ -3751,7 +3751,7 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, args: []const []co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (pf.lots) |lot| {
|
for (pf.lots) |lot| {
|
||||||
if (lot.lot_type == .watch and !seen.contains(lot.priceSymbol())) {
|
if (lot.security_type == .watch and !seen.contains(lot.priceSymbol())) {
|
||||||
seen.put(lot.priceSymbol(), {}) catch {};
|
seen.put(lot.priceSymbol(), {}) catch {};
|
||||||
watch_syms.append(allocator, lot.priceSymbol()) catch {};
|
watch_syms.append(allocator, lot.priceSymbol()) catch {};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue