Дистанция мамилеси боюнча жупталган соода: Rust менен ишке ашыруу жана талдоо
Жупталган соодадагы Дистанция мамилеси анын элегантык жөнөкөйлүгү жана натыйжалуулугу аркасында чоң популярдуулукка ээ болду. Бул техника статистикалык өлчөмдөр аркылуу актив жуптарын аныктайт жана алардын баа мамилелеринин четтөөсү менен жакындашуусуна негизделип соода жүргүзөт. Бул макалада негизги жана өркүндөтүлгөн Дистанция мамилеси методологияларынын кеңири талдоосу, жогорку жыштыктагы трейдерлерге, алгоритмдик иштеп чыгуучуларга, математиктерге жана ишенимдүү чечимдерди издеген программисттерге багытталган Rust'та практикалык ишке ашыруулар менен берилет.
Дистанция мамилесин визуалдаштыруу: A жана B активдери бири-бирин кайталайт, спреддин четтөөсүнө негизделген соода сигналдары түзүлөт (Long/Short)
Дистанция мамилесинин теориялык негизи
Дистанция мамилеси активдер ортосундагы нормалдаштырылган баа кыймылдарына негизделген жупталган соода үчүн алкак түзөт. Методдун өзөгүндө, тарыхый жактан бирге кыймылдаган активдерди аныктоо үчүн Евклид квадраттык дистанция өлчөмдөрү колдонулат жана алардын нормалдаштырылган баа четтөөсү статистикалык жактан маанилүү босогону ашканда соода сигналдары түзүлөт[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].
Негизги Дистанция мамилесин ишке ашыруу
Маалыматты нормалдаштыруу
Дистанцияларды эсептөөдөн мурун, салыштырма шкалаларды түзүү үчүн баа маалыматтарын нормалдаштыруу керек. Адатта 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()
}
Өркүндөтүлгөн мамиле: Пирсон корреляция методу
Пирсон корреляция мамилеси баа дистанцияларынын ордуна кирешелердин корреляциясына басым жасап, негизги Дистанция мамилесине караганда бир нече артыкчылыкты сунуштайт[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
}
Интеграциялык тестирлөө үчүн, биз тесттерди долбоордун тамыр каталогунда өзүнчө tests каталогуна жайгаштыруунун Rust конвенциясын карманабыз[15][18].
Корутунду
Дистанция мамилеси жупталган соода үчүн ишенимдүү алкак сунуштайт, негизги жана өркүндөтүлгөн методологиялар тең баалуу статистикалык арбитраж мүмкүнчүлүктөрүн берет. Евклид дистанциясына басым жасаган негизги мамиле жөнөкөйлүк жана натыйжалуулукту сунуштайт, ал эми Пирсон корреляция мамилеси кошумча ийкемдүүлүктү жана мүмкүн болгон жакшыраак четтөө-кайтуу мүнөздөмөлөрүн берет.
Rust'тун майнаптуулук мүнөздөмөлөрү аны, өзгөчө SIMD жана параллелдик иштетүү сыяктуу оптималдаштыруулар менен, эсептөө жактан оор бул стратегияларды ишке ашыруу үчүн идеалдуу тил кылат. Статистикалык тактык менен натыйжалуу ишке ашыруунун айкалышы алгоритмдик трейдерлер үчүн күчтүү инструменттер жыйнагын түзөт.
Жупталган соода системасын ишке ашырганда, бир нече нерселерди эске алуу керек:
- Жөнөкөйлүк (негизги мамиле) менен күчөтүлгөн статистикалык күч (Пирсон мамилеси) ортосундагы тандоо
- Чоң масштабдагы жуп талдоо үчүн керектүү эсептөө ресурстары
- Кирешелүүлүккө олуттуу таасир этиши мүмкүн болгон транзакция чыгымдары[3]
- Жуптарды үзгүлтүксүз көзөмөлдөө жана кайра калибрлөө зарылчылыгы
Дистанция мамилесин 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.