Ancient

 view release on metacpan or  search on metacpan

t/9053-nvec-valid-finance.t  view on Meta::CPAN

subtest 'present value calculation' => sub {
    # Future cash flows
    my $cash_flows = nvec::new([100, 100, 100, 100, 100]);  # 5 annual payments
    my $rate = 0.05;  # 5% discount rate

    # Discount factors: 1/(1+r)^t for t=1,2,3,4,5
    my @factors;
    for my $t (1..5) {
        push @factors, 1 / ((1 + $rate) ** $t);
    }
    my $discount_factors = nvec::new(\@factors);

    # PV = sum(CF * DF)
    my $pv = $cash_flows->dot($discount_factors);

    # Should be less than sum of cash flows (432.95 for these values)
    ok($pv < $cash_flows->sum(), 'PV < sum of cash flows');
    ok($pv > 400, 'PV in reasonable range');
};

subtest 'compound interest' => sub {
    my $principal = 1000;
    my $rate = 0.05;  # 5% annual
    my $years = 10;

    # Future value = P * (1 + r)^n
    my $growth_factor = (1 + $rate) ** $years;
    my $fv = $principal * $growth_factor;

    within_tolerance($fv, 1000 * (1.05 ** 10), 'compound interest FV');

    # Using nvec: create growth path
    my $time = nvec::range(0, $years + 1);  # 0 to 10
    my $one_plus_r = nvec::fill($years + 1, 1 + $rate);

    # growth[t] = (1+r)^t
    my $growth = $one_plus_r->pow($time->to_array()->[0]);  # pow takes scalar

    # Verify final value
    # Actually let's do this differently - build the growth path
    my @growth_vals = map { (1 + $rate) ** $_ } (0..$years);
    my $growth_vec = nvec::new(\@growth_vals);

    within_tolerance($growth_vec->get($years), (1 + $rate) ** $years, 'growth factor at year 10');
};

subtest 'correlation approximation via dot product' => sub {
    # Standardized returns (mean=0, std=1)
    my $r1 = nvec::new([-1.2, 0.5, 0.3, -0.2, 0.6]);
    my $r2 = nvec::new([-1.0, 0.4, 0.5, -0.1, 0.2]);

    # Correlation ≈ dot(r1, r2) / (n-1) for standardized data
    my $n = $r1->len();
    my $corr = $r1->dot($r2) / ($n - 1);

    # Should be positive (similar patterns)
    ok($corr > 0, 'positive correlation for similar patterns');
    ok($corr < 1, 'correlation < 1');
};

subtest 'value at risk (VaR) - percentile approach' => sub {
    # Simulated returns (sorted for percentile)
    my $returns = nvec::new([-0.05, -0.03, -0.02, -0.01, 0, 0.01, 0.02, 0.03, 0.04, 0.05]);

    # 5% VaR: 5th percentile of returns
    my $sorted = $returns->sort();
    my $var_5pct = $sorted->get(0);  # Worst return in this sample

    within_tolerance($var_5pct, -0.05, 'VaR 5% = worst return');
};

subtest 'position sizing' => sub {
    my $portfolio_value = 100000;
    my $risk_per_trade = 0.01;  # 1% risk

    # Multiple assets with different volatilities
    my $volatilities = nvec::new([0.02, 0.05, 0.03, 0.08]);

    # Position size = (portfolio * risk) / volatility
    my $risk_amount = $portfolio_value * $risk_per_trade;
    my $positions = nvec::fill($volatilities->len(), $risk_amount)->div($volatilities);

    # Higher volatility = smaller position
    ok($positions->get(1) < $positions->get(0), 'higher vol = smaller position');
    ok($positions->get(3) < $positions->get(2), 'highest vol = smallest position');
};

subtest 'Kelly criterion approximation' => sub {
    my $win_prob = 0.6;
    my $win_amount = 1.0;  # Win 1x
    my $lose_amount = 1.0;  # Lose 1x

    # Kelly fraction = (p * b - q) / b
    # where p = win prob, q = lose prob, b = win/lose ratio
    my $b = $win_amount / $lose_amount;
    my $kelly = ($win_prob * $b - (1 - $win_prob)) / $b;

    # Kelly = (0.6 * 1 - 0.4) / 1 = 0.2
    within_tolerance($kelly, 0.2, 'Kelly fraction = 20%');
};

done_testing();



( run in 2.208 seconds using v1.01-cache-2.11-cpan-8dfa8b56332 )