Парлық сауда-саттықтағы Distance Approach: Rust тілінде іске асыру және талдау
Парлық сауда-саттықтағы Distance Approach (қашықтық тәсілі) өзінің талғампаз қарапайымдылығы мен тиімділігінің арқасында үлкен танымалдыққа ие болды. Бұл техника статистикалық өлшемдер арқылы активтер жұптарын анықтайды және олардың баға арақатынасының алшақтауы мен жақындасуы негізінде сауда жасайды. Бұл мақалада базалық және кеңейтілген Distance Approach әдіснамаларының жан-жақты талдауы берілген, жоғары жиілікті трейдерлерге, алгоритмдік әзірлеушілерге, математиктерге және сенімді шешім іздейтін бағдарламашыларға арналған Rust тіліндегі практикалық іске асырулармен қоса.
Distance Approach-ты көрнекілендіру: A және B активтері бір-бірін қадағалайды, спредтің алшақтауы негізінде сауда сигналдары генерацияланады (Long/Short)
Distance Approach-тың теориялық негізі
Distance Approach активтер арасындағы қалыпқа келтірілген баға қозғалыстары негізінде парлық сауда-саттықтың негізін қалайды. Әдістің өзегінде Евклид квадраттық қашықтығы өлшемдері қолданылады — олар тарихи түрде бірге қозғалатын активтерді анықтап, олардың қалыпқа келтірілген баға алшақтығы статистикалық маңызды шектерден асқанда сауда сигналдарын генерациялайды[2].
Бұл тәсіл екі негізгі кезеңнен тұрады:
- Жұптарды қалыптастыру — статистикалық байланысты активтер жұптарын анықтау
- Сауда сигналдарын генерациялау — алшақтық негізінде кіру және шығу ережелерін құру
Математикалық негіз
Базалық іске асыру қалыпқа келтірілген баға қатарлары арасындағы Евклид қашықтығын пайдаланады. X және Y қалыпқа келтірілген баға уақыт қатарлары бар екі актив үшін мынаны есептейміз:
fn euclidean_squared_distance(x: &[f64], y: &[f64]) -> f64 {
assert_eq!(x.len(), y.len(), "Time series must have equal length");
x.iter()
.zip(y.iter())
.map(|(xi, yi)| (xi - yi).powi(2))
.sum()
}
Бұл қашықтық метрикасы тарихи түрде бірге қозғалатын активтерді анықтауға көмектеседі, статистикалық арбитраж мүмкіндіктерінің негізін қалайды[2].
Базалық Distance Approach іске асыруы
Деректерді нормалдау
Қашықтықты есептеуден бұрын салыстырмалы шкалаларды орнату үшін баға деректерін қалыпқа келтіру керек. Әдетте min-max нормалдау қолданылады:
fn min_max_normalize(prices: &[f64]) -> Vec<f64> {
if prices.is_empty() {
return Vec::new();
}
let min_price = prices.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_price = prices.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let range = max_price - min_price;
if range.abs() < f64::EPSILON {
return vec![0.5; prices.len()];
}
prices.iter()
.map(|&price| (price - min_price) / range)
.collect()
}
Ең жақын жұптарды табу
Барлық активтер комбинациялары арасындағы Евклид қашықтығын есептеп, қашықтығы ең кіші жұптарды таңдау арқылы ықтимал жұптарды анықтаймыз:
#[derive(Debug, Clone)]
struct StockPair {
stock1_idx: usize,
stock2_idx: usize,
distance: f64,
}
impl PartialEq for StockPair {
fn eq(&self, other: &Self) -> bool {
self.distance.eq(&other.distance)
}
}
impl Eq for StockPair {}
impl PartialOrd for StockPair {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.distance.partial_cmp(&other.distance)
}
}
impl Ord for StockPair {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
}
}
fn find_closest_pairs(normalized_prices: &[Vec<f64>], top_n: usize) -> Vec<StockPair> {
let stock_count = normalized_prices.len();
let mut pairs = BinaryHeap::new();
for i in 0..stock_count {
for j in (i+1)..stock_count {
let distance = euclidean_squared_distance(&normalized_prices[i], &normalized_prices[j]);
pairs.push(Reverse(StockPair {
stock1_idx: i,
stock2_idx: j,
distance,
}));
// Keep only top N pairs
if pairs.len() > top_n {
pairs.pop();
}
}
}
// Convert from heap to vector and reverse to get ascending order
pairs.into_iter().map(|Reverse(pair)| pair).collect()
}
Тарихи волатильдікті есептеу
Тарихи волатильдікті есептеу тиісті сауда шектерін орнату үшін өте маңызды:
fn calculate_spread_volatility(normalized_price1: &[f64], normalized_price2: &[f64]) -> f64 {
assert_eq!(normalized_price1.len(), normalized_price2.len());
// Calculate price spread
let spread: Vec<f64> = normalized_price1.iter()
.zip(normalized_price2.iter())
.map(|(p1, p2)| p1 - p2)
.collect();
// Calculate mean of spread
let mean = spread.iter().sum::<f64>() / spread.len() as f64;
// Calculate standard deviation
let variance = spread.iter()
.map(|&x| (x - mean).powi(2))
.sum::<f64>() / spread.len() as f64;
variance.sqrt()
}
Кеңейтілген таңдау әдістері
Салалық топ бойынша сүзу
Жұп таңдауды бір саламен шектеу экономикалық байланысты активтерді таңдау арқылы өнімділікті арттыруы мүмкін:
fn find_industry_pairs(
normalized_prices: &[Vec<f64>],
industry_codes: &[usize],
top_n_per_industry: usize
) -> Vec<StockPair> {
// Group stocks by industry
let mut industry_groups: std::collections::HashMap<usize, Vec<usize>> = std::collections::HashMap::new();
for (idx, &code) in industry_codes.iter().enumerate() {
industry_groups.entry(code).or_default().push(idx);
}
// Find closest pairs within each industry
let mut all_pairs = Vec::new();
for (_industry_code, stock_indices) in industry_groups {
let mut industry_pairs = Vec::new();
for i in 0..stock_indices.len() {
for j in (i+1)..stock_indices.len() {
let stock1_idx = stock_indices[i];
let stock2_idx = stock_indices[j];
let distance = euclidean_squared_distance(
&normalized_prices[stock1_idx],
&normalized_prices[stock2_idx]
);
industry_pairs.push(StockPair {
stock1_idx,
stock2_idx,
distance,
});
}
}
// Sort pairs by distance
industry_pairs.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap());
// Take top N from each industry
let top_pairs: Vec<StockPair> = industry_pairs.into_iter()
.take(top_n_per_industry)
.collect();
all_pairs.extend(top_pairs);
}
all_pairs
}
Нөлдік қиылысулар (zero-crossings) тәсілі жиі жақындасу мен алшақтауды көрсететін жұптарды анықтайды, бұл әлеуетті түрде табысты сауда мүмкіндіктерін білдіруі мүмкін:
Zero-Crossings тұжырымдамасы: спредтің нөлдік сызықты қиып өтуімен көрсетілетін, жиі орташа мәнге қайта оралатын жұптарды анықтау
fn count_zero_crossings(spread: &[f64]) -> usize {
if spread.len() < 2 {
return 0;
}
let mut count = 0;
for i in 1..spread.len() {
if (spread[i-1] < 0.0 && spread[i] >= 0.0) ||
(spread[i-1] >= 0.0 && spread[i] < 0.0) {
count += 1;
}
}
count
}
fn find_zero_crossing_pairs(
normalized_prices: &[Vec<f64>],
top_distance_threshold: f64,
min_crossings: usize
) -> Vec<StockPair> {
let stock_count = normalized_prices.len();
let mut qualifying_pairs = Vec::new();
for i in 0..stock_count {
for j in (i+1)..stock_count {
let distance = euclidean_squared_distance(&normalized_prices[i], &normalized_prices[j]);
// Only consider pairs with distance below threshold
if distance < top_distance_threshold {
// Calculate spread
let spread: Vec<f64> = normalized_prices[i].iter()
.zip(normalized_prices[j].iter())
.map(|(p1, p2)| p1 - p2)
.collect();
let crossings = count_zero_crossings(&spread);
if crossings >= min_crossings {
qualifying_pairs.push(StockPair {
stock1_idx: i,
stock2_idx: j,
distance,
});
}
}
}
}
// Sort by number of crossings (could extend StockPair to include this)
qualifying_pairs.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap());
qualifying_pairs
}
Тарихи стандартты ауытқуды ескеру
Бұл әдіс спред волатильдігі жоғарырақ жұптарға басымдық беру арқылы базалық тәсілдің шектеулігін жояды, бұл пайда әлеуетін арттыруы мүмкін:
fn find_highsd_pairs(
normalized_prices: &[Vec<f64>],
top_distance_count: usize,
min_volatility: f64
) -> Vec<StockPair> {
let stock_count = normalized_prices.len();
let mut all_pairs = Vec::new();
for i in 0..stock_count {
for j in (i+1)..stock_count {
let distance = euclidean_squared_distance(&normalized_prices[i], &normalized_prices[j]);
// Calculate spread volatility
let spread: Vec<f64> = normalized_prices[i].iter()
.zip(normalized_prices[j].iter())
.map(|(p1, p2)| p1 - p2)
.collect();
let volatility = calculate_spread_volatility(&normalized_prices[i], &normalized_prices[j]);
if volatility >= min_volatility {
all_pairs.push(StockPair {
stock1_idx: i,
stock2_idx: j,
distance,
});
}
}
}
// Sort by distance
all_pairs.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap());
// Take top N pairs with highest volatility that meet distance criteria
all_pairs.into_iter().take(top_distance_count).collect()
}
Кеңейтілген тәсіл: Пирсон корреляциясы әдісі
Пирсон корреляциясы тәсілі базалық Distance Approach-қа қарағанда бірнеше артықшылыққа ие, өйткені ол баға қашықтығына емес, кірістілік корреляцияларына назар аударады[1].
Rust тіліндегі іске асыру
fn pearson_correlation(x: &[f64], y: &[f64]) -> f64 {
assert_eq!(x.len(), y.len(), "Arrays must have the same length");
let n = x.len() as f64;
let sum_x: f64 = x.iter().sum();
let sum_y: f64 = y.iter().sum();
let sum_xx: f64 = x.iter().map(|&val| val * val).sum();
let sum_yy: f64 = y.iter().map(|&val| val * val).sum();
let sum_xy: f64 = x.iter().zip(y.iter()).map(|(&xi, &yi)| xi * yi).sum();
let numerator = n * sum_xy - sum_x * sum_y;
let denominator = ((n * sum_xx - sum_x * sum_x) * (n * sum_yy - sum_y * sum_y)).sqrt();
if denominator.abs() < f64::EPSILON {
return 0.0;
}
numerator / denominator
}
struct PearsonPair {
stock_idx: usize,
comover_indices: Vec<usize>,
correlations: Vec<f64>,
}
fn find_pearson_pairs(returns: &[Vec<f64>], top_n_comovers: usize) -> Vec<PearsonPair> {
let stock_count = returns.len();
let mut all_pairs = Vec::new();
for i in 0..stock_count {
let mut correlations = Vec::with_capacity(stock_count - 1);
for j in 0..stock_count {
if i == j {
continue;
}
let correlation = pearson_correlation(&returns[i], &returns[j]).abs();
correlations.push((j, correlation));
}
// Sort by correlation (highest first)
correlations.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
// Take top N comovers
let top_comovers: Vec<(usize, f64)> = correlations.into_iter()
.take(top_n_comovers)
.collect();
let (comover_indices, correlation_values): (Vec<usize>, Vec<f64>) =
top_comovers.into_iter().unzip();
all_pairs.push(PearsonPair {
stock_idx: i,
comover_indices,
correlations: correlation_values,
});
}
all_pairs
}
Портфель құрастыру және бета есептеу
Пирсон тәсілі әрбір акция үшін бірге қозғалатындар портфелін құрады, содан кейін регрессия коэффициенттерін есептейді:
fn calculate_beta(stock_returns: &[f64], portfolio_returns: &[f64]) -> f64 {
let cov_xy = covariance(stock_returns, portfolio_returns);
let var_x = variance(portfolio_returns);
if var_x.abs() < f64::EPSILON {
return 0.0;
}
cov_xy / var_x
}
fn covariance(x: &[f64], y: &[f64]) -> f64 {
assert_eq!(x.len(), y.len());
let n = x.len() as f64;
let mean_x: f64 = x.iter().sum::<f64>() / n;
let mean_y: f64 = y.iter().sum::<f64>() / n;
let sum_cov: f64 = x.iter()
.zip(y.iter())
.map(|(&xi, &yi)| (xi - mean_x) * (yi - mean_y))
.sum();
sum_cov / n
}
fn variance(x: &[f64]) -> f64 {
let n = x.len() as f64;
let mean: f64 = x.iter().sum::<f64>() / n;
let sum_var: f64 = x.iter()
.map(|&xi| (xi - mean).powi(2))
.sum();
sum_var / n
}
Сауда сигналдарын генерациялау
Екі тәсілдегі де соңғы қадам — алшақтық шегі негізінде сауда сигналдарын генерациялау:
enum TradingSignal {
Long,
Short,
Neutral
}
struct TradePosition {
stock1_idx: usize,
stock2_idx: usize,
signal: TradingSignal,
entry_spread: f64,
timestamp: usize,
}
fn generate_trading_signals(
normalized_prices: &[Vec<f64>],
pairs: &[StockPair],
threshold_multiplier: f64,
volatilities: &[f64],
current_time: usize
) -> Vec<TradePosition> {
let mut positions = Vec::new();
for (pair_idx, pair) in pairs.iter().enumerate() {
let stock1_idx = pair.stock1_idx;
let stock2_idx = pair.stock2_idx;
// Calculate current spread
let current_spread = normalized_prices[stock1_idx][current_time] -
normalized_prices[stock2_idx][current_time];
let threshold = threshold_multiplier * volatilities[pair_idx];
let signal = if current_spread > threshold {
// Stock1 is overvalued relative to Stock2
TradingSignal::Short
} else if current_spread < -threshold {
// Stock1 is undervalued relative to Stock2
TradingSignal::Long
} else {
TradingSignal::Neutral
};
if signal != TradingSignal::Neutral {
positions.push(TradePosition {
stock1_idx,
stock2_idx,
signal,
entry_spread: current_spread,
timestamp: current_time,
});
}
}
positions
}
Өнімділікті оңтайландыру
Жоғары жиілікті сауда жүйелері үшін өнімділік өте маңызды. SIMD (Single Instruction, Multiple Data) командалары қашықтықты есептеуді айтарлықтай жылдамдата алады:
SIMD үдетуі: бірнеше баға нүктесін бір мезгілде өңдеу үшін Rust тіліндегі деректер деңгейіндегі параллелизмді пайдалану, кідірісті айтарлықтай азайту
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
#[cfg(target_arch = "x86_64")]
#[inline]
unsafe fn euclidean_distance_simd(x: &[f32], y: &[f32]) -> f32 {
assert_eq!(x.len(), y.len());
let mut sum = _mm256_setzero_ps();
let chunks = x.len() / 8;
for i in 0..chunks {
let xi = _mm256_loadu_ps(&x[i * 8]);
let yi = _mm256_loadu_ps(&y[i * 8]);
let diff = _mm256_sub_ps(xi, yi);
let squared = _mm256_mul_ps(diff, diff);
sum = _mm256_add_ps(sum, squared);
}
// Handle the remaining elements
let mut result = _mm256_reduce_add_ps(sum);
for i in (chunks * 8)..x.len() {
result += (x[i] - y[i]).powi(2);
}
result.sqrt()
}
// Helper function to sum SIMD vector
#[cfg(target_arch = "x86_64")]
#[inline(always)]
unsafe fn _mm256_reduce_add_ps(v: __m256) -> f32 {
let hilow = _mm256_extractf128_ps(v, 1);
let low = _mm256_castps256_ps128(v);
let sum128 = _mm_add_ps(hilow, low);
let hi64 = _mm_extractf128_si128(_mm_castps_si128(sum128), 1);
let low64 = _mm_castps_si128(sum128);
let sum64 = _mm_add_ps(_mm_castsi128_ps(hi64), _mm_castsi128_ps(low64));
_mm_cvtss_f32(_mm_hadd_ps(sum64, sum64))
}
Асинхронды өңдеу, әсіресе бірнеше акция жұбымен жұмыс істегенде, өткізу қабілетін одан әрі жақсарта алады:
use tokio::task;
use futures::future::join_all;
async fn process_pairs_async(
normalized_prices: &[Vec<f64>],
stock_count: usize,
chunk_size: usize
) -> Vec<StockPair> {
let mut tasks = Vec::new();
// Split work into chunks
let chunks = (stock_count + chunk_size - 1) / chunk_size;
for chunk in 0..chunks {
let start = chunk * chunk_size;
let end = std::cmp::min((chunk + 1) * chunk_size, stock_count);
let prices_clone = normalized_prices.to_vec();
let task = task::spawn(async move {
let mut pairs = Vec::new();
for i in start..end {
for j in (i+1)..stock_count {
let distance = euclidean_squared_distance(&prices_clone[i], &prices_clone[j]);
pairs.push(StockPair {
stock1_idx: i,
stock2_idx: j,
distance,
});
}
}
pairs
});
tasks.push(task);
}
// Await all tasks and combine results
let results = join_all(tasks).await;
let mut all_pairs = Vec::new();
for result in results {
if let Ok(pairs) = result {
all_pairs.extend(pairs);
}
}
// Sort by distance
all_pairs.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap_or(std::cmp::Ordering::Equal));
all_pairs
}
Стратегия іске асыруын тестілеу
Іске асыруымызды бағалау үшін дұрыс тестілеу инфрақұрылымы қажет:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalization() {
let prices = vec![10.0, 15.0, 12.0, 18.0, 20.0];
let normalized = min_max_normalize(&prices);
let expected = vec![0.0, 0.5, 0.2, 0.8, 1.0];
for (a, b) in normalized.iter().zip(expected.iter()) {
assert!((a - b).abs() < 0.001);
}
}
#[test]
fn test_euclidean_distance() {
let x = vec![0.1, 0.2, 0.3, 0.4, 0.5];
let y = vec![0.15, 0.22, 0.35, 0.38, 0.53];
let distance = euclidean_squared_distance(&x, &y);
let expected = 0.0049; // Calculated manually
assert!((distance - expected).abs() < 0.0001);
}
#[test]
fn test_pearson_correlation() {
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y = vec![5.0, 4.0, 3.0, 2.0, 1.0];
let corr = pearson_correlation(&x, &y);
let expected = -1.0; // Perfect negative correlation
assert!((corr - expected).abs() < 0.0001);
}
// Integration tests would be implemented in tests/ directory
}
Интеграциялық тестілеу үшін біз Rust дәстүрін ұстанып, тесттерді жоба түбіріндегі бөлек tests каталогына орналастырар едік[15][18].
Қорытынды
Distance Approach парлық сауда-саттыққа сенімді негіз береді, базалық та, кеңейтілген де әдіснамалар статистикалық арбитраждың құнды мүмкіндіктерін ұсынады. Евклид қашықтығына негізделген базалық тәсіл қарапайымдылық пен тиімділікті ұсынса, ал Пирсон корреляциясы тәсілі қосымша икемділік пен алшақтықтың орташа мәнге қайта оралу сипаттамаларының жақсырақ болу мүмкіндігін ұсынады.
Rust-тың өнімділік сипаттамалары оны есептеу тұрғысынан қарқынды осы стратегияларды, әсіресе SIMD және параллель өңдеу сияқты оңтайландырулармен іске асыруға өте қолайлы тіл етеді. Статистикалық қаталдық пен тиімді іске асырудың үйлесімі алгоритмдік трейдерлерге арналған қуатты құралдар жиынтығын жасайды.
Парлық сауда жүйесін іске асырған кезде бірнеше жайтты ескеру керек:
- Қарапайымдылық (базалық тәсіл) пен күшейтілген статистикалық қуат (Пирсон тәсілі) арасындағы теңгерім
- Ірі ауқымды жұп талдауы үшін қажетті есептеу ресурстары
- Пайдалылыққа айтарлықтай әсер етуі мүмкін транзакция шығындары[3]
- Жұптарды үздіксіз бақылау және қайта калибрлеу қажеттілігі
Distance Approach-ты Rust-тың өнімділік мүмкіндіктерімен үйлестіру арқылы трейдерлер қазіргі нарықтар талап ететін жылдамдық пен ауқымда жұмыс істеуге қабілетті, жоғары тиімді әрі әсерлі статистикалық арбитраж жүйелерін әзірлей алады.
Citation
@software{soloviov2025distanceapproach,
author = {Soloviov, Eugen},
title = {Distance Approach in Pairs Trading: Implementation and Analysis with Rust},
year = {2025},
url = {https://marketmaker.cc/en/blog/post/distance-approach-pairs-trading},
version = {0.1.0},
description = {A comprehensive analysis of basic and advanced Distance Approach methodologies for pairs trading, with practical implementations in Rust tailored for high-frequency traders and algorithmic developers.}
}
References
- Hudson Thames - Introduction to Distance Approach in Pairs Trading Part II
- Hudson Thames - Distance Approach in Pairs Trading Part I
- Reddit - Pairs Trading is Too Good to Be True?
- GitHub - Kucoin Arbitrage
- docs.rs - Euclidean Distance in geo crate
- Simple Linear Regression in Rust
- GitHub - correlation_rust
- docs.rs - Cointegration in algolotl-ta
- GitHub - trading_engine_rust
- docs.rs - distances crate
- Reddit - Looking for stats crate for Dickey-Fuller
- crates.io - crypto-pair-trader
- w3resource - Rust Structs and Enums Exercise
- Rust Book - Test Organization
- Design Patterns in Rust
- GitHub - simd-euclidean
- Rust by Example - Integration Testing
- YouTube - Integration Testing in Rust
- Stack Overflow - Calculate Total Distance Between Multiple Points
- Databento - Pairs Trading Example
- Rust std - f64 Primitive
- Hudson & Thames - Distance Approach Documentation
- GitHub - trading-algorithms-rust
- docs.rs - linreg crate
- Rust Book - References and Borrowing
- Stack Overflow - How to Interpret adfuller Test Results
- lib.rs - arima crate
- Econometrics with R - Cointegration
- DolphinDB - adfuller Function
- docs.rs - arima crate (latest)
- Wikipedia - Cointegration
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.