VOLI 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- __init__.py +0 -0
- analysis/__init__.py +0 -0
- analysis/confidence_scorer.py +199 -0
- analysis/pattern_matcher.py +262 -0
- analysis/range_calculator.py +350 -0
- data/__init__.py +0 -0
- data/calendar_client.py +268 -0
- data/twelve_data_client.py +355 -0
- server.py +153 -0
- tools/__init__.py +0 -0
- tools/session_analyzer.py +397 -0
- utils/__init__.py +0 -0
- utils/formatters.py +304 -0
- utils/sessions.py +247 -0
- voli-0.1.0.dist-info/METADATA +13 -0
- voli-0.1.0.dist-info/RECORD +19 -0
- voli-0.1.0.dist-info/WHEEL +5 -0
- voli-0.1.0.dist-info/entry_points.txt +2 -0
- voli-0.1.0.dist-info/top_level.txt +6 -0
__init__.py
ADDED
|
File without changes
|
analysis/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Confidence scoring for session analysis.
|
|
3
|
+
Pure algorithmic approach - no AI/ML needed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfidenceScorer:
|
|
10
|
+
"""Calculate confidence scores for volatility predictions."""
|
|
11
|
+
|
|
12
|
+
# When Scoring weights
|
|
13
|
+
WEIGHTS = {
|
|
14
|
+
"sample_size": 0.40, # Historical pattern sample size
|
|
15
|
+
"pattern_strength": 0.25, # How strong the expansion pattern is
|
|
16
|
+
"event_catalyst": 0.20, # Presence of high-impact event
|
|
17
|
+
"data_quality": 0.15 # Data recency and completeness
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def calculate_confidence(
|
|
22
|
+
occurrences: int,
|
|
23
|
+
expansion_rate: float,
|
|
24
|
+
has_event: bool,
|
|
25
|
+
data_age_days: int = 30,
|
|
26
|
+
max_data_age: int = 60
|
|
27
|
+
) -> float:
|
|
28
|
+
"""
|
|
29
|
+
Calculate overall confidence score.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
occurrences: Number of similar historical patterns found
|
|
33
|
+
expansion_rate: Rate at which patterns expanded (0-1)
|
|
34
|
+
has_event: Whether high-impact event is scheduled
|
|
35
|
+
data_age_days: How old the data is (lower = better)
|
|
36
|
+
max_data_age: Maximum acceptable data age
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Confidence score (0.0 to 0.85)
|
|
40
|
+
|
|
41
|
+
Scoring breakdown:
|
|
42
|
+
- Sample size: More patterns = higher confidence
|
|
43
|
+
- Pattern strength: Extreme expansion rates (near 0 or 1) = higher confidence
|
|
44
|
+
- Event catalyst: Events add certainty to volatility expectations
|
|
45
|
+
- Data quality: Fresh data = higher confidence
|
|
46
|
+
"""
|
|
47
|
+
# 1. Sample Size Score (0-0.40)
|
|
48
|
+
# More historical matches = more confidence
|
|
49
|
+
# Target: 120 matches for max score
|
|
50
|
+
sample_score = min(occurrences / 120, 1.0) * ConfidenceScorer.WEIGHTS["sample_size"]
|
|
51
|
+
|
|
52
|
+
# 2. Pattern Strength Score (0-0.25)
|
|
53
|
+
# Extreme rates (near 0 or 1) indicate clearer patterns
|
|
54
|
+
# Neutral rate (0.5) indicates uncertainty
|
|
55
|
+
pattern_certainty = abs(expansion_rate - 0.5) * 2 # 0-1 scale
|
|
56
|
+
pattern_score = pattern_certainty * ConfidenceScorer.WEIGHTS["pattern_strength"]
|
|
57
|
+
|
|
58
|
+
# 3. Event Catalyst Score (0-0.20)
|
|
59
|
+
# Events provide additional certainty about volatility
|
|
60
|
+
event_score = ConfidenceScorer.WEIGHTS["event_catalyst"] if has_event else 0.0
|
|
61
|
+
|
|
62
|
+
# 4. Data Quality Score (0-0.15)
|
|
63
|
+
# Fresher data = higher quality
|
|
64
|
+
# Penalize data older than 30 days
|
|
65
|
+
data_freshness = max(0, 1 - (data_age_days / max_data_age))
|
|
66
|
+
data_score = data_freshness * ConfidenceScorer.WEIGHTS["data_quality"]
|
|
67
|
+
|
|
68
|
+
# Total score
|
|
69
|
+
total = sample_score + pattern_score + event_score + data_score
|
|
70
|
+
|
|
71
|
+
# Cap at 0.85 (never 100% certain in markets)
|
|
72
|
+
return round(min(total, 0.85), 2)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def get_confidence_breakdown(
|
|
76
|
+
occurrences: int,
|
|
77
|
+
expansion_rate: float,
|
|
78
|
+
has_event: bool,
|
|
79
|
+
data_age_days: int = 30,
|
|
80
|
+
max_data_age: int = 60
|
|
81
|
+
) -> Dict[str, float]:
|
|
82
|
+
"""
|
|
83
|
+
Get detailed breakdown of confidence components.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
Same as calculate_confidence
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Dict with component scores
|
|
90
|
+
"""
|
|
91
|
+
sample_component = min(occurrences / 120, 1.0) * ConfidenceScorer.WEIGHTS["sample_size"]
|
|
92
|
+
|
|
93
|
+
pattern_certainty = abs(expansion_rate - 0.5) * 2
|
|
94
|
+
pattern_component = pattern_certainty * ConfidenceScorer.WEIGHTS["pattern_strength"]
|
|
95
|
+
|
|
96
|
+
event_component = ConfidenceScorer.WEIGHTS["event_catalyst"] if has_event else 0.0
|
|
97
|
+
|
|
98
|
+
data_freshness = max(0, 1 - (data_age_days / max_data_age))
|
|
99
|
+
data_component = data_freshness * ConfidenceScorer.WEIGHTS["data_quality"]
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"sample_size_score": round(sample_component, 3),
|
|
103
|
+
"pattern_strength_score": round(pattern_component, 3),
|
|
104
|
+
"event_catalyst_score": round(event_component, 3),
|
|
105
|
+
"data_quality_score": round(data_component, 3),
|
|
106
|
+
"total": round(
|
|
107
|
+
min(sample_component + pattern_component + event_component + data_component, 0.85),
|
|
108
|
+
2
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def get_confidence_explanation(
|
|
114
|
+
confidence: float,
|
|
115
|
+
occurrences: int,
|
|
116
|
+
expansion_rate: float,
|
|
117
|
+
has_event: bool
|
|
118
|
+
) -> str:
|
|
119
|
+
"""
|
|
120
|
+
Generate human-readable explanation of confidence score.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
confidence: Calculated confidence score
|
|
124
|
+
occurrences: Pattern matches
|
|
125
|
+
expansion_rate: Historical expansion rate
|
|
126
|
+
has_event: Event presence
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Explanation string
|
|
130
|
+
"""
|
|
131
|
+
explanations = []
|
|
132
|
+
|
|
133
|
+
# Sample size
|
|
134
|
+
if occurrences >= 100:
|
|
135
|
+
explanations.append("strong historical sample size")
|
|
136
|
+
elif occurrences >= 50:
|
|
137
|
+
explanations.append("moderate historical sample")
|
|
138
|
+
else:
|
|
139
|
+
explanations.append("limited historical data")
|
|
140
|
+
|
|
141
|
+
# Pattern clarity
|
|
142
|
+
if expansion_rate > 0.7:
|
|
143
|
+
explanations.append("clear expansion pattern")
|
|
144
|
+
elif expansion_rate < 0.3:
|
|
145
|
+
explanations.append("clear range-bound pattern")
|
|
146
|
+
else:
|
|
147
|
+
explanations.append("mixed historical outcomes")
|
|
148
|
+
|
|
149
|
+
# Event
|
|
150
|
+
if has_event:
|
|
151
|
+
explanations.append("high-impact event scheduled")
|
|
152
|
+
|
|
153
|
+
# Overall assessment
|
|
154
|
+
if confidence >= 0.70:
|
|
155
|
+
prefix = "High confidence:"
|
|
156
|
+
elif confidence >= 0.50:
|
|
157
|
+
prefix = "Moderate confidence:"
|
|
158
|
+
else:
|
|
159
|
+
prefix = "Low confidence:"
|
|
160
|
+
|
|
161
|
+
return f"{prefix} {', '.join(explanations)}."
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def adjust_for_volatility_regime(
|
|
165
|
+
base_confidence: float,
|
|
166
|
+
current_atr: float,
|
|
167
|
+
avg_atr: float,
|
|
168
|
+
adjustment_factor: float = 0.10
|
|
169
|
+
) -> float:
|
|
170
|
+
"""
|
|
171
|
+
Adjust confidence based on current volatility regime.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
base_confidence: Initial confidence score
|
|
175
|
+
current_atr: Current ATR
|
|
176
|
+
avg_atr: Historical average ATR
|
|
177
|
+
adjustment_factor: Max adjustment amount
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Adjusted confidence score
|
|
181
|
+
"""
|
|
182
|
+
if avg_atr == 0:
|
|
183
|
+
return base_confidence
|
|
184
|
+
|
|
185
|
+
# If current volatility is very different from average, reduce confidence
|
|
186
|
+
volatility_ratio = current_atr / avg_atr
|
|
187
|
+
|
|
188
|
+
# Penalize extreme deviations (both high and low)
|
|
189
|
+
if volatility_ratio > 1.5 or volatility_ratio < 0.5:
|
|
190
|
+
# Unusual volatility regime, reduce confidence
|
|
191
|
+
adjustment = -adjustment_factor
|
|
192
|
+
elif 0.8 <= volatility_ratio <= 1.2:
|
|
193
|
+
# Normal regime, slight boost
|
|
194
|
+
adjustment = adjustment_factor * 0.5
|
|
195
|
+
else:
|
|
196
|
+
adjustment = 0
|
|
197
|
+
|
|
198
|
+
adjusted = base_confidence + adjustment
|
|
199
|
+
return round(max(0.0, min(adjusted, 0.85)), 2)
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Historical pattern matching for similar market conditions.
|
|
3
|
+
Pure in-memory analysis without database.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import numpy as np
|
|
8
|
+
from typing import Dict, List, Tuple, Optional, Any
|
|
9
|
+
from datetime import time, datetime, timedelta
|
|
10
|
+
|
|
11
|
+
from analysis.range_calculator import RangeCalculator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PatternMatcher:
|
|
15
|
+
"""Match current conditions against historical patterns."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, pair: str):
|
|
18
|
+
"""
|
|
19
|
+
Initialize pattern matcher.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
pair: Currency pair
|
|
23
|
+
"""
|
|
24
|
+
self.pair = pair
|
|
25
|
+
self.range_calc = RangeCalculator(pair)
|
|
26
|
+
|
|
27
|
+
def find_similar_conditions(
|
|
28
|
+
self,
|
|
29
|
+
current_pre_range: float,
|
|
30
|
+
avg_pre_range: float,
|
|
31
|
+
historical_df: pd.DataFrame,
|
|
32
|
+
session_start: time,
|
|
33
|
+
session_end: time,
|
|
34
|
+
threshold: float = 0.15
|
|
35
|
+
) -> Dict[str, Any]:
|
|
36
|
+
"""
|
|
37
|
+
Find historical days with similar pre-session compression.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
current_pre_range: Today's pre-session range (pips)
|
|
41
|
+
avg_pre_range: 30-day average pre-session range (pips)
|
|
42
|
+
historical_df: 30-60 days of intraday data
|
|
43
|
+
session_start: Session start time
|
|
44
|
+
session_end: Session end time
|
|
45
|
+
threshold: Similarity threshold (0.15 = ±15%)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Dict with:
|
|
49
|
+
- similar_conditions_occurrences: int
|
|
50
|
+
- expansion_rate: float (0-1)
|
|
51
|
+
- avg_expansion_pips: float
|
|
52
|
+
- matched_dates: list of date strings
|
|
53
|
+
"""
|
|
54
|
+
# Calculate current compression ratio
|
|
55
|
+
if avg_pre_range == 0:
|
|
56
|
+
return {
|
|
57
|
+
"similar_conditions_occurrences": 0,
|
|
58
|
+
"expansion_rate": 0.5,
|
|
59
|
+
"avg_expansion_pips": 0.0,
|
|
60
|
+
"matched_dates": []
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
current_ratio = current_pre_range / avg_pre_range
|
|
64
|
+
|
|
65
|
+
# Define target range for "similar" days
|
|
66
|
+
lower_bound = current_ratio - threshold
|
|
67
|
+
upper_bound = current_ratio + threshold
|
|
68
|
+
|
|
69
|
+
# Group historical data by date
|
|
70
|
+
historical_df = historical_df.copy()
|
|
71
|
+
historical_df['date'] = pd.to_datetime(historical_df.index).date
|
|
72
|
+
|
|
73
|
+
matches = []
|
|
74
|
+
|
|
75
|
+
for date, day_df in historical_df.groupby('date'):
|
|
76
|
+
# Calculate pre-session range for this day
|
|
77
|
+
pre_range = self.range_calc.calculate_pre_session_range(
|
|
78
|
+
day_df,
|
|
79
|
+
session_start,
|
|
80
|
+
minutes_before=90
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if pre_range == 0:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
# Calculate this day's compression ratio
|
|
87
|
+
day_ratio = pre_range / avg_pre_range
|
|
88
|
+
|
|
89
|
+
# Check if within similarity threshold
|
|
90
|
+
if lower_bound <= day_ratio <= upper_bound:
|
|
91
|
+
# Calculate session range for this day
|
|
92
|
+
session_range = self.range_calc.calculate_session_range(
|
|
93
|
+
day_df,
|
|
94
|
+
session_start,
|
|
95
|
+
session_end
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Determine if expansion occurred
|
|
99
|
+
# Expansion = session range significantly exceeded pre-session range
|
|
100
|
+
expansion_multiplier = 1.5 # Session must be 50%+ larger
|
|
101
|
+
expanded = session_range > (pre_range * expansion_multiplier)
|
|
102
|
+
|
|
103
|
+
matches.append({
|
|
104
|
+
"date": str(date),
|
|
105
|
+
"pre_range": pre_range,
|
|
106
|
+
"session_range": session_range,
|
|
107
|
+
"expanded": expanded,
|
|
108
|
+
"expansion_pips": session_range - pre_range
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
# Calculate statistics
|
|
112
|
+
if not matches:
|
|
113
|
+
return {
|
|
114
|
+
"similar_conditions_occurrences": 0,
|
|
115
|
+
"expansion_rate": 0.5,
|
|
116
|
+
"avg_expansion_pips": 0.0,
|
|
117
|
+
"matched_dates": []
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
expansion_count = sum(1 for m in matches if m["expanded"])
|
|
121
|
+
expansion_rate = expansion_count / len(matches)
|
|
122
|
+
avg_expansion = np.mean([m["expansion_pips"] for m in matches])
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
"similar_conditions_occurrences": len(matches),
|
|
126
|
+
"expansion_rate": round(expansion_rate, 2),
|
|
127
|
+
"avg_expansion_pips": round(avg_expansion, 1),
|
|
128
|
+
"matched_dates": [m["date"] for m in matches[-10:]] # Last 10 for reference
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
def find_event_day_patterns(
|
|
132
|
+
self,
|
|
133
|
+
historical_df: pd.DataFrame,
|
|
134
|
+
event_dates: List[str],
|
|
135
|
+
session_start: time,
|
|
136
|
+
session_end: time
|
|
137
|
+
) -> Dict[str, Any]:
|
|
138
|
+
"""
|
|
139
|
+
Analyze volatility patterns on days with high-impact events.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
historical_df: Historical intraday data
|
|
143
|
+
event_dates: List of dates with events (YYYY-MM-DD format)
|
|
144
|
+
session_start: Session start time
|
|
145
|
+
session_end: Session end time
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dict with event-day statistics
|
|
149
|
+
"""
|
|
150
|
+
historical_df = historical_df.copy()
|
|
151
|
+
historical_df['date'] = historical_df.index.date
|
|
152
|
+
|
|
153
|
+
event_date_objs = [datetime.strptime(d, "%Y-%m-%d").date() for d in event_dates]
|
|
154
|
+
|
|
155
|
+
event_ranges = []
|
|
156
|
+
|
|
157
|
+
for date, day_df in historical_df.groupby('date'):
|
|
158
|
+
if date in event_date_objs:
|
|
159
|
+
session_range = self.range_calc.calculate_session_range(
|
|
160
|
+
day_df,
|
|
161
|
+
session_start,
|
|
162
|
+
session_end
|
|
163
|
+
)
|
|
164
|
+
event_ranges.append(session_range)
|
|
165
|
+
|
|
166
|
+
if not event_ranges:
|
|
167
|
+
return {
|
|
168
|
+
"event_day_count": 0,
|
|
169
|
+
"avg_event_day_range": 0.0,
|
|
170
|
+
"event_volatility_multiplier": 1.0
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Compare to non-event days
|
|
174
|
+
non_event_ranges = []
|
|
175
|
+
for date, day_df in historical_df.groupby('date'):
|
|
176
|
+
if date not in event_date_objs:
|
|
177
|
+
session_range = self.range_calc.calculate_session_range(
|
|
178
|
+
day_df,
|
|
179
|
+
session_start,
|
|
180
|
+
session_end
|
|
181
|
+
)
|
|
182
|
+
if session_range > 0:
|
|
183
|
+
non_event_ranges.append(session_range)
|
|
184
|
+
|
|
185
|
+
avg_event = np.mean(event_ranges)
|
|
186
|
+
avg_non_event = np.mean(non_event_ranges) if non_event_ranges else avg_event
|
|
187
|
+
|
|
188
|
+
multiplier = avg_event / avg_non_event if avg_non_event > 0 else 1.0
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
"event_day_count": len(event_ranges),
|
|
192
|
+
"avg_event_day_range": round(avg_event, 1),
|
|
193
|
+
"event_volatility_multiplier": round(multiplier, 2)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
def calculate_directional_bias(
|
|
197
|
+
self,
|
|
198
|
+
historical_df: pd.DataFrame,
|
|
199
|
+
session_start: time,
|
|
200
|
+
session_end: time
|
|
201
|
+
) -> Dict[str, Any]:
|
|
202
|
+
"""
|
|
203
|
+
Calculate directional bias for the session (bullish/bearish tendency).
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
historical_df: Historical data
|
|
207
|
+
session_start: Session start
|
|
208
|
+
session_end: Session end
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Dict with directional statistics
|
|
212
|
+
"""
|
|
213
|
+
historical_df = historical_df.copy()
|
|
214
|
+
historical_df['date'] = historical_df.index.date
|
|
215
|
+
|
|
216
|
+
bullish_days = 0
|
|
217
|
+
bearish_days = 0
|
|
218
|
+
|
|
219
|
+
for date, day_df in historical_df.groupby('date'):
|
|
220
|
+
session_df = self.range_calc._filter_session(
|
|
221
|
+
day_df,
|
|
222
|
+
session_start,
|
|
223
|
+
session_end
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if session_df.empty:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
open_price = session_df['open'].iloc[0]
|
|
230
|
+
close_price = session_df['close'].iloc[-1]
|
|
231
|
+
|
|
232
|
+
if close_price > open_price:
|
|
233
|
+
bullish_days += 1
|
|
234
|
+
else:
|
|
235
|
+
bearish_days += 1
|
|
236
|
+
|
|
237
|
+
total_days = bullish_days + bearish_days
|
|
238
|
+
|
|
239
|
+
if total_days == 0:
|
|
240
|
+
return {
|
|
241
|
+
"bias": "neutral",
|
|
242
|
+
"bullish_percentage": 50.0,
|
|
243
|
+
"bearish_percentage": 50.0
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
bullish_pct = (bullish_days / total_days) * 100
|
|
247
|
+
bearish_pct = (bearish_days / total_days) * 100
|
|
248
|
+
|
|
249
|
+
# Determine bias (needs >60% to be considered directional)
|
|
250
|
+
if bullish_pct > 60:
|
|
251
|
+
bias = "bullish"
|
|
252
|
+
elif bearish_pct > 60:
|
|
253
|
+
bias = "bearish"
|
|
254
|
+
else:
|
|
255
|
+
bias = "neutral"
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
"bias": bias,
|
|
259
|
+
"bullish_percentage": round(bullish_pct, 1),
|
|
260
|
+
"bearish_percentage": round(bearish_pct, 1),
|
|
261
|
+
"sample_size": total_days
|
|
262
|
+
}
|