Compare commits

..

No commits in common. "3e13faa66f8e40158a91a78e0e2bce75a364a36e" and "d0c13847f553a465d2df073fc70e5406a8164668" have entirely different histories.

4 changed files with 13 additions and 36 deletions

View file

@ -11,8 +11,10 @@ Example: 500 shares of AMZN at $225, with 3 sold calls at $220 strike.
## Human review of analytics modules ## Human review of analytics modules
AI review complete; human review still needed for: AI review complete; human review still needed for:
- `src/analytics/performance.zig` — Morningstar-style trailing returns
- `src/analytics/risk.zig` — Sharpe, volatility, max drawdown, portfolio summary - `src/analytics/risk.zig` — Sharpe, volatility, max drawdown, portfolio summary
- `src/analytics/indicators.zig` — SMA, Bollinger Bands, RSI - `src/analytics/indicators.zig` — SMA, Bollinger Bands, RSI
- `src/models/classification.zig` — Sector/geo/asset-class metadata parsing
Known issues from AI review: Known issues from AI review:
- `risk.zig` uses population variance (divides by n) instead of sample - `risk.zig` uses population variance (divides by n) instead of sample

View file

@ -106,7 +106,3 @@ symbol::ORCBI,asset_class::Bonds,geo::US,pct:num:40
symbol::ORC42,asset_class::US Large Cap,geo::US,pct:num:55.67 symbol::ORC42,asset_class::US Large Cap,geo::US,pct:num:55.67
symbol::ORC42,asset_class::International Developed,geo::International Developed,pct:num:36 symbol::ORC42,asset_class::International Developed,geo::International Developed,pct:num:36
symbol::ORC42,asset_class::Bonds,geo::US,pct:num:8.33 symbol::ORC42,asset_class::Bonds,geo::US,pct:num:8.33
symbol::VFORX,geo::US,asset_class::US Large Cap
symbol::VTTHX,geo::US,asset_class::US Large Cap

View file

@ -3,19 +3,6 @@ const Date = @import("../models/date.zig").Date;
const Candle = @import("../models/candle.zig").Candle; const Candle = @import("../models/candle.zig").Candle;
const Dividend = @import("../models/dividend.zig").Dividend; const Dividend = @import("../models/dividend.zig").Dividend;
/// Minimum holding period (in years) before annualizing returns.
/// Set below 1.0 to handle trading-day snap (e.g. a "1-year" lookback
/// that lands on 362 days due to weekends).
const min_annualize_years = 0.95;
/// Compute CAGR from a total return over a given number of years.
/// Returns null for periods shorter than `min_annualize_years` where
/// extrapolating to a full year would be misleading.
inline fn annualizedReturn(total: f64, years: f64) ?f64 {
if (years < min_annualize_years) return null;
return std.math.pow(f64, 1.0 + total, 1.0 / years) - 1.0;
}
/// Performance calculation results, Morningstar-style. /// Performance calculation results, Morningstar-style.
pub const PerformanceResult = struct { pub const PerformanceResult = struct {
/// Total return over the period (e.g., 0.25 = 25%) /// Total return over the period (e.g., 0.25 = 25%)
@ -53,7 +40,10 @@ fn totalReturnFromAdjCloseSnap(candles: []const Candle, from: Date, to: Date, st
return .{ return .{
.total_return = total, .total_return = total,
.annualized_return = annualizedReturn(total, years), .annualized_return = if (years >= 0.95)
std.math.pow(f64, 1.0 + total, 1.0 / years) - 1.0
else
null,
.from = start.date, .from = start.date,
.to = end.date, .to = end.date,
}; };
@ -114,7 +104,10 @@ fn totalReturnWithDividendsSnap(
return .{ return .{
.total_return = total, .total_return = total,
.annualized_return = annualizedReturn(total, years), .annualized_return = if (years >= 0.95)
std.math.pow(f64, 1.0 + total, 1.0 / years) - 1.0
else
null,
.from = start.date, .from = start.date,
.to = end.date, .to = end.date,
}; };

View file

@ -29,12 +29,9 @@ pub fn init(max_per_window: u32, window_ns: u64) RateLimiter {
}; };
} }
/// Convenience: N requests per minute. /// Convenience: N requests per minute
/// Starts with 1 token (no burst) to stay within provider sliding-window limits.
pub fn perMinute(n: u32) RateLimiter { pub fn perMinute(n: u32) RateLimiter {
var rl = init(n, std.time.ns_per_min); return init(n, std.time.ns_per_min);
rl.tokens = 1.0;
return rl;
} }
/// Convenience: N requests per day /// Convenience: N requests per day
@ -90,19 +87,8 @@ fn refill(self: *RateLimiter) void {
test "rate limiter basic" { test "rate limiter basic" {
var rl = RateLimiter.perMinute(60); var rl = RateLimiter.perMinute(60);
// perMinute starts with 1 token (no burst) // Should have full bucket initially
try std.testing.expect(rl.tryAcquire()); try std.testing.expect(rl.tryAcquire());
// Second call should be rate-limited immediately
try std.testing.expect(!rl.tryAcquire());
}
test "rate limiter perDay keeps full burst" {
var rl = RateLimiter.perDay(25);
// perDay starts with full bucket
for (0..25) |_| {
try std.testing.expect(rl.tryAcquire());
}
try std.testing.expect(!rl.tryAcquire());
} }
test "rate limiter exhaustion" { test "rate limiter exhaustion" {