#![allow(clippy::inconsistent_digit_grouping)]
use nanobook::Symbol;
#[test]
fn symbol_truncated_empty() {
let sym = Symbol::from_str_truncated("");
assert_eq!(sym.as_str(), "");
}
#[test]
fn symbol_truncated_exact_8() {
let sym = Symbol::from_str_truncated("12345678");
assert_eq!(sym.as_str(), "12345678");
}
#[test]
fn symbol_truncated_9_bytes() {
let sym = Symbol::from_str_truncated("123456789");
assert_eq!(sym.as_str(), "12345678");
}
#[test]
fn symbol_truncated_long_string() {
let sym = Symbol::from_str_truncated("VERYLONGSYMBOLNAME");
assert_eq!(sym.as_str(), "VERYLONG");
}
#[test]
fn symbol_truncated_unicode_boundary() {
let sym = Symbol::from_str_truncated("1234567Ω");
assert_eq!(sym.as_str(), "1234567");
}
#[test]
fn symbol_truncated_all_ascii_normal() {
let sym = Symbol::from_str_truncated("AAPL");
assert_eq!(sym.as_str(), "AAPL");
}
#[cfg(feature = "portfolio")]
mod backtest {
use nanobook::Symbol;
use nanobook::backtest_bridge::{BarPrices, FillPolicy, backtest_weights};
use nanobook::portfolio::CostModel;
fn aapl() -> Symbol {
Symbol::new("AAPL")
}
fn bar(p: i64) -> BarPrices {
BarPrices {
open: p,
high: p,
low: p,
close: p,
}
}
#[test]
fn mismatched_schedule_lengths() {
let weights = vec![vec![(aapl(), 0.5)]];
let prices = vec![
vec![(aapl(), bar(100_00))],
vec![(aapl(), bar(110_00))], ];
let result = backtest_weights(
&weights,
&prices,
1_000_000_00,
CostModel {
commission_bps: 10.0,
slippage_bps: 0.0,
min_commission: 0,
},
FillPolicy::SignalBarClose,
252.0,
0.0,
);
assert!(result.returns.is_empty());
assert!(result.metrics.is_none());
}
#[test]
fn nan_weight_returns_empty() {
let weights = vec![vec![(aapl(), f64::NAN)]];
let prices = vec![vec![(aapl(), bar(100_00))]];
let result = backtest_weights(
&weights,
&prices,
1_000_000_00,
CostModel {
commission_bps: 10.0,
slippage_bps: 0.0,
min_commission: 0,
},
FillPolicy::SignalBarClose,
252.0,
0.0,
);
assert!(result.returns.is_empty());
}
#[test]
fn inf_weight_returns_empty() {
let weights = vec![vec![(aapl(), f64::INFINITY)]];
let prices = vec![vec![(aapl(), bar(100_00))]];
let result = backtest_weights(
&weights,
&prices,
1_000_000_00,
CostModel {
commission_bps: 10.0,
slippage_bps: 0.0,
min_commission: 0,
},
FillPolicy::SignalBarClose,
252.0,
0.0,
);
assert!(result.returns.is_empty());
}
#[test]
fn negative_price_returns_empty() {
let weights = vec![vec![(aapl(), 0.5)]];
let prices = vec![vec![(
aapl(),
BarPrices {
open: -100,
high: -100,
low: -100,
close: -100,
},
)]];
let result = backtest_weights(
&weights,
&prices,
1_000_000_00,
CostModel {
commission_bps: 10.0,
slippage_bps: 0.0,
min_commission: 0,
},
FillPolicy::SignalBarClose,
252.0,
0.0,
);
assert!(result.returns.is_empty());
}
#[test]
fn zero_initial_cash_returns_empty() {
let weights = vec![vec![(aapl(), 0.5)]];
let prices = vec![vec![(aapl(), bar(100_00))]];
let result = backtest_weights(
&weights,
&prices,
0,
CostModel {
commission_bps: 10.0,
slippage_bps: 0.0,
min_commission: 0,
},
FillPolicy::SignalBarClose,
252.0,
0.0,
);
assert!(result.returns.is_empty());
}
#[test]
fn negative_initial_cash_returns_empty() {
let weights = vec![vec![(aapl(), 0.5)]];
let prices = vec![vec![(aapl(), bar(100_00))]];
let result = backtest_weights(
&weights,
&prices,
-1,
CostModel {
commission_bps: 10.0,
slippage_bps: 0.0,
min_commission: 0,
},
FillPolicy::SignalBarClose,
252.0,
0.0,
);
assert!(result.returns.is_empty());
}
#[test]
fn large_commission_bps_no_guard() {
let weights = vec![vec![(aapl(), 0.5)]];
let prices = vec![vec![(aapl(), bar(100_00))]];
let result = backtest_weights(
&weights,
&prices,
1_000_000_00,
CostModel {
commission_bps: 10_001.0,
slippage_bps: 0.0,
min_commission: 0,
},
FillPolicy::SignalBarClose,
252.0,
0.0,
);
assert!(!result.equity_curve.is_empty());
}
#[test]
fn empty_schedules_still_work() {
let result = backtest_weights(
&[],
&[],
1_000_000_00,
CostModel {
commission_bps: 10.0,
slippage_bps: 0.0,
min_commission: 0,
},
FillPolicy::SignalBarClose,
252.0,
0.0,
);
assert!(result.returns.is_empty());
assert_eq!(result.equity_curve.len(), 1);
assert_eq!(result.final_cash, 1_000_000_00);
}
}
#[cfg(feature = "portfolio")]
mod portfolio_safety {
use nanobook::Symbol;
use nanobook::portfolio::{CostModel, Portfolio};
fn aapl() -> Symbol {
Symbol::new("AAPL")
}
#[test]
fn rebalance_simple_with_zero_equity_is_noop() {
let mut portfolio = Portfolio::new(100_00, CostModel::zero());
let prices = [(aapl(), 100_00)];
portfolio.rebalance_simple(&[(aapl(), 1.0)], &prices);
let zero_prices = [(aapl(), 0)];
let equity = portfolio.total_equity(&zero_prices);
assert_eq!(equity, 0);
portfolio.rebalance_simple(&[(aapl(), 0.5)], &zero_prices);
}
}