Function bodies 248 total
new function · rust · L26-L39 (14 LOC)src/backtest/engine.rs
pub fn new(
config: FuturesConfig,
learning: LearningEngine,
seed: u64,
) -> Self {
Self {
portfolio: Portfolio::new(config),
learning,
rng: StdRng::seed_from_u64(seed),
trades: TradeManager::new(),
max_seen_index: 0,
funding_rates: HashMap::new(),
}
}set_funding_rates function · rust · L42-L46 (5 LOC)src/backtest/engine.rs
pub fn set_funding_rates(&mut self, rates: &[FundingRate]) {
self.funding_rates = rates.iter()
.map(|r| (r.funding_time, r.funding_rate))
.collect();
}run function · rust · L55-L195 (141 LOC)src/backtest/engine.rs
pub fn run(
&mut self,
symbol: &Symbol,
candles: &[Candle],
warmup: usize,
) -> BacktestResult {
let lookback = 60;
if candles.len() < warmup + lookback {
return BacktestResult::empty();
}
// Verify temporal order
for i in 1..candles.len() {
assert!(
candles[i].open_time >= candles[i - 1].open_time,
"LOOK-AHEAD VIOLATION: candles not in temporal order at index {}",
i
);
}
for i in warmup..candles.len() {
assert!(
i >= self.max_seen_index,
"LOOK-AHEAD VIOLATION: processing index {} after seeing {}",
i, self.max_seen_index
);
self.max_seen_index = i;
let candle = &candles[i];
// Tick adaptive parameter engine
self.learning.adaptive.tick_candle();
// Update portfolio mark-to-mexecute_decision function · rust · L196-L265 (70 LOC)src/backtest/engine.rs
fn execute_decision(
&mut self,
symbol: &Symbol,
decision: &TradingDecision,
candle: &Candle,
atr: f64,
) {
match decision.signal {
TradeSignal::Hold => {}
TradeSignal::Close => {
if self.portfolio.has_position() {
if let Some(record) = self.portfolio.close_position(
candle.close, candle.close_time, self.trades.candles_held,
self.trades.max_adverse, &decision.strategy_name,
self.trades.accumulated_funding,
) {
self.trades.on_exit(symbol, &record, &mut self.learning);
}
self.trades.reset(&self.learning);
}
}
TradeSignal::Long => {
if self.portfolio.has_position() {
let pos_side = self.portfolio.position.as_ref().unwrap().side;
apply_funding_fee function · rust · L268-L301 (34 LOC)src/backtest/engine.rs
fn apply_funding_fee(&mut self, candle: &Candle) {
let pos = match &self.portfolio.position {
Some(p) => p,
None => return,
};
let funding_interval_ms: i64 = 8 * 3600 * 1000;
let first_funding = (candle.open_time / funding_interval_ms) * funding_interval_ms;
let mut funding_time = first_funding;
while funding_time <= candle.close_time {
if funding_time >= candle.open_time && funding_time <= candle.close_time {
let rate = self.funding_rates.get(&funding_time)
.copied()
.unwrap_or(0.0001);
let notional = pos.size * pos.entry_price;
let fee = match pos.side {
PositionSide::Long => notional * rate,
PositionSide::Short => -notional * rate,
PositionSide::Flat => 0.0,
};
if fee > 0.0 {
self.trades.add_fundbuild_result function · rust · L302-L326 (25 LOC)src/backtest/engine.rs
fn build_result(&self) -> BacktestResult {
let pnls: Vec<f64> = self.portfolio.trade_log.iter().map(|t| t.pnl_pct).collect();
let periods: Vec<usize> = self.portfolio.trade_log.iter().map(|t| t.holding_periods).collect();
let days = if self.portfolio.equity_curve.len() > 1 {
self.portfolio.equity_curve.len() as f64 / 96.0
} else {
1.0
};
let performance = metrics::calculate_metrics(
&self.portfolio.equity_curve,
&pnls,
&periods,
days,
);
BacktestResult {
performance,
trades: self.portfolio.trade_log.clone(),
final_equity: self.portfolio.current_equity(),
health: self.learning.health_report(),
}
}empty function · rust · L338-L354 (17 LOC)src/backtest/engine.rs
pub fn empty() -> Self {
Self {
performance: metrics::PerformanceMetrics {
total_return_pct: 0.0, annualized_return_pct: 0.0,
sharpe_ratio: 0.0, sortino_ratio: 0.0,
max_drawdown_pct: 0.0, calmar_ratio: 0.0,
win_rate: 0.0, profit_factor: 0.0,
total_trades: 0, winning_trades: 0, losing_trades: 0,
avg_win_pct: 0.0, avg_loss_pct: 0.0,
avg_holding_periods: 0.0, max_consecutive_losses: 0,
equity_curve: vec![],
},
trades: vec![],
final_equity: 0.0,
health: std::collections::HashMap::new(),
}
}Powered by Repobility — scan your code at https://repobility.com
print_summary function · rust · L355-L382 (28 LOC)src/backtest/engine.rs
pub fn print_summary(&self) {
let p = &self.performance;
let total_tx_fees: f64 = self.trades.iter().map(|t| t.fees_paid).sum();
let total_funding: f64 = self.trades.iter().map(|t| t.funding_fees_paid).sum();
println!("\n{}", "=".repeat(60));
println!(" BACKTEST RESULTS");
println!("{}", "=".repeat(60));
println!(" Total Return: {:>10.2}%", p.total_return_pct);
println!(" Annualized Return: {:>10.2}%", p.annualized_return_pct);
println!(" Sharpe Ratio: {:>10.2}", p.sharpe_ratio);
println!(" Sortino Ratio: {:>10.2}", p.sortino_ratio);
println!(" Max Drawdown: {:>10.2}%", p.max_drawdown_pct);
println!(" Calmar Ratio: {:>10.2}", p.calmar_ratio);
println!(" Win Rate: {:>10.2}%", p.win_rate * 100.0);
println!(" Profit Factor: {:>10.2}", p.profit_factor);
println!(" Total Trades: {:>10}", p.total_trades);
make_trending_candles function · rust · L389-L406 (18 LOC)src/backtest/engine.rs
fn make_trending_candles(n: usize, start_price: f64, trend: f64) -> Vec<Candle> {
(0..n).map(|i| {
let price = start_price + i as f64 * trend;
let noise = (i as f64 * 0.1).sin() * 0.5;
Candle {
open_time: i as i64 * 900_000,
open: price - 0.3 + noise,
high: price + 1.0 + noise.abs(),
low: price - 1.0 - noise.abs(),
close: price + noise,
volume: 1000.0 + (i as f64 * 10.0),
close_time: (i as i64 + 1) * 900_000 - 1,
quote_volume: price * 1000.0,
trades: 500,
}
}).collect()
}test_backtest_runs_without_panic function · rust · L409-L423 (15 LOC)src/backtest/engine.rs
fn test_backtest_runs_without_panic() {
let learning = LearningEngine::new(
vec!["BTCUSDT".into()],
LearnerConfig::default(),
);
let mut bt = BacktestEngine::new(
FuturesConfig::default(),
learning,
42,
);
let candles = make_trending_candles(200, 50000.0, 10.0);
let result = bt.run(&Symbol("BTCUSDT".into()), &candles, 60);
assert!(result.final_equity > 0.0);
assert!(!result.performance.equity_curve.is_empty());
}test_temporal_order_enforced function · rust · L426-L438 (13 LOC)src/backtest/engine.rs
fn test_temporal_order_enforced() {
let learning = LearningEngine::new(
vec!["BTCUSDT".into()],
LearnerConfig::default(),
);
let mut bt = BacktestEngine::new(
FuturesConfig::default(),
learning,
42,
);
let candles = make_trending_candles(200, 50000.0, 10.0);
let _result = bt.run(&Symbol("BTCUSDT".into()), &candles, 60);
}test_unordered_candles_rejected function · rust · L442-L455 (14 LOC)src/backtest/engine.rs
fn test_unordered_candles_rejected() {
let learning = LearningEngine::new(
vec!["BTCUSDT".into()],
LearnerConfig::default(),
);
let mut bt = BacktestEngine::new(
FuturesConfig::default(),
learning,
42,
);
let mut candles = make_trending_candles(200, 50000.0, 10.0);
candles.swap(100, 50);
let _result = bt.run(&Symbol("BTCUSDT".into()), &candles, 60);
}new function · rust · L45-L56 (12 LOC)src/backtest/portfolio.rs
pub fn new(config: FuturesConfig) -> Self {
let equity = config.initial_equity;
Self {
equity,
position: None,
config,
realized_pnl: 0.0,
trade_log: Vec::new(),
equity_curve: vec![equity],
peak_equity: equity,
}
}open_position function · rust · L59-L97 (39 LOC)src/backtest/portfolio.rs
pub fn open_position(
&mut self,
symbol: &Symbol,
side: PositionSide,
price: f64,
size_fraction: f64,
strategy: &str,
timestamp: i64,
) -> bool {
if self.position.is_some() { return false; }
if side == PositionSide::Flat { return false; }
let size_fraction = size_fraction.clamp(0.05, 0.5);
let notional = self.equity * size_fraction * self.config.leverage;
let size = notional / price;
// Apply slippage
let slippage = price * self.config.slippage_bps / 10_000.0;
let entry_price = match side {
PositionSide::Long => price + slippage,
PositionSide::Short => price - slippage,
PositionSide::Flat => unreachable!(),
};
// Pay taker fee on entry
let fee = notional * self.config.taker_fee;
self.equity -= fee;
self.position = Some(Position {
symbol: symbol.clone(),
sideclose_position function · rust · L100-L171 (72 LOC)src/backtest/portfolio.rs
pub fn close_position(
&mut self,
price: f64,
timestamp: i64,
candles_held: usize,
max_adverse: f64,
strategy: &str,
accumulated_funding: f64,
) -> Option<TradeRecord> {
let pos = self.position.take()?;
// Apply slippage
let slippage = price * self.config.slippage_bps / 10_000.0;
let exit_price = match pos.side {
PositionSide::Long => price - slippage,
PositionSide::Short => price + slippage,
PositionSide::Flat => return None,
};
let notional = pos.size * pos.entry_price;
// Calculate P&L
let pnl = match pos.side {
PositionSide::Long => pos.size * (exit_price - pos.entry_price),
PositionSide::Short => pos.size * (pos.entry_price - exit_price),
PositionSide::Flat => 0.0,
};
// Pay taker fee on exit
let exit_notional = pos.size * exit_price;
let exit_fee =Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
update_mark function · rust · L174-L197 (24 LOC)src/backtest/portfolio.rs
pub fn update_mark(&mut self, price: f64) -> f64 {
let unrealized = if let Some(ref mut pos) = self.position {
match pos.side {
PositionSide::Long => pos.size * (price - pos.entry_price),
PositionSide::Short => pos.size * (pos.entry_price - price),
PositionSide::Flat => 0.0,
}
} else {
0.0
};
if let Some(ref mut pos) = self.position {
pos.unrealized_pnl = unrealized;
}
let current_equity = self.equity + unrealized;
self.equity_curve.push(current_equity);
if current_equity > self.peak_equity {
self.peak_equity = current_equity;
}
current_equity
}check_exits function · rust · L200-L224 (25 LOC)src/backtest/portfolio.rs
pub fn check_exits(&self, candle: &Candle, sl: Option<f64>, tp: Option<f64>) -> Option<(f64, &str)> {
let pos = self.position.as_ref()?;
match pos.side {
PositionSide::Long => {
if let Some(sl) = sl {
if candle.low <= sl { return Some((sl, "stop_loss")); }
}
if let Some(tp) = tp {
if candle.high >= tp { return Some((tp, "take_profit")); }
}
}
PositionSide::Short => {
if let Some(sl) = sl {
if candle.high >= sl { return Some((sl, "stop_loss")); }
}
if let Some(tp) = tp {
if candle.low <= tp { return Some((tp, "take_profit")); }
}
}
PositionSide::Flat => {}
}
None
}check_liquidation function · rust · L227-L243 (17 LOC)src/backtest/portfolio.rs
pub fn check_liquidation(&self, price: f64) -> bool {
let pos = match &self.position {
Some(p) => p,
None => return false,
};
let notional = pos.size * pos.entry_price;
let margin = notional / self.config.leverage;
let unrealized = match pos.side {
PositionSide::Long => pos.size * (price - pos.entry_price),
PositionSide::Short => pos.size * (pos.entry_price - price),
PositionSide::Flat => 0.0,
};
// Liquidation when loss exceeds margin (simplified)
unrealized < -margin * 0.95
}has_position function · rust · L244-L247 (4 LOC)src/backtest/portfolio.rs
pub fn has_position(&self) -> bool {
self.position.is_some()
}current_equity function · rust · L248-L253 (6 LOC)src/backtest/portfolio.rs
pub fn current_equity(&self) -> f64 {
self.equity + self.position.as_ref()
.map(|p| p.unrealized_pnl)
.unwrap_or(0.0)
}default_portfolio function · rust · L259-L262 (4 LOC)src/backtest/portfolio.rs
fn default_portfolio() -> Portfolio {
Portfolio::new(FuturesConfig::default())
}test_open_close_long_profitable function · rust · L265-L275 (11 LOC)src/backtest/portfolio.rs
fn test_open_close_long_profitable() {
let mut port = default_portfolio();
let sym = Symbol("BTCUSDT".into());
port.open_position(&sym, PositionSide::Long, 50000.0, 0.1, "test", 0);
assert!(port.has_position());
let record = port.close_position(51000.0, 1000, 5, 0.5, "test", 0.0).unwrap();
assert!(record.pnl > 0.0, "Long from 50k to 51k should be profitable");
assert!(!port.has_position());
}test_open_close_short_profitable function · rust · L278-L285 (8 LOC)src/backtest/portfolio.rs
fn test_open_close_short_profitable() {
let mut port = default_portfolio();
let sym = Symbol("BTCUSDT".into());
port.open_position(&sym, PositionSide::Short, 50000.0, 0.1, "test", 0);
let record = port.close_position(49000.0, 1000, 5, 0.5, "test", 0.0).unwrap();
assert!(record.pnl > 0.0, "Short from 50k to 49k should be profitable");
}Source: Repobility analyzer · https://repobility.com
test_fees_reduce_pnl function · rust · L288-L297 (10 LOC)src/backtest/portfolio.rs
fn test_fees_reduce_pnl() {
let mut port = default_portfolio();
let sym = Symbol("BTCUSDT".into());
// Open and close at same price
port.open_position(&sym, PositionSide::Long, 50000.0, 0.1, "test", 0);
let record = port.close_position(50000.0, 1000, 1, 0.0, "test", 0.0).unwrap();
// Should be slightly negative due to fees + slippage
assert!(record.pnl < 0.0, "Round-trip at same price should be negative due to fees");
}test_cannot_double_open function · rust · L300-L306 (7 LOC)src/backtest/portfolio.rs
fn test_cannot_double_open() {
let mut port = default_portfolio();
let sym = Symbol("BTCUSDT".into());
assert!(port.open_position(&sym, PositionSide::Long, 50000.0, 0.1, "test", 0));
assert!(!port.open_position(&sym, PositionSide::Long, 50000.0, 0.1, "test", 1));
}test_stop_loss_long function · rust · L309-L323 (15 LOC)src/backtest/portfolio.rs
fn test_stop_loss_long() {
let mut port = default_portfolio();
let sym = Symbol("BTCUSDT".into());
port.open_position(&sym, PositionSide::Long, 50000.0, 0.1, "test", 0);
let candle = Candle {
open_time: 0, open: 49500.0, high: 49600.0, low: 48000.0,
close: 48500.0, volume: 1000.0, close_time: 0,
quote_volume: 0.0, trades: 0,
};
let exit = port.check_exits(&candle, Some(49000.0), Some(55000.0));
assert!(exit.is_some());
assert_eq!(exit.unwrap().1, "stop_loss");
}test_equity_curve_tracking function · rust · L326-L331 (6 LOC)src/backtest/portfolio.rs
fn test_equity_curve_tracking() {
let mut port = default_portfolio();
port.update_mark(50000.0);
port.update_mark(50000.0);
assert!(port.equity_curve.len() >= 3); // initial + 2 updates
}test_liquidation_check function · rust · L334-L348 (15 LOC)src/backtest/portfolio.rs
fn test_liquidation_check() {
let mut port = default_portfolio();
let sym = Symbol("BTCUSDT".into());
// Use max position size 0.5 with 3x leverage
port.open_position(&sym, PositionSide::Long, 50000.0, 0.5, "test", 0);
// With 3x leverage, position notional = equity * 0.5 * 3 = 15000
// margin = 15000/3 = 5000
// Liquidation when unrealized loss > 95% of margin = 4750
// size = 15000 / 50000 = 0.3 BTC (adjusted by slippage)
// Loss at price P = 0.3 * (50000 - P)
// Need: 0.3 * (50000 - P) > 4750 => P < 50000 - 15833 = 34167
assert!(!port.check_liquidation(45000.0)); // 10% drop, no liq
assert!(port.check_liquidation(30000.0)); // 40% drop, should liquidate
}walk_forward_validation function · rust · L6-L67 (62 LOC)src/backtest/validation.rs
pub fn walk_forward_validation(
symbol: &Symbol,
candles: &[Candle],
config: &FuturesConfig,
learner_config: &LearnerConfig,
n_folds: usize,
train_ratio: f64,
) -> ValidationResult {
let total = candles.len();
let fold_size = total / n_folds;
let warmup = 60;
if fold_size < warmup * 2 {
return ValidationResult {
fold_results: vec![],
is_valid: false,
reasons: vec!["Insufficient data for walk-forward validation".into()],
};
}
let mut fold_results = Vec::new();
for fold in 0..n_folds {
let start = fold * fold_size;
let end = (start + fold_size).min(total);
let split = start + ((end - start) as f64 * train_ratio) as usize;
if split <= start + warmup || end <= split + warmup {
continue;
}
let train_data = &candles[start..split];
let test_data = &candles[start..end]; // test includes train for indicator warmup
permutation_test function · rust · L70-L116 (47 LOC)src/backtest/validation.rs
pub fn permutation_test(
trade_pnls: &[f64],
actual_return: f64,
n_permutations: usize,
seed: u64,
) -> PermutationResult {
use rand::SeedableRng;
use rand::seq::SliceRandom;
if trade_pnls.is_empty() {
return PermutationResult {
actual_return,
mean_random_return: 0.0,
p_value: 1.0,
is_significant: false,
percentile: 50.0,
};
}
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let mut random_returns = Vec::with_capacity(n_permutations);
for _ in 0..n_permutations {
let mut shuffled = trade_pnls.to_vec();
shuffled.shuffle(&mut rng);
let cum_return: f64 = shuffled.iter()
.fold(1.0, |acc, &pnl| acc * (1.0 + pnl / 100.0));
random_returns.push((cum_return - 1.0) * 100.0);
}
random_returns.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mean_random = random_returns.iter().sum::<f64>() / random_returns.len() as foverfitting_check function · rust · L119-L164 (46 LOC)src/backtest/validation.rs
pub fn overfitting_check(
train_return: f64,
test_return: f64,
train_sharpe: f64,
test_sharpe: f64,
) -> OverfittingResult {
let mut warnings = Vec::new();
// Return degradation
let return_degradation = if train_return > 0.0 {
1.0 - test_return / train_return
} else {
0.0
};
if return_degradation > 0.5 {
warnings.push("High return degradation (>50%) from train to test".into());
}
// Sharpe degradation
let sharpe_degradation = if train_sharpe > 0.0 {
1.0 - test_sharpe / train_sharpe
} else {
0.0
};
if sharpe_degradation > 0.5 {
warnings.push("Sharpe ratio degrades >50% out-of-sample".into());
}
// Train too good to be true
if train_sharpe > 5.0 {
warnings.push(format!("Suspiciously high train Sharpe: {:.2}", train_sharpe));
}
if train_return > 100.0 {
warnings.push(format!("Suspiciously high train return: {:.1}%", train_return));
}Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
validate_results function · rust · L165-L214 (50 LOC)src/backtest/validation.rs
fn validate_results(folds: &[FoldResult]) -> ValidationResult {
if folds.is_empty() {
return ValidationResult {
fold_results: vec![],
is_valid: false,
reasons: vec!["No folds completed".into()],
};
}
let mut reasons = Vec::new();
// Check if majority of folds are profitable
let profitable_folds = folds.iter().filter(|f| f.test_return > 0.0).count();
if profitable_folds < folds.len() / 2 {
reasons.push(format!(
"Only {}/{} folds profitable out-of-sample",
profitable_folds, folds.len()
));
}
// Check for consistent returns across folds
let returns: Vec<f64> = folds.iter().map(|f| f.test_return).collect();
let mean_return = returns.iter().sum::<f64>() / returns.len() as f64;
let variance = returns.iter().map(|r| (r - mean_return).powi(2)).sum::<f64>()
/ returns.len() as f64;
let cv = if mean_return.abs() > 0.01 { variance.sqrt() / mean_retprint_summary function · rust · L252-L269 (18 LOC)src/backtest/validation.rs
pub fn print_summary(&self) {
println!("\n--- Walk-Forward Validation ---");
println!("Valid: {}", self.is_valid);
for fold in &self.fold_results {
println!(
" Fold {}: train={:+.2}% test={:+.2}% sharpe={:.2} dd={:.1}% trades={} wr={:.0}%",
fold.fold, fold.train_return, fold.test_return,
fold.test_sharpe, fold.test_drawdown,
fold.test_trades, fold.test_win_rate * 100.0,
);
}
if !self.reasons.is_empty() {
println!(" Warnings:");
for r in &self.reasons {
println!(" - {}", r);
}
}
}print_summary function · rust · L273-L280 (8 LOC)src/backtest/validation.rs
pub fn print_summary(&self) {
println!("\n--- Permutation Test ---");
println!(" Actual return: {:+.2}%", self.actual_return);
println!(" Mean random return: {:+.2}%", self.mean_random_return);
println!(" P-value: {:.4}", self.p_value);
println!(" Significant (p<0.05): {}", self.is_significant);
println!(" Percentile: {:.1}%", self.percentile);
}print_summary function · rust · L284-L292 (9 LOC)src/backtest/validation.rs
pub fn print_summary(&self) {
println!("\n--- Overfitting Check ---");
println!(" Return degradation: {:.0}%", self.return_degradation * 100.0);
println!(" Sharpe degradation: {:.0}%", self.sharpe_degradation * 100.0);
println!(" Likely overfit: {}", self.likely_overfit);
for w in &self.warnings {
println!(" WARNING: {}", w);
}
}test_permutation_test function · rust · L300-L305 (6 LOC)src/backtest/validation.rs
fn test_permutation_test() {
let pnls = vec![2.0, -1.0, 3.0, -0.5, 1.5, -1.0, 2.0, -0.5, 1.0, 0.5];
let actual_return: f64 = pnls.iter().sum();
let result = permutation_test(&pnls, actual_return, 1000, 42);
assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
}test_overfitting_check_good function · rust · L308-L311 (4 LOC)src/backtest/validation.rs
fn test_overfitting_check_good() {
let result = overfitting_check(10.0, 8.0, 2.0, 1.8);
assert!(!result.likely_overfit);
}test_overfitting_check_bad function · rust · L314-L318 (5 LOC)src/backtest/validation.rs
fn test_overfitting_check_bad() {
let result = overfitting_check(50.0, 5.0, 5.5, 0.5);
assert!(result.likely_overfit);
assert!(!result.warnings.is_empty());
}save_to_csv function · rust · L5-L29 (25 LOC)src/data/cache.rs
pub fn save_to_csv(candles: &[Candle], path: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut writer = csv::Writer::from_path(path)?;
writer.write_record(&[
"open_time", "open", "high", "low", "close",
"volume", "close_time", "quote_volume", "trades",
])?;
for c in candles {
writer.write_record(&[
c.open_time.to_string(),
c.open.to_string(),
c.high.to_string(),
c.low.to_string(),
c.close.to_string(),
c.volume.to_string(),
c.close_time.to_string(),
c.quote_volume.to_string(),
c.trades.to_string(),
])?;
}
writer.flush()?;
Ok(())
}Powered by Repobility — scan your code at https://repobility.com
load_from_csv function · rust · L32-L64 (33 LOC)src/data/cache.rs
pub fn load_from_csv(path: &str) -> Result<Vec<Candle>, Box<dyn std::error::Error>> {
if !Path::new(path).exists() {
return Err(format!("Cache file not found: {}", path).into());
}
let mut reader = csv::Reader::from_path(path)?;
let mut candles = Vec::new();
for result in reader.records() {
let record = result?;
let candle = Candle {
open_time: record[0].parse()?,
open: record[1].parse()?,
high: record[2].parse()?,
low: record[3].parse()?,
close: record[4].parse()?,
volume: record[5].parse()?,
close_time: record[6].parse()?,
quote_volume: record[7].parse()?,
trades: record[8].parse()?,
};
candles.push(candle);
}
// Verify temporal ordering
for i in 1..candles.len() {
if candles[i].open_time < candles[i - 1].open_time {
return Err("Cache file has non-monotonic timestamps".into());
cache_path function · rust · L67-L69 (3 LOC)src/data/cache.rs
pub fn cache_path(symbol: &str, data_dir: &str) -> String {
format!("{}/{}_15m.csv", data_dir, symbol.to_lowercase())
}load_or_fetch function · rust · L72-L104 (33 LOC)src/data/cache.rs
pub async fn load_or_fetch(
symbol: &str,
days: i64,
data_dir: &str,
) -> Result<Vec<Candle>, Box<dyn std::error::Error>> {
let path = cache_path(symbol, data_dir);
// Try cache first
if let Ok(candles) = load_from_csv(&path) {
if !candles.is_empty() {
let age_ms = chrono::Utc::now().timestamp_millis()
- candles.last().unwrap().close_time;
let age_hours = age_ms as f64 / 3_600_000.0;
// Use cache if less than 1 hour old
if age_hours < 1.0 {
println!("Using cached data for {} ({} candles, {:.1}h old)",
symbol, candles.len(), age_hours);
return Ok(candles);
}
}
}
// Fetch from Binance
let candles = super::fetcher::fetch_last_n_days(symbol, days).await?;
// Save to cache
std::fs::create_dir_all(data_dir)?;
save_to_csv(&candles, &path)?;
println!("Cached {} candles for {} at {}", candles.len()save_funding_csv function · rust · L107-L119 (13 LOC)src/data/cache.rs
pub fn save_funding_csv(rates: &[FundingRate], path: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut writer = csv::Writer::from_path(path)?;
writer.write_record(&["symbol", "funding_time", "funding_rate"])?;
for r in rates {
writer.write_record(&[
r.symbol.clone(),
r.funding_time.to_string(),
r.funding_rate.to_string(),
])?;
}
writer.flush()?;
Ok(())
}load_funding_csv function · rust · L122-L138 (17 LOC)src/data/cache.rs
pub fn load_funding_csv(path: &str) -> Result<Vec<FundingRate>, Box<dyn std::error::Error>> {
if !Path::new(path).exists() {
return Err(format!("Funding cache not found: {}", path).into());
}
let mut reader = csv::Reader::from_path(path)?;
let mut rates = Vec::new();
for result in reader.records() {
let record = result?;
rates.push(FundingRate {
symbol: record[0].to_string(),
funding_time: record[1].parse()?,
funding_rate: record[2].parse()?,
});
}
rates.sort_by_key(|r| r.funding_time);
Ok(rates)
}funding_cache_path function · rust · L141-L143 (3 LOC)src/data/cache.rs
pub fn funding_cache_path(symbol: &str, data_dir: &str) -> String {
format!("{}/{}_funding.csv", data_dir, symbol.to_lowercase())
}load_or_fetch_funding function · rust · L146-L171 (26 LOC)src/data/cache.rs
pub async fn load_or_fetch_funding(
symbol: &str,
days: i64,
data_dir: &str,
) -> Result<Vec<FundingRate>, Box<dyn std::error::Error>> {
let path = funding_cache_path(symbol, data_dir);
if let Ok(rates) = load_funding_csv(&path) {
if !rates.is_empty() {
let age_ms = chrono::Utc::now().timestamp_millis()
- rates.last().unwrap().funding_time;
let age_hours = age_ms as f64 / 3_600_000.0;
if age_hours < 1.0 {
println!("Using cached funding for {} ({} rates, {:.1}h old)",
symbol, rates.len(), age_hours);
return Ok(rates);
}
}
}
let rates = super::fetcher::fetch_funding_last_n_days(symbol, days).await?;
std::fs::create_dir_all(data_dir)?;
save_funding_csv(&rates, &path)?;
println!("Cached {} funding rates for {}", rates.len(), symbol);
Ok(rates)
}test_csv_round_trip function · rust · L178-L201 (24 LOC)src/data/cache.rs
fn test_csv_round_trip() {
let candles = vec![
Candle {
open_time: 1000, open: 100.0, high: 101.0, low: 99.0,
close: 100.5, volume: 500.0, close_time: 1999,
quote_volume: 50000.0, trades: 100,
},
Candle {
open_time: 2000, open: 100.5, high: 102.0, low: 100.0,
close: 101.0, volume: 600.0, close_time: 2999,
quote_volume: 60000.0, trades: 120,
},
];
let dir = tempfile::tempdir().unwrap();
let path = format!("{}/test.csv", dir.path().display());
save_to_csv(&candles, &path).unwrap();
let loaded = load_from_csv(&path).unwrap();
assert_eq!(loaded.len(), 2);
assert_eq!(loaded[0].open_time, 1000);
assert!((loaded[1].close - 101.0).abs() < 1e-10);
}Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
fetch_candles function · rust · L8-L72 (65 LOC)src/data/fetcher.rs
pub async fn fetch_candles(
symbol: &str,
interval: &str,
start_time: i64,
end_time: i64,
) -> Result<Vec<Candle>, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let mut all_candles = Vec::new();
let mut current_start = start_time;
let limit = 1500; // Binance max per request
while current_start < end_time {
let resp = client.get(BINANCE_FUTURES_URL)
.query(&[
("symbol", symbol),
("interval", interval),
("startTime", ¤t_start.to_string()),
("endTime", &end_time.to_string()),
("limit", &limit.to_string()),
])
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(format!("Binance API error {}: {}", status, body).into());
}
let data: Vec<Vec<serde_jfetch_last_n_days function · rust · L75-L88 (14 LOC)src/data/fetcher.rs
pub async fn fetch_last_n_days(
symbol: &str,
days: i64,
) -> Result<Vec<Candle>, Box<dyn std::error::Error>> {
let now = Utc::now();
let start = now - Duration::days(days);
fetch_candles(
symbol,
"15m",
start.timestamp_millis(),
now.timestamp_millis(),
).await
}fetch_multi_symbol function · rust · L91-L111 (21 LOC)src/data/fetcher.rs
pub async fn fetch_multi_symbol(
symbols: &[&str],
days: i64,
) -> Result<Vec<(String, Vec<Candle>)>, Box<dyn std::error::Error>> {
let mut results = Vec::new();
for symbol in symbols {
println!("Fetching {} ({} days)...", symbol, days);
match fetch_last_n_days(symbol, days).await {
Ok(candles) => {
println!(" {} candles fetched", candles.len());
results.push((symbol.to_string(), candles));
}
Err(e) => {
eprintln!(" Error fetching {}: {}", symbol, e);
}
}
}
Ok(results)
}page 1 / 5next ›