Compare commits
3 commits
d0c13847f5
...
3e13faa66f
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e13faa66f | |||
| 7fabc794c1 | |||
| 724a13a012 |
4 changed files with 36 additions and 13 deletions
2
TODO.md
2
TODO.md
|
|
@ -11,10 +11,8 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -106,3 +106,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,19 @@ 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%)
|
||||||
|
|
@ -40,10 +53,7 @@ fn totalReturnFromAdjCloseSnap(candles: []const Candle, from: Date, to: Date, st
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.total_return = total,
|
.total_return = total,
|
||||||
.annualized_return = if (years >= 0.95)
|
.annualized_return = annualizedReturn(total, years),
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
@ -104,10 +114,7 @@ fn totalReturnWithDividendsSnap(
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.total_return = total,
|
.total_return = total,
|
||||||
.annualized_return = if (years >= 0.95)
|
.annualized_return = annualizedReturn(total, years),
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,12 @@ 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 {
|
||||||
return init(n, std.time.ns_per_min);
|
var rl = init(n, std.time.ns_per_min);
|
||||||
|
rl.tokens = 1.0;
|
||||||
|
return rl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience: N requests per day
|
/// Convenience: N requests per day
|
||||||
|
|
@ -87,8 +90,19 @@ fn refill(self: *RateLimiter) void {
|
||||||
|
|
||||||
test "rate limiter basic" {
|
test "rate limiter basic" {
|
||||||
var rl = RateLimiter.perMinute(60);
|
var rl = RateLimiter.perMinute(60);
|
||||||
// Should have full bucket initially
|
// perMinute starts with 1 token (no burst)
|
||||||
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" {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue