1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#![allow(clippy::uninlined_format_args)]
//! Async histogram data example
//!
//! This example demonstrates how to retrieve histogram data (price distribution)
//! for a contract using the async API.
//!
//! # Usage
//!
//! Make sure IB Gateway or TWS is running with API connections enabled, then run:
//!
//! ```bash
//! cargo run --features async --example async_histogram_data
//! ```
//!
//! # Configuration
//!
//! - Adjust the connection address if needed (default: 127.0.0.1:4002)
//! - Change the contract and period as desired
use std::sync::Arc;
use ibapi::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
// Connect to IB Gateway (port 4002) or TWS (port 7497)
let client = Arc::new(Client::connect("127.0.0.1:4002", 100).await?);
println!("Connected to IB Gateway");
// Test different contracts and periods
let test_cases = vec![
(
"AAPL",
Contract::stock("AAPL").build(),
HistoricalBarSize::Week,
TradingHours::Regular,
"1 week RTH",
),
(
"SPY",
Contract::stock("SPY").build(),
HistoricalBarSize::Day,
TradingHours::Regular,
"1 day RTH",
),
(
"TSLA",
Contract::stock("TSLA").build(),
HistoricalBarSize::Week,
TradingHours::Extended,
"1 week all hours",
),
];
for (symbol, contract, period, trading_hours, description) in test_cases {
println!("\n{symbol} Histogram ({description}):");
println!("Period: {period:?}, Trading hours: {trading_hours:?}");
match client.histogram_data(&contract, trading_hours, period).await {
Ok(histogram) => {
if histogram.is_empty() {
println!("No histogram data available");
continue;
}
// Calculate statistics
let total_count: f64 = histogram.iter().filter_map(|e| e.size).sum();
let min_price = histogram.iter().map(|e| e.price).fold(f64::INFINITY, f64::min);
let max_price = histogram.iter().map(|e| e.price).fold(f64::NEG_INFINITY, f64::max);
// Calculate weighted average price
let weighted_sum: f64 = histogram.iter().filter_map(|e| e.size.map(|s| e.price * s)).sum();
let weighted_avg = weighted_sum / total_count;
println!("\nPrice Distribution:");
println!("Price | Count | Percentage | Bar");
println!("----------|----------|------------|{}", "-".repeat(50));
// Find max count for bar chart scaling. `max_by_key` needs `Ord`, which
// f64 doesn't implement, so reduce with `f64::max` — and fall back to 1.0
// only when there is nothing to scale against. Seeding the fold with 1.0
// would floor the scale and squash bars for sub-1.0 (fractional) sizes.
let max_count = histogram
.iter()
.filter_map(|e| e.size)
.reduce(f64::max)
.filter(|m| *m > 0.0)
.unwrap_or(1.0);
// Sort by price for better display
let mut sorted_histogram = histogram.clone();
sorted_histogram.sort_by(|a, b| a.price.partial_cmp(&b.price).unwrap());
// Display top and bottom 10 price levels
let display_count = 10;
let total_entries = sorted_histogram.len();
if total_entries <= display_count * 2 {
// Show all entries if small enough
for entry in &sorted_histogram {
print_histogram_entry(entry, total_count, max_count);
}
} else {
// Show top and bottom entries with separator
println!("Top {display_count} price levels:");
for entry in sorted_histogram.iter().rev().take(display_count).rev() {
print_histogram_entry(entry, total_count, max_count);
}
println!("... ({} entries omitted) ...", total_entries - display_count * 2);
println!("Bottom {display_count} price levels:");
for entry in sorted_histogram.iter().take(display_count) {
print_histogram_entry(entry, total_count, max_count);
}
}
// Display statistics
println!("\nStatistics:");
println!(" Total observations: {total_count:.0}");
println!(" Price range: ${min_price:.2} - ${max_price:.2}");
println!(" Price levels: {}", histogram.len());
println!(" Weighted average: ${weighted_avg:.2}");
// Find mode (most frequent price)
// Entries with no size reported are skipped rather than counted as zero.
let mode = histogram.iter().filter_map(|e| e.size.map(|s| (e, s))).max_by(|a, b| a.1.total_cmp(&b.1));
if let Some((mode_entry, mode_size)) = mode {
let mode_pct = (mode_size / total_count) * 100.0;
println!(" Mode: ${:.2} ({mode_size:.0} occurrences, {mode_pct:.1}%)", mode_entry.price);
}
}
Err(e) => {
println!("Error: {e}");
}
}
// Small delay to avoid rate limiting
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
println!("\nExample completed!");
Ok(())
}
fn print_histogram_entry(entry: &ibapi::market_data::historical::HistogramEntry, total_count: f64, max_count: f64) {
// A bucket with no reported size contributes nothing to the arithmetic, but
// print it as "n/a" rather than as a zero it didn't report.
let size = entry.size.unwrap_or(0.0);
let percentage = (size / total_count) * 100.0;
let bar_length = ((size / max_count) * 50.0) as usize;
let bar = "█".repeat(bar_length);
println!("${:8.2} | {:>8} | {percentage:9.2}% | {bar}", entry.price, fmt_size(entry.size));
}
/// `None` means TWS reported no size for the bucket — show that rather than
/// silently printing a zero.
fn fmt_size(size: Option<f64>) -> String {
size.map_or_else(|| "n/a".to_string(), |s| format!("{s:.0}"))
}