jasonlib-dev 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.
- jasonlib/__init__.py +53 -0
- jasonlib/_calculator.py +248 -0
- jasonlib/_math.py +52 -0
- jasonlib/_models.py +62 -0
- jasonlib/_numba_kernels.py +195 -0
- jasonlib/_pivot_analysis.py +308 -0
- jasonlib/_pivot_models.py +45 -0
- jasonlib/_trading_calendar.py +93 -0
- jasonlib/_trend_analysis.py +634 -0
- jasonlib/_trend_models.py +165 -0
- jasonlib_dev-0.1.0.dist-info/METADATA +10 -0
- jasonlib_dev-0.1.0.dist-info/RECORD +13 -0
- jasonlib_dev-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Pivot breakout detection and analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from jasonlib._models import JInterval
|
|
11
|
+
from jasonlib._pivot_models import (
|
|
12
|
+
PivotAnalysisParameters,
|
|
13
|
+
PivotAnalysisResult,
|
|
14
|
+
PivotAnalysisSummary,
|
|
15
|
+
PivotSortOption,
|
|
16
|
+
)
|
|
17
|
+
from jasonlib._trend_models import CoinDataset, InsufficientDataError
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _normalize_merge_time_precision(series: pd.Series) -> pd.Series:
|
|
23
|
+
"""Force merge keys to a consistent timezone-aware precision for merge_asof."""
|
|
24
|
+
if series.dt.tz is None:
|
|
25
|
+
return series.dt.tz_localize("UTC").astype("datetime64[ns, UTC]")
|
|
26
|
+
return series.astype("datetime64[ns, UTC]")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def detect_pivot_breakouts(
|
|
30
|
+
jason_data: dict[str, pd.DataFrame],
|
|
31
|
+
ohlc_data: dict[str, pd.DataFrame],
|
|
32
|
+
cooldown_hours: float = 24.0,
|
|
33
|
+
lookback_hours: float = 4.0,
|
|
34
|
+
) -> pd.DataFrame:
|
|
35
|
+
"""Vectorized pivot breakout detection.
|
|
36
|
+
|
|
37
|
+
Detects when price breaks above/below the pivot from N hours ago.
|
|
38
|
+
Uses pd.merge_asof for efficient time-series matching.
|
|
39
|
+
Cooldown period avoids recounting oscillations.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
jason_data: JASON DataFrames with 'high_pivot', 'low_pivot' columns.
|
|
43
|
+
ohlc_data: OHLC DataFrames with 'close' column.
|
|
44
|
+
cooldown_hours: Minimum hours between counting separate breakout events.
|
|
45
|
+
lookback_hours: Hours to look back for pivot comparison.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
DataFrame with breakout status, distances, and counts per ticker.
|
|
49
|
+
"""
|
|
50
|
+
breakout_data = {}
|
|
51
|
+
lookback_timedelta = pd.Timedelta(hours=lookback_hours)
|
|
52
|
+
cooldown_timedelta = pd.Timedelta(hours=cooldown_hours)
|
|
53
|
+
|
|
54
|
+
for ticker in jason_data.keys():
|
|
55
|
+
if ticker not in ohlc_data:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
jason_df = jason_data[ticker].copy()
|
|
59
|
+
ohlc_df = ohlc_data[ticker].copy()
|
|
60
|
+
|
|
61
|
+
if jason_df.empty or ohlc_df.empty or len(jason_df) < 2:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
if "high_pivot" not in jason_df.columns or "low_pivot" not in jason_df.columns:
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# === CURRENT STATUS (Latest values) ===
|
|
68
|
+
latest_price = ohlc_df["close"].iloc[-1]
|
|
69
|
+
latest_timestamp = ohlc_df.index[-1]
|
|
70
|
+
|
|
71
|
+
# Find pivot from lookback_hours ago for current status
|
|
72
|
+
lookback_time = latest_timestamp - lookback_timedelta
|
|
73
|
+
valid_jason = jason_df[jason_df.index < latest_timestamp]
|
|
74
|
+
if len(valid_jason) == 0:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
time_diffs = abs(valid_jason.index - lookback_time)
|
|
78
|
+
closest_idx = time_diffs.argmin()
|
|
79
|
+
lookback_jason = valid_jason.iloc[closest_idx]
|
|
80
|
+
|
|
81
|
+
high_pivot = lookback_jason["high_pivot"]
|
|
82
|
+
low_pivot = lookback_jason["low_pivot"]
|
|
83
|
+
|
|
84
|
+
# Calculate distances
|
|
85
|
+
dist_to_high_pct = ((latest_price - high_pivot) / high_pivot) * 100
|
|
86
|
+
dist_to_low_pct = ((latest_price - low_pivot) / low_pivot) * 100
|
|
87
|
+
|
|
88
|
+
if latest_price > high_pivot:
|
|
89
|
+
current_breakout = "High Broken"
|
|
90
|
+
elif latest_price < low_pivot:
|
|
91
|
+
current_breakout = "Low Broken"
|
|
92
|
+
else:
|
|
93
|
+
current_breakout = "Between Pivots"
|
|
94
|
+
|
|
95
|
+
# Merge OHLC and JASON data
|
|
96
|
+
merged = pd.merge(
|
|
97
|
+
ohlc_df[["close"]],
|
|
98
|
+
jason_df[["high_pivot", "low_pivot"]],
|
|
99
|
+
left_index=True,
|
|
100
|
+
right_index=True,
|
|
101
|
+
how="inner",
|
|
102
|
+
).dropna()
|
|
103
|
+
|
|
104
|
+
if len(merged) < 2:
|
|
105
|
+
breakout_data[ticker] = {
|
|
106
|
+
"current_price": latest_price,
|
|
107
|
+
"high_pivot": high_pivot,
|
|
108
|
+
"low_pivot": low_pivot,
|
|
109
|
+
"dist_to_high_pivot_pct": dist_to_high_pct,
|
|
110
|
+
"dist_to_low_pivot_pct": dist_to_low_pct,
|
|
111
|
+
"current_breakout": current_breakout,
|
|
112
|
+
"high_breaks_count": 0,
|
|
113
|
+
"low_breaks_count": 0,
|
|
114
|
+
"total_breaks": 0,
|
|
115
|
+
"last_breakout_time": None,
|
|
116
|
+
"hours_since_last_breakout": None,
|
|
117
|
+
}
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
high_breaks = 0
|
|
121
|
+
low_breaks = 0
|
|
122
|
+
last_high_break_time = None
|
|
123
|
+
last_low_break_time = None
|
|
124
|
+
|
|
125
|
+
# === VECTORIZED HISTORICAL BREAKOUT COUNTING ===
|
|
126
|
+
merged_with_target = merged.copy()
|
|
127
|
+
merged_with_target["target_time"] = merged_with_target.index - lookback_timedelta
|
|
128
|
+
|
|
129
|
+
# Prepare jason data for merge_asof
|
|
130
|
+
jason_for_merge = jason_df[["high_pivot", "low_pivot"]].copy()
|
|
131
|
+
jason_for_merge = jason_for_merge.reset_index()
|
|
132
|
+
jason_for_merge.columns = ["time", "lookback_high_pivot", "lookback_low_pivot"]
|
|
133
|
+
jason_for_merge["time"] = _normalize_merge_time_precision(jason_for_merge["time"])
|
|
134
|
+
|
|
135
|
+
merged_with_target = merged_with_target.reset_index()
|
|
136
|
+
merged_with_target.columns = ["time", "close", "high_pivot", "low_pivot", "target_time"]
|
|
137
|
+
merged_with_target["target_time"] = _normalize_merge_time_precision(
|
|
138
|
+
merged_with_target["target_time"]
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Merge lookback pivots using merge_asof (efficient nearest-match join)
|
|
142
|
+
result = pd.merge_asof(
|
|
143
|
+
merged_with_target.sort_values("target_time"),
|
|
144
|
+
jason_for_merge.sort_values("time"),
|
|
145
|
+
left_on="target_time",
|
|
146
|
+
right_on="time",
|
|
147
|
+
direction="nearest",
|
|
148
|
+
suffixes=("", "_lookup"),
|
|
149
|
+
)
|
|
150
|
+
result = result.set_index("time").sort_index()
|
|
151
|
+
|
|
152
|
+
# Vectorized breakout detection
|
|
153
|
+
result["above_high"] = result["close"] > result["lookback_high_pivot"]
|
|
154
|
+
result["below_low"] = result["close"] < result["lookback_low_pivot"]
|
|
155
|
+
|
|
156
|
+
# Detect crossovers (transitions from False to True)
|
|
157
|
+
result["high_cross"] = result["above_high"] & ~result["above_high"].shift(
|
|
158
|
+
1, fill_value=False
|
|
159
|
+
)
|
|
160
|
+
result["low_cross"] = result["below_low"] & ~result["below_low"].shift(
|
|
161
|
+
1, fill_value=False
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Apply cooldown — iterate only over crossover events
|
|
165
|
+
high_break_indices = result[result["high_cross"]].index
|
|
166
|
+
for break_time in high_break_indices:
|
|
167
|
+
if (
|
|
168
|
+
last_high_break_time is None
|
|
169
|
+
or (break_time - last_high_break_time) > cooldown_timedelta
|
|
170
|
+
):
|
|
171
|
+
high_breaks += 1
|
|
172
|
+
last_high_break_time = break_time
|
|
173
|
+
|
|
174
|
+
low_break_indices = result[result["low_cross"]].index
|
|
175
|
+
for break_time in low_break_indices:
|
|
176
|
+
if (
|
|
177
|
+
last_low_break_time is None
|
|
178
|
+
or (break_time - last_low_break_time) > cooldown_timedelta
|
|
179
|
+
):
|
|
180
|
+
low_breaks += 1
|
|
181
|
+
last_low_break_time = break_time
|
|
182
|
+
|
|
183
|
+
# Determine the last breakout time
|
|
184
|
+
last_breakout_time = None
|
|
185
|
+
if last_high_break_time is not None and last_low_break_time is not None:
|
|
186
|
+
last_breakout_time = max(last_high_break_time, last_low_break_time)
|
|
187
|
+
elif last_high_break_time is not None:
|
|
188
|
+
last_breakout_time = last_high_break_time
|
|
189
|
+
elif last_low_break_time is not None:
|
|
190
|
+
last_breakout_time = last_low_break_time
|
|
191
|
+
|
|
192
|
+
# Calculate time since last breakout
|
|
193
|
+
time_since_last_breakout = None
|
|
194
|
+
if last_breakout_time is not None:
|
|
195
|
+
time_since_last_breakout = (
|
|
196
|
+
latest_timestamp - last_breakout_time
|
|
197
|
+
).total_seconds() / 3600.0
|
|
198
|
+
|
|
199
|
+
breakout_data[ticker] = {
|
|
200
|
+
"current_price": latest_price,
|
|
201
|
+
"high_pivot": high_pivot,
|
|
202
|
+
"low_pivot": low_pivot,
|
|
203
|
+
"dist_to_high_pivot_pct": dist_to_high_pct,
|
|
204
|
+
"dist_to_low_pivot_pct": dist_to_low_pct,
|
|
205
|
+
"current_breakout": current_breakout,
|
|
206
|
+
"high_breaks_count": high_breaks,
|
|
207
|
+
"low_breaks_count": low_breaks,
|
|
208
|
+
"total_breaks": high_breaks + low_breaks,
|
|
209
|
+
"last_breakout_time": last_breakout_time,
|
|
210
|
+
"hours_since_last_breakout": time_since_last_breakout,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return pd.DataFrame.from_dict(breakout_data, orient="index")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
_PIVOT_SORT_COLUMN_MAP: dict[PivotSortOption, str] = {
|
|
217
|
+
PivotSortOption.TOTAL_BREAKS: "total_breaks",
|
|
218
|
+
PivotSortOption.HIGH_BREAKS: "high_breaks_count",
|
|
219
|
+
PivotSortOption.LOW_BREAKS: "low_breaks_count",
|
|
220
|
+
PivotSortOption.BREAKOUT_STATUS: "current_breakout",
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def compute_pivot_analysis(
|
|
225
|
+
datasets: list[CoinDataset],
|
|
226
|
+
interval: JInterval,
|
|
227
|
+
start_date: pd.Timestamp,
|
|
228
|
+
end_date: pd.Timestamp,
|
|
229
|
+
cooldown_hours: float = 24.0,
|
|
230
|
+
lookback_hours: float = 4.0,
|
|
231
|
+
sort_by: PivotSortOption = PivotSortOption.TOTAL_BREAKS,
|
|
232
|
+
) -> PivotAnalysisResult:
|
|
233
|
+
"""Compute pivot breakout analysis across a set of coins.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
datasets: List of CoinDataset, each containing a coin identifier,
|
|
237
|
+
OHLC klines DataFrame (with 'close' column), and jason DataFrame
|
|
238
|
+
(with 'high_pivot', 'low_pivot' columns).
|
|
239
|
+
interval: Data interval (e.g., JInterval.H4, JInterval.D1).
|
|
240
|
+
start_date: Start of the analysis window.
|
|
241
|
+
end_date: End of the analysis window.
|
|
242
|
+
cooldown_hours: Minimum hours between counting separate breakout events.
|
|
243
|
+
lookback_hours: Hours to look back for pivot comparison.
|
|
244
|
+
sort_by: How to sort the breakout results.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
PivotAnalysisResult with summary metrics and breakout DataFrame.
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
ValueError: If start_date > end_date.
|
|
251
|
+
InsufficientDataError: If no coins have enough data to produce results.
|
|
252
|
+
"""
|
|
253
|
+
if start_date > end_date:
|
|
254
|
+
raise ValueError("start_date must be before or equal to end_date")
|
|
255
|
+
|
|
256
|
+
# Convert CoinDataset list to dict pairs for detect_pivot_breakouts
|
|
257
|
+
jason_data = {ds.coin: ds.jason_df for ds in datasets}
|
|
258
|
+
ohlc_data = {ds.coin: ds.klines_df for ds in datasets}
|
|
259
|
+
|
|
260
|
+
breakout_df = detect_pivot_breakouts(
|
|
261
|
+
jason_data=jason_data,
|
|
262
|
+
ohlc_data=ohlc_data,
|
|
263
|
+
cooldown_hours=cooldown_hours,
|
|
264
|
+
lookback_hours=lookback_hours,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if breakout_df.empty:
|
|
268
|
+
raise InsufficientDataError(
|
|
269
|
+
"No coins have enough data to produce pivot analysis results."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Sort by requested column
|
|
273
|
+
sort_col = _PIVOT_SORT_COLUMN_MAP[sort_by]
|
|
274
|
+
sorted_df = breakout_df.sort_values(by=sort_col, ascending=False)
|
|
275
|
+
|
|
276
|
+
# Compute summary
|
|
277
|
+
summary = PivotAnalysisSummary(
|
|
278
|
+
high_pivots_broken=int((sorted_df["current_breakout"] == "High Broken").sum()),
|
|
279
|
+
low_pivots_broken=int((sorted_df["current_breakout"] == "Low Broken").sum()),
|
|
280
|
+
avg_total_breaks=float(sorted_df["total_breaks"].mean()),
|
|
281
|
+
between_pivots=int((sorted_df["current_breakout"] == "Between Pivots").sum()),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Determine as_of from latest OHLC timestamp
|
|
285
|
+
latest_timestamps = []
|
|
286
|
+
for ds in datasets:
|
|
287
|
+
if ds.klines_df is not None and not ds.klines_df.empty:
|
|
288
|
+
latest_timestamps.append(pd.Timestamp(ds.klines_df.index.max()))
|
|
289
|
+
as_of = max(latest_timestamps) if latest_timestamps else datetime.now(timezone.utc)
|
|
290
|
+
# Ensure as_of is a proper datetime
|
|
291
|
+
if isinstance(as_of, pd.Timestamp):
|
|
292
|
+
as_of = as_of.to_pydatetime()
|
|
293
|
+
|
|
294
|
+
return PivotAnalysisResult(
|
|
295
|
+
start_date=start_date.strftime("%Y-%m-%d"),
|
|
296
|
+
end_date=end_date.strftime("%Y-%m-%d"),
|
|
297
|
+
interval=interval,
|
|
298
|
+
generated_at=datetime.now(timezone.utc),
|
|
299
|
+
as_of=as_of,
|
|
300
|
+
parameters=PivotAnalysisParameters(
|
|
301
|
+
cooldown_hours=cooldown_hours,
|
|
302
|
+
lookback_hours=lookback_hours,
|
|
303
|
+
sort_by=sort_by,
|
|
304
|
+
sort_options=list(PivotSortOption),
|
|
305
|
+
),
|
|
306
|
+
summary=summary,
|
|
307
|
+
breakouts=sorted_df,
|
|
308
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Pivot analysis data models and enums."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from jasonlib._models import JInterval
|
|
11
|
+
from jasonlib._trend_models import BaseModelWithPandasFields
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PivotSortOption(str, Enum):
|
|
15
|
+
"""Sort options for pivot breakout results."""
|
|
16
|
+
|
|
17
|
+
TOTAL_BREAKS = "Total Breaks"
|
|
18
|
+
HIGH_BREAKS = "High Breaks"
|
|
19
|
+
LOW_BREAKS = "Low Breaks"
|
|
20
|
+
BREAKOUT_STATUS = "Breakout Status"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PivotAnalysisSummary(BaseModelWithPandasFields):
|
|
24
|
+
high_pivots_broken: int
|
|
25
|
+
low_pivots_broken: int
|
|
26
|
+
avg_total_breaks: float
|
|
27
|
+
between_pivots: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PivotAnalysisParameters(BaseModelWithPandasFields):
|
|
31
|
+
cooldown_hours: float
|
|
32
|
+
lookback_hours: float
|
|
33
|
+
sort_by: PivotSortOption
|
|
34
|
+
sort_options: list[PivotSortOption]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PivotAnalysisResult(BaseModelWithPandasFields):
|
|
38
|
+
start_date: str
|
|
39
|
+
end_date: str
|
|
40
|
+
interval: JInterval
|
|
41
|
+
generated_at: datetime
|
|
42
|
+
as_of: datetime
|
|
43
|
+
parameters: PivotAnalysisParameters
|
|
44
|
+
summary: PivotAnalysisSummary
|
|
45
|
+
breakouts: pd.DataFrame
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Trading calendar constants and helpers for JASON calculations."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from jasonlib._models import JAssetClass, JInterval
|
|
6
|
+
|
|
7
|
+
TRADING_DAYS_PER_YEAR: dict[JAssetClass, int] = {
|
|
8
|
+
JAssetClass.CRYPTO: 365,
|
|
9
|
+
JAssetClass.EQUITIES: 252,
|
|
10
|
+
JAssetClass.COMMOD: 252,
|
|
11
|
+
JAssetClass.FX: 252,
|
|
12
|
+
JAssetClass.EQUITY_IDX: 252,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
TRADING_HOURS_PER_DAY: dict[JAssetClass, float] = {
|
|
16
|
+
JAssetClass.CRYPTO: 24.0,
|
|
17
|
+
JAssetClass.EQUITIES: 6.5,
|
|
18
|
+
JAssetClass.COMMOD: 23.0,
|
|
19
|
+
JAssetClass.FX: 23.0,
|
|
20
|
+
JAssetClass.EQUITY_IDX: 23.0,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def normalize_asset_class(asset_class: JAssetClass | str | None) -> JAssetClass:
|
|
25
|
+
"""Normalize legacy strings and enum members to JAssetClass.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
asset_class: A JAssetClass member, a string alias, or None.
|
|
29
|
+
None defaults to CRYPTO.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The resolved JAssetClass member.
|
|
33
|
+
"""
|
|
34
|
+
if isinstance(asset_class, JAssetClass):
|
|
35
|
+
return asset_class
|
|
36
|
+
if asset_class is None:
|
|
37
|
+
return JAssetClass.CRYPTO
|
|
38
|
+
|
|
39
|
+
alias_map: dict[str, JAssetClass] = {
|
|
40
|
+
"commod": JAssetClass.COMMOD,
|
|
41
|
+
"commodity": JAssetClass.COMMOD,
|
|
42
|
+
"commodities": JAssetClass.COMMOD,
|
|
43
|
+
"crypto": JAssetClass.CRYPTO,
|
|
44
|
+
"fx": JAssetClass.FX,
|
|
45
|
+
"equity": JAssetClass.EQUITIES,
|
|
46
|
+
"equities": JAssetClass.EQUITIES,
|
|
47
|
+
"equity_idx": JAssetClass.EQUITY_IDX,
|
|
48
|
+
}
|
|
49
|
+
return alias_map.get(asset_class.strip().lower(), JAssetClass.CRYPTO)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_logger(name: str) -> logging.Logger:
|
|
53
|
+
"""Get a configured logger instance.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
name: Name for the logger.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Configured Logger with a StreamHandler and INFO level.
|
|
60
|
+
"""
|
|
61
|
+
logger = logging.getLogger(name)
|
|
62
|
+
if not logger.handlers:
|
|
63
|
+
handler = logging.StreamHandler()
|
|
64
|
+
formatter = logging.Formatter(
|
|
65
|
+
"[%(asctime)s] %(levelname)s %(name)s: %(message)s"
|
|
66
|
+
)
|
|
67
|
+
handler.setFormatter(formatter)
|
|
68
|
+
logger.addHandler(handler)
|
|
69
|
+
logger.setLevel(logging.INFO)
|
|
70
|
+
return logger
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_annualization_factor(interval: JInterval, asset_class: JAssetClass) -> float:
|
|
74
|
+
"""Return the number of periods per year for the given interval and asset class.
|
|
75
|
+
|
|
76
|
+
Computes the annualization factor based on the interval duration in seconds,
|
|
77
|
+
trading days per year, and trading hours per day for the asset class.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
interval: Candle interval.
|
|
81
|
+
asset_class: Asset class determining the trading calendar.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Number of periods per year.
|
|
85
|
+
"""
|
|
86
|
+
trading_days = TRADING_DAYS_PER_YEAR[asset_class]
|
|
87
|
+
hours_per_day = TRADING_HOURS_PER_DAY[asset_class]
|
|
88
|
+
trading_seconds_per_day = hours_per_day * 3600
|
|
89
|
+
if interval.seconds >= 86400:
|
|
90
|
+
periods_per_day = 1.0
|
|
91
|
+
else:
|
|
92
|
+
periods_per_day = trading_seconds_per_day / interval.seconds
|
|
93
|
+
return trading_days * periods_per_day
|