Juftlik savdosida masofa yondashuvi: Rust yordamida amalga oshirish va tahlil
Juftlik savdosidagi Masofa yondashuvi o'zining nafis soddaligi va samaradorligi tufayli katta ommaviylikka erishdi. Ushbu texnika statistik o'lchovlar orqali aktiv juftliklarini aniqlaydi va ularning narx munosabatlarining ajralishi hamda yaqinlashishiga asoslanib savdo qiladi. Ushbu maqola asosiy va ilgor Masofa yondashuvi metodologiyalarining keng qamrovli tahlilini, mustahkam yechimlar izlayotgan yuqori chastotali treyderlar, algoritmik dasturchilar, matematiklar va dasturchilar uchun moslashtirilgan Rust'dagi amaliy implementatsiyalar bilan taqdim etadi.
Masofa yondashuvini vizualizatsiya qilish: A va B aktivlari bir-birini kuzatib boradi, savdo signallari spred ajralishiga asoslanib generatsiya qilinadi (Long/Short)
Masofa yondashuvining nazariy asosi
Masofa yondashuvi aktivlar orasidagi normallashtirilgan narx harakatlariga asoslangan juftlik savdosi uchun asos yaratadi. O'zining mohiyatida usul tarixiy jihatdan birgalikda harakatlanadigan aktivlarni aniqlash uchun Evklid kvadratik masofa o'lchovlaridan foydalanadi va ularning normallashtirilgan narx ajralishi statistik jihatdan muhim chegaralardan oshib ketganda savdo signallarini generatsiya qiladi[2].
Ushbu yondashuv ikkita asosiy bosqichdan iborat:
- Juftliklarni shakllantirish - statistik jihatdan bog'liq aktiv juftliklarini aniqlash
- Savdo signalini generatsiya qilish - ajralishga asoslangan kirish va chiqish qoidalarini yaratish
Matematik asos
Asosiy implementatsiya normallashtirilgan narx qatorlari orasidagi Evklid masofasidan foydalanadi. Normallashtirilgan narx vaqt qatorlari X va Y bo'lgan ikkita aktiv uchun biz quyidagini hisoblaymiz:
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()
}
Ushbu masofa metrikasi tarixiy jihatdan birgalikda harakatlanadigan aktivlarni aniqlashga yordam beradi va statistik arbitraj imkoniyatlari uchun asos yaratadi[2].
Asosiy Masofa yondashuvi implementatsiyasi
Ma'lumotlarni normallashtirish
Masofalarni hisoblashdan oldin biz taqqoslanadigan miqyoslarni o'rnatish uchun narx ma'lumotlarini normallashtirishimiz kerak. Min-max normallashtirish ko'p qo'llaniladi:
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()
}
Eng yaqin juftliklarni topish
Biz barcha aktiv kombinatsiyalari orasidagi Evklid masofasini hisoblab va eng kichik masofalarga ega bo'lganlarini tanlab, potentsial juftliklarni aniqlaymiz:
#[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()
}
Tarixiy volatillikni hisoblash
Tarixiy volatillikni hisoblash mos savdo chegaralarini o'rnatish uchun juda muhim:
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()
}
Ilgor tanlash usullari
Soha guruhi bo'yicha filtrlash
Juftlik tanlashni bir xil sohaga cheklash iqtisodiy jihatdan bog'liq aktivlarni tanlash orqali samaradorlikni oshirishi mumkin:
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
}
Nol kesishmalar yondashuvi tez-tez yaqinlashish va ajralishga ega juftliklarni aniqlaydi, bu esa ehtimoliy jihatdan yanada foydali savdo imkoniyatlarini ko'rsatishi mumkin:
Nol kesishmalar kontseptsiyasi: spredning nol chizig'ini kesib o'tishi bilan ko'rsatilgan, tez-tez o'rtacha qiymatga qaytadigan juftliklarni aniqlash
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
}
Tarixiy standart chetlanishni hisobga olish
Ushbu usul yuqori spred volatilligiga ega juftliklarga ustuvorlik berish orqali asosiy yondashuvning bir cheklovini bartaraf etadi, bu esa foyda potentsialini oshirishi mumkin:
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()
}
Ilgor yondashuv: Pirson korrelyatsiya usuli
Pirson korrelyatsiya yondashuvi asosiy Masofa yondashuviga nisbatan bir necha afzalliklarni taqdim etadi, narx masofalari o'rniga daromad korrelyatsiyalariga e'tibor qaratadi[1].
Rust'da implementatsiya
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
}
Portfel shakllantirish va beta hisoblash
Pirson yondashuvi har bir aktsiya uchun birgalikda harakatlanuvchilardan (comover) portfellar yaratadi, so'ngra regressiya koeffitsiyentlarini hisoblaydi:
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
}
Savdo signalini generatsiya qilish
Ikkala yondashuvdagi yakuniy bosqich ajralish chegaralariga asoslanib savdo signallarini generatsiya qilishdir:
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
}
Ishlash samaradorligini optimallashtirish
Yuqori chastotali savdo tizimlari uchun ishlash samaradorligi juda muhim. SIMD (Single Instruction, Multiple Data) ko'rsatmalari masofa hisoblashlarini sezilarli darajada tezlashtirishi mumkin:
SIMD tezlashtirish: bir vaqtning o'zida bir nechta narx nuqtalarini qayta ishlash uchun Rust'da ma'lumot darajasidagi parallelizmdan foydalanish, kechikishni keskin kamaytiradi
#[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))
}
Asinxron qayta ishlash, ayniqsa bir nechta aktsiya juftliklari bilan ishlaganda, o'tkazuvchanlik qobiliyatini yanada oshirishi mumkin:
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
}
Strategiya implementatsiyasini sinovdan o'tkazish
Implementatsiyamizni baholash uchun bizga to'g'ri sinov infratuzilmasi kerak:
#[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
}
Integratsiya sinovlari uchun biz Rust an'anasiga amal qilib, sinovlarni loyiha ildizidagi alohida tests katalogiga joylashtiramiz[15][18].
Xulosa
Masofa yondashuvi juftlik savdosi uchun mustahkam asos yaratadi, asosiy va ilgor metodologiyalarning ikkalasi ham qimmatli statistik arbitraj imkoniyatlarini taklif etadi. Asosiy yondashuv Evklid masofasiga e'tibor qaratib, soddalik va samaradorlikni taqdim etadi, Pirson korrelyatsiya yondashuvi esa qo'shimcha moslashuvchanlik va ehtimoliy jihatdan yaxshiroq ajralishning qaytish xususiyatlarini ta'minlaydi.
Rust'ning ishlash xususiyatlari uni ushbu hisoblash jihatidan intensiv strategiyalarni amalga oshirish uchun ideal til qiladi, ayniqsa SIMD va parallel qayta ishlash kabi optimizatsiyalar bilan. Statistik qat'iylik va samarali implementatsiyaning uyg'unligi algoritmik treyderlar uchun kuchli vositalar to'plamini yaratadi.
Juftlik savdosi tizimini amalga oshirishda bir necha jihatlar hisobga olinishi kerak:
- Soddalik (asosiy yondashuv) va kuchaytirilgan statistik quvvat (Pirson yondashuvi) o'rtasidagi murosaga kelish
- Keng ko'lamli juftlik tahlili uchun zarur bo'lgan hisoblash resurslari
- Foydaliylikka sezilarli ta'sir ko'rsatishi mumkin bo'lgan tranzaksiya xarajatlari[3]
- Juftliklarni doimiy monitoring qilish va qayta kalibrlash zarurati
Masofa yondashuvini Rust'ning ishlash imkoniyatlari bilan birlashtirish orqali treyderlar zamonaviy bozorlar uchun talab qilinadigan tezlik va miqyosda ishlashga qodir bo'lgan yuqori darajada samarali va effektiv statistik arbitraj tizimlarini ishlab chiqishlari mumkin.
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.