Pendekatan Jarak dalam Dagangan Pasangan: Pelaksanaan dan Analisis dengan Rust
Pendekatan Jarak dalam dagangan pasangan telah mendapat populariti yang ketara berkat kesederhanaan dan keberkesanannya yang elegan. Teknik ini mengenal pasti pasangan aset melalui ukuran statistik dan berdagang berdasarkan penyelewengan serta penumpuan hubungan harga mereka. Artikel ini memberikan analisis menyeluruh tentang metodologi Pendekatan Jarak asas dan lanjutan, dengan pelaksanaan praktikal dalam Rust yang direka khusus untuk pedagang frekuensi tinggi, pembangun algoritma, ahli matematik, dan pengaturcara yang mencari penyelesaian kukuh.
Memvisualisasikan Pendekatan Jarak: Aset A dan B saling mengekori, dengan isyarat dagangan dijana berdasarkan penyelewengan spread (Long/Short)
Asas Teori Pendekatan Jarak
Pendekatan Jarak mewujudkan rangka kerja untuk dagangan pasangan berdasarkan pergerakan harga ternormal antara aset. Pada terasnya, kaedah ini menggunakan pengukuran jarak Euclidean kuasa dua untuk mengenal pasti aset yang bergerak seiring secara sejarah dan menjana isyarat dagangan apabila penyelewengan harga ternormal mereka melebihi ambang yang signifikan secara statistik[2].
Pendekatan ini terdiri daripada dua peringkat utama:
- Pembentukan pasangan - mengenal pasti pasangan aset yang berkaitan secara statistik
- Jana isyarat dagangan - mencipta peraturan masuk dan keluar berdasarkan penyelewengan
Asas Matematik
Pelaksanaan asas menggunakan jarak Euclidean antara siri harga ternormal. Untuk dua aset dengan siri masa harga ternormal X dan Y, kita mengira:
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()
}
Metrik jarak ini membantu mengenal pasti aset yang bergerak seiring secara sejarah, memberikan asas untuk peluang arbitraj statistik[2].
Pelaksanaan Asas Pendekatan Jarak
Penormalan Data
Sebelum mengira jarak, kita perlu menormalkan data harga untuk mewujudkan skala yang boleh dibandingkan. Penormalan min-maks lazim digunakan:
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()
}
Mencari Pasangan Terdekat
Kita mengenal pasti pasangan berpotensi dengan mengira jarak Euclidean antara semua gabungan aset dan memilih yang mempunyai jarak terkecil:
#[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()
}
Mengira Volatiliti Sejarah
Pengiraan volatiliti sejarah adalah penting untuk menetapkan ambang dagangan yang sesuai:
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()
}
Kaedah Pemilihan Lanjutan
Penapisan Kumpulan Industri
Mengehadkan pemilihan pasangan kepada industri yang sama boleh meningkatkan prestasi dengan memilih aset yang berkaitan secara ekonomi:
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
}
Pendekatan lintasan-sifar mengenal pasti pasangan dengan penumpuan dan penyelewengan yang kerap, yang berpotensi menunjukkan peluang dagangan yang lebih menguntungkan:
Konsep Lintasan-Sifar: mengenal pasti pasangan yang kerap melakukan mean-revert, ditunjukkan oleh spread yang melintasi garisan sifar
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
}
Pertimbangan Sisihan Piawai Sejarah
Kaedah ini menangani had pendekatan asas dengan mengutamakan pasangan yang mempunyai volatiliti spread yang lebih tinggi, yang boleh meningkatkan potensi keuntungan:
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()
}
Pendekatan Lanjutan: Kaedah Korelasi Pearson
Pendekatan Korelasi Pearson menawarkan beberapa kelebihan berbanding Pendekatan Jarak asas, dengan menumpukan pada korelasi pulangan berbanding jarak harga[1].
Pelaksanaan dalam 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
}
Pembentukan Portfolio dan Pengiraan Beta
Pendekatan Pearson mencipta portfolio comover untuk setiap saham, kemudian mengira pekali regresi:
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
}
Jana Isyarat Dagangan
Langkah terakhir dalam kedua-dua pendekatan ialah menjana isyarat dagangan berdasarkan ambang penyelewengan:
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
}
Pengoptimuman Prestasi
Bagi sistem dagangan frekuensi tinggi, prestasi adalah kritikal. Arahan SIMD (Single Instruction, Multiple Data) boleh mempercepatkan pengiraan jarak dengan ketara:
Pecutan SIMD: menggunakan selari peringkat data dalam Rust untuk memproses berbilang titik harga secara serentak, mengurangkan kependaman secara drastik
#[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))
}
Pemprosesan tak segerak boleh meningkatkan daya pemprosesan dengan lebih lanjut, terutamanya apabila berurusan dengan berbilang pasangan saham:
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
}
Menguji Pelaksanaan Strategi
Untuk menilai pelaksanaan kita, infrastruktur pengujian yang betul diperlukan:
#[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
}
Untuk pengujian integrasi, kita akan mengikuti konvensyen Rust dengan meletakkan ujian dalam direktori tests berasingan di akar projek[15][18].
Kesimpulan
Pendekatan Jarak menyediakan rangka kerja yang kukuh untuk dagangan pasangan, dengan metodologi asas dan lanjutan yang menawarkan peluang arbitraj statistik yang berharga. Pendekatan asas, dengan fokus pada jarak Euclidean, menawarkan kesederhanaan dan keberkesanan, manakala pendekatan Korelasi Pearson memberikan fleksibiliti tambahan dan ciri-ciri pembalikan penyelewengan yang berpotensi lebih baik.
Ciri-ciri prestasi Rust menjadikannya bahasa yang ideal untuk melaksanakan strategi yang intensif dari segi pengiraan ini, terutamanya dengan pengoptimuman seperti SIMD dan pemprosesan serentak. Gabungan ketegasan statistik dan pelaksanaan yang cekap mewujudkan kit alat yang berkuasa untuk pedagang algoritma.
Semasa melaksanakan sistem dagangan pasangan, beberapa pertimbangan perlu diambil kira:
- Pertukaran antara kesederhanaan (pendekatan asas) dan kuasa statistik yang dipertingkat (pendekatan Pearson)
- Sumber pengiraan yang diperlukan untuk analisis pasangan berskala besar
- Kos transaksi, yang boleh memberi kesan ketara terhadap keuntungan[3]
- Keperluan untuk pemantauan dan penentukuran semula pasangan secara berterusan
Dengan menggabungkan Pendekatan Jarak dengan keupayaan prestasi Rust, pedagang boleh membangunkan sistem arbitraj statistik yang sangat cekap dan berkesan, yang mampu beroperasi pada kelajuan dan skala yang diperlukan untuk pasaran moden.
Petikan
@software{soloviov2025distanceapproach,
author = {Soloviov, Eugen},
title = {Distance Approach in Pairs Trading: Implementation and Analysis with Rust},
year = {2025},
url = {https://marketmaker.cc/ms/blog/post/distance-approach-pairs-trading},
version = {0.1.0},
description = {Analisis menyeluruh tentang metodologi Pendekatan Jarak asas dan lanjutan untuk dagangan pasangan, dengan pelaksanaan praktikal dalam Rust yang direka khusus untuk pedagang frekuensi tinggi dan pembangun algoritma.}
}
Rujukan
- Hudson Thames - Pengenalan kepada Pendekatan Jarak dalam Dagangan Pasangan Bahagian II
- Hudson Thames - Pendekatan Jarak dalam Dagangan Pasangan Bahagian I
- Reddit - Adakah Dagangan Pasangan Terlalu Bagus untuk Menjadi Kenyataan?
- GitHub - Kucoin Arbitrage
- docs.rs - Jarak Euclidean dalam crate geo
- Regresi Linear Mudah dalam Rust
- GitHub - correlation_rust
- docs.rs - Kointegrasi dalam algolotl-ta
- GitHub - trading_engine_rust
- docs.rs - crate distances
- Reddit - Mencari crate statistik untuk Dickey-Fuller
- crates.io - crypto-pair-trader
- w3resource - Latihan Struct dan Enum Rust
- Buku Rust - Organisasi Ujian
- Corak Reka Bentuk dalam Rust
- GitHub - simd-euclidean
- Rust by Example - Pengujian Integrasi
- YouTube - Pengujian Integrasi dalam Rust
- Stack Overflow - Mengira Jumlah Jarak Antara Berbilang Titik
- Databento - Contoh Dagangan Pasangan
- Rust std - Primitif f64
- Hudson & Thames - Dokumentasi Pendekatan Jarak
- GitHub - trading-algorithms-rust
- docs.rs - crate linreg
- Buku Rust - Rujukan dan Peminjaman
- Stack Overflow - Cara Mentafsir Keputusan Ujian adfuller
- lib.rs - crate arima
- Ekonometrik dengan R - Kointegrasi
- DolphinDB - Fungsi adfuller
- docs.rs - crate arima (terkini)
- Wikipedia - Kointegrasi
Pengarang
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.