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 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
+ }