algotrading-core 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.
- algotrading_core/__init__.py +0 -0
- algotrading_core/features/__init__.py +7 -0
- algotrading_core/features/aggregated.py +0 -0
- algotrading_core/features/base.py +0 -0
- algotrading_core/features/levels.py +297 -0
- algotrading_core/features/time_features.py +0 -0
- algotrading_core/features/volatility.py +0 -0
- algotrading_core/preprocessing/__init__.py +0 -0
- algotrading_core/preprocessing/adjustments.py +0 -0
- algotrading_core/preprocessing/candles.py +0 -0
- algotrading_core/preprocessing/transformers.py +0 -0
- algotrading_core/schemas/__init__.py +7 -0
- algotrading_core/schemas/candle.py +26 -0
- algotrading_core/schemas/feature.py +0 -0
- algotrading_core/schemas/model.py +0 -0
- algotrading_core/utils/__init__.py +15 -0
- algotrading_core/utils/datetime_utils.py +0 -0
- algotrading_core/utils/path_utils.py +97 -0
- algotrading_core/utils/setup_logger.py +83 -0
- algotrading_core-0.1.0.dist-info/METADATA +314 -0
- algotrading_core-0.1.0.dist-info/RECORD +22 -0
- algotrading_core-0.1.0.dist-info/WHEEL +4 -0
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Support and resistance levels feature generation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from scipy.signal import argrelextrema
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
_REQUIRED_OHLC_COLUMNS = ["open", "high", "low", "close"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _validate_ohlc_columns(df: pd.DataFrame, context: str = "") -> None:
|
|
16
|
+
"""Validate DataFrame has required OHLC columns.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
df: DataFrame to validate.
|
|
20
|
+
context: Optional context for error message.
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
ValueError: If any required column is missing.
|
|
24
|
+
"""
|
|
25
|
+
missing = set(_REQUIRED_OHLC_COLUMNS) - set(df.columns)
|
|
26
|
+
if missing:
|
|
27
|
+
raise ValueError(
|
|
28
|
+
f"❌ Missing required columns: {sorted(missing)}\n"
|
|
29
|
+
f" Available columns: {sorted(df.columns)}\n"
|
|
30
|
+
f" Context: {context or 'levels feature'}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _levels_dict_to_dataframe(levels_dict: dict) -> pd.DataFrame:
|
|
35
|
+
"""Convert levels dict to DataFrame with start_date, end_date, level.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
levels_dict: Dict mapping start_date -> (level, end_date).
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
DataFrame with columns start_date, end_date, level, sorted by start_date.
|
|
42
|
+
"""
|
|
43
|
+
levels_df = pd.DataFrame.from_dict(levels_dict, orient="index", columns=["level", "end_date"])
|
|
44
|
+
levels_df.index.name = "start_date"
|
|
45
|
+
levels_df = levels_df.reset_index().sort_values(by="start_date")
|
|
46
|
+
return levels_df
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _compute_is_support_per_level(df: pd.DataFrame, levels_df: pd.DataFrame) -> pd.Series:
|
|
50
|
+
"""Compute whether each level acts as support (True) or resistance (False).
|
|
51
|
+
|
|
52
|
+
A level is support when more closes in its range are above the level than below.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
df: OHLC DataFrame with datetime index.
|
|
56
|
+
levels_df: DataFrame with columns start_date, end_date, level.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Boolean Series indexed by levels_df index (True = support, False = resistance).
|
|
60
|
+
"""
|
|
61
|
+
is_support_list = []
|
|
62
|
+
for row in levels_df.itertuples():
|
|
63
|
+
end = row.end_date if pd.notna(row.end_date) else df.index[-1]
|
|
64
|
+
valid_close = df.loc[row.start_date : end, "close"]
|
|
65
|
+
below = (valid_close <= row.level).sum()
|
|
66
|
+
above = (valid_close > row.level).sum()
|
|
67
|
+
is_support_list.append(below < above)
|
|
68
|
+
return pd.Series(is_support_list, index=levels_df.index)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _assign_levels_vectorized(
|
|
72
|
+
df: pd.DataFrame,
|
|
73
|
+
levels_df: pd.DataFrame,
|
|
74
|
+
column_name: str,
|
|
75
|
+
) -> pd.DataFrame:
|
|
76
|
+
"""Assign support/resistance level to each row using merge_asof (vectorized).
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
df: OHLC DataFrame; modified in place with new columns.
|
|
80
|
+
levels_df: DataFrame with start_date, end_date, level, is_support.
|
|
81
|
+
column_name: Base name for output columns (e.g. closest_level_10th_order).
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
df with columns {column_name}_support and {column_name}_resistance added.
|
|
85
|
+
"""
|
|
86
|
+
support_col = f"{column_name}_support"
|
|
87
|
+
resistance_col = f"{column_name}_resistance"
|
|
88
|
+
df[support_col] = np.nan
|
|
89
|
+
df[resistance_col] = np.nan
|
|
90
|
+
|
|
91
|
+
if levels_df.empty:
|
|
92
|
+
return df
|
|
93
|
+
|
|
94
|
+
levels_sorted = levels_df.sort_values("start_date").copy()
|
|
95
|
+
df_sorted = df.sort_index()
|
|
96
|
+
merged = pd.merge_asof(
|
|
97
|
+
df_sorted,
|
|
98
|
+
levels_sorted,
|
|
99
|
+
left_index=True,
|
|
100
|
+
right_on="start_date",
|
|
101
|
+
direction="backward",
|
|
102
|
+
)
|
|
103
|
+
in_range = (merged.index >= merged["start_date"]) & (
|
|
104
|
+
merged["end_date"].isna() | (merged.index <= merged["end_date"])
|
|
105
|
+
)
|
|
106
|
+
merged[support_col] = np.where(merged["is_support"] & in_range, merged["level"], np.nan)
|
|
107
|
+
merged[resistance_col] = np.where(~merged["is_support"] & in_range, merged["level"], np.nan)
|
|
108
|
+
df[support_col] = merged[support_col].reindex(df.index).values
|
|
109
|
+
df[resistance_col] = merged[resistance_col].reindex(df.index).values
|
|
110
|
+
return df
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def generate_levels_feature(
|
|
114
|
+
df: pd.DataFrame,
|
|
115
|
+
order: int,
|
|
116
|
+
) -> pd.DataFrame:
|
|
117
|
+
"""Create support and resistance level features.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
df: DataFrame containing OHLC candle data with datetime index.
|
|
121
|
+
order: Order for local extrema (number of candles per side).
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
DataFrame with added columns {column_name}_support and {column_name}_resistance,
|
|
125
|
+
where column_name is closest_level_{order}th_order.
|
|
126
|
+
"""
|
|
127
|
+
_validate_ohlc_columns(df, context="generate_levels_feature")
|
|
128
|
+
column_name = f"closest_level_{order}th_order"
|
|
129
|
+
levels_dict = _build_levels(df, order)
|
|
130
|
+
if not levels_dict:
|
|
131
|
+
logger.debug("🔄 No levels found for order=%d, leaving support/resistance empty", order)
|
|
132
|
+
df[f"{column_name}_support"] = np.nan
|
|
133
|
+
df[f"{column_name}_resistance"] = np.nan
|
|
134
|
+
return df
|
|
135
|
+
|
|
136
|
+
levels_df = _levels_dict_to_dataframe(levels_dict)
|
|
137
|
+
levels_df["is_support"] = _compute_is_support_per_level(df, levels_df)
|
|
138
|
+
return _assign_levels_vectorized(df, levels_df, column_name)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _detect_levels(df: pd.DataFrame, order: int = 10) -> dict[Any, tuple[float, Any | None]]:
|
|
142
|
+
"""Detect support and resistance levels using local extrema.
|
|
143
|
+
|
|
144
|
+
Identifies local minima (support) and maxima (resistance) in price data.
|
|
145
|
+
Ensures intercalation of support and resistance levels.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
df: DataFrame with at least 'low' and 'high' and datetime index.
|
|
149
|
+
order: Number of candles to consider for local extrema.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Dict mapping start_date -> (price_level, None). None reserved for end_date.
|
|
153
|
+
"""
|
|
154
|
+
_validate_ohlc_columns(df, context="_detect_levels")
|
|
155
|
+
low_prices = df["low"].to_numpy()
|
|
156
|
+
high_prices = df["high"].to_numpy()
|
|
157
|
+
dates = df.index.to_numpy()
|
|
158
|
+
|
|
159
|
+
local_minima = argrelextrema(low_prices, np.less, order=order)[0]
|
|
160
|
+
local_maxima = argrelextrema(high_prices, np.greater, order=order)[0]
|
|
161
|
+
|
|
162
|
+
levels = np.concatenate(
|
|
163
|
+
[
|
|
164
|
+
np.column_stack((local_minima, low_prices[local_minima], np.zeros(len(local_minima)))),
|
|
165
|
+
np.column_stack((local_maxima, high_prices[local_maxima], np.ones(len(local_maxima)))),
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
levels = levels[levels[:, 0].argsort()]
|
|
169
|
+
|
|
170
|
+
valid_indices = (levels[:, 0] >= order) & (levels[:, 0] < len(df) - order)
|
|
171
|
+
levels = levels[valid_indices.astype(bool)]
|
|
172
|
+
|
|
173
|
+
intercalated_levels = _intercalate_levels(levels, low_prices, high_prices)
|
|
174
|
+
if not intercalated_levels.size:
|
|
175
|
+
return {}
|
|
176
|
+
|
|
177
|
+
return {dates[int(idx)]: (float(price), None) for idx, price, _ in intercalated_levels}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _intercalate_levels(
|
|
181
|
+
levels: np.ndarray,
|
|
182
|
+
low_prices: np.ndarray,
|
|
183
|
+
high_prices: np.ndarray,
|
|
184
|
+
) -> np.ndarray:
|
|
185
|
+
"""Ensure support and resistance levels alternate by inserting intermediate levels.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
levels: Array of (index, price, type) with type 0=support, 1=resistance.
|
|
189
|
+
low_prices: Full low price array.
|
|
190
|
+
high_prices: Full high price array.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Array of intercalated (index, price, type).
|
|
194
|
+
"""
|
|
195
|
+
intercalated: list[list[float]] = []
|
|
196
|
+
for i in range(len(levels) - 1):
|
|
197
|
+
current_type = levels[i, 2]
|
|
198
|
+
next_type = levels[i + 1, 2]
|
|
199
|
+
if current_type == next_type:
|
|
200
|
+
start_idx, end_idx = int(levels[i, 0]), int(levels[i + 1, 0])
|
|
201
|
+
if current_type == 0:
|
|
202
|
+
intermediate_idx = np.argmax(high_prices[start_idx:end_idx]) + start_idx
|
|
203
|
+
intercalated.append([float(intermediate_idx), high_prices[intermediate_idx], 1.0])
|
|
204
|
+
else:
|
|
205
|
+
intermediate_idx = np.argmin(low_prices[start_idx:end_idx]) + start_idx
|
|
206
|
+
intercalated.append([float(intermediate_idx), low_prices[intermediate_idx], 0.0])
|
|
207
|
+
intercalated.append(levels[i].tolist())
|
|
208
|
+
if levels.shape[0] > 0:
|
|
209
|
+
intercalated.append(levels[-1].tolist())
|
|
210
|
+
return np.array(intercalated)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _compute_level_end_dates(
|
|
214
|
+
df: pd.DataFrame,
|
|
215
|
+
levels: dict[Any, tuple[float, Any | None]],
|
|
216
|
+
) -> dict[Any, tuple[float, Any | None]]:
|
|
217
|
+
"""Compute end date for each level (first price crossing).
|
|
218
|
+
|
|
219
|
+
Filters levels by subsequent price action: end_date is when price first
|
|
220
|
+
crosses the level, or None if it never does.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
df: DataFrame with 'open' and 'close' and datetime index.
|
|
224
|
+
levels: Dict mapping start_date -> (price_level, None).
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Dict mapping start_date -> (price_level, end_date). end_date is when
|
|
228
|
+
price first crosses the level, or None if never.
|
|
229
|
+
"""
|
|
230
|
+
_validate_ohlc_columns(df, context="_compute_level_end_dates")
|
|
231
|
+
open_prices = df["open"].to_numpy()
|
|
232
|
+
close_prices = df["close"].to_numpy()
|
|
233
|
+
dates = df.index.to_numpy()
|
|
234
|
+
|
|
235
|
+
filtered_levels: dict[Any, tuple[float, Any | None]] = {}
|
|
236
|
+
for date, (price, _) in levels.items():
|
|
237
|
+
idx = np.searchsorted(dates, date)
|
|
238
|
+
future_open = open_prices[idx + 1 :]
|
|
239
|
+
future_close = close_prices[idx + 1 :]
|
|
240
|
+
crossing = (future_open > price) & (future_close < price) | (
|
|
241
|
+
(future_open < price) & (future_close > price)
|
|
242
|
+
)
|
|
243
|
+
filtered_idx = np.argmax(crossing) if crossing.any() else None
|
|
244
|
+
filtered_date = dates[idx + 1 + filtered_idx] if filtered_idx is not None else None
|
|
245
|
+
filtered_levels[date] = (price, filtered_date)
|
|
246
|
+
return filtered_levels
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _offset_levels(
|
|
250
|
+
df: pd.DataFrame,
|
|
251
|
+
levels: dict[Any, tuple[float, Any | None]],
|
|
252
|
+
order: int,
|
|
253
|
+
) -> dict[Any, tuple[float, Any | None]]:
|
|
254
|
+
"""Offset level start dates by order candles.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
df: DataFrame with datetime index.
|
|
258
|
+
levels: Dict mapping start_date -> (price_level, end_date).
|
|
259
|
+
order: Number of candles to offset.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Dict mapping (offset) start_date -> (price_level, end_date).
|
|
263
|
+
"""
|
|
264
|
+
dates = df.index.to_numpy()
|
|
265
|
+
result: dict[Any, tuple[float, Any | None]] = {}
|
|
266
|
+
previous_end_date: Any | None = None
|
|
267
|
+
|
|
268
|
+
for start_date, (value, end_date) in levels.items():
|
|
269
|
+
idx = np.searchsorted(dates, start_date)
|
|
270
|
+
offset_idx = min(idx + order, len(dates) - 1)
|
|
271
|
+
offset_date = dates[offset_idx]
|
|
272
|
+
|
|
273
|
+
if previous_end_date is not None and offset_date > previous_end_date:
|
|
274
|
+
result[previous_end_date] = (value, end_date)
|
|
275
|
+
else:
|
|
276
|
+
result[offset_date] = (value, end_date)
|
|
277
|
+
previous_end_date = end_date
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _build_levels(df: pd.DataFrame, order: int = 10) -> dict[Any, tuple[float, Any | None]]:
|
|
282
|
+
"""Build levels pipeline: detect, compute end dates, then offset.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
df: DataFrame with OHLC and datetime index.
|
|
286
|
+
order: Order for local extrema and offset.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Dict mapping start_date -> (price_level, end_date). Empty dict if no levels.
|
|
290
|
+
"""
|
|
291
|
+
_validate_ohlc_columns(df, context="_build_levels")
|
|
292
|
+
levels_dict = _detect_levels(df, order)
|
|
293
|
+
if not levels_dict:
|
|
294
|
+
return {}
|
|
295
|
+
levels_dict = _compute_level_end_dates(df, levels_dict)
|
|
296
|
+
levels_dict = _offset_levels(df, levels_dict, order)
|
|
297
|
+
return levels_dict
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Candle (OHLCV) schema."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, model_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Candle(BaseModel):
|
|
9
|
+
"""Single candlestick: timestamp and OHLCV.
|
|
10
|
+
|
|
11
|
+
Matches CSV format with columns: date, open, high, low, close, volume.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
date: datetime = Field(..., description="Candle timestamp (e.g. 2012-07-02 09:00:00)")
|
|
15
|
+
open: float = Field(..., gt=0, description="Open price")
|
|
16
|
+
high: float = Field(..., gt=0, description="High price")
|
|
17
|
+
low: float = Field(..., gt=0, description="Low price")
|
|
18
|
+
close: float = Field(..., gt=0, description="Close price")
|
|
19
|
+
volume: float = Field(..., ge=0, description="Trading volume")
|
|
20
|
+
|
|
21
|
+
@model_validator(mode="after")
|
|
22
|
+
def high_not_below_low(self) -> "Candle":
|
|
23
|
+
"""Ensure high >= low."""
|
|
24
|
+
if self.high < self.low:
|
|
25
|
+
raise ValueError(f"❌ high ({self.high}) must be >= low ({self.low})")
|
|
26
|
+
return self
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Shared utilities for algotrading-core."""
|
|
2
|
+
|
|
3
|
+
from algotrading_core.utils.path_utils import (
|
|
4
|
+
add_project_to_sys_path,
|
|
5
|
+
get_project_root,
|
|
6
|
+
get_project_subpath,
|
|
7
|
+
)
|
|
8
|
+
from algotrading_core.utils.setup_logger import setup_logger
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"add_project_to_sys_path",
|
|
12
|
+
"get_project_root",
|
|
13
|
+
"get_project_subpath",
|
|
14
|
+
"setup_logger",
|
|
15
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Path utilities for algotrading projects.
|
|
2
|
+
|
|
3
|
+
Works for any project that has pyproject.toml at the root. Supports:
|
|
4
|
+
- algotrading-core: src layout (use subpath=\"src\" when adding to sys.path).
|
|
5
|
+
- algotrading-research, algotrading-backend, algotrading-execution: package
|
|
6
|
+
dirs at project root (use subpath=None when adding to sys.path).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
_PYPROJECT_FILENAME = "pyproject.toml"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_project_root() -> Path:
|
|
16
|
+
"""Return the project root directory (directory containing pyproject.toml).
|
|
17
|
+
|
|
18
|
+
Walks up from this file's location until pyproject.toml is found.
|
|
19
|
+
Works when run from an installed package or from source.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Absolute path to project root.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
FileNotFoundError: If pyproject.toml is not found in any parent directory.
|
|
26
|
+
"""
|
|
27
|
+
current = Path(__file__).resolve().parent
|
|
28
|
+
for parent in current.parents:
|
|
29
|
+
if (parent / _PYPROJECT_FILENAME).exists():
|
|
30
|
+
return parent
|
|
31
|
+
raise FileNotFoundError(
|
|
32
|
+
f"❌ '{_PYPROJECT_FILENAME}' not found in any parent of {current}\n"
|
|
33
|
+
" Run from the project tree."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_project_subpath(relative_path: str = "") -> Path:
|
|
38
|
+
"""Return a path under project root.
|
|
39
|
+
|
|
40
|
+
Use for any project layout: e.g. "src/algotrading_core", "ml", "backend",
|
|
41
|
+
"execution_agent". Empty string returns project root.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
relative_path: Path relative to project root (forward slashes). Use ""
|
|
45
|
+
for project root.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Absolute path: project_root / relative_path.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
FileNotFoundError: If relative_path is non-empty and the resolved path
|
|
52
|
+
does not exist.
|
|
53
|
+
"""
|
|
54
|
+
root = get_project_root()
|
|
55
|
+
if not relative_path:
|
|
56
|
+
return root
|
|
57
|
+
resolved = (root / relative_path).resolve()
|
|
58
|
+
if not resolved.exists():
|
|
59
|
+
raise FileNotFoundError(
|
|
60
|
+
f"❌ Path not found: {resolved}\n Relative to project root: {relative_path!r}"
|
|
61
|
+
)
|
|
62
|
+
return resolved
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def add_project_to_sys_path(subpath: str | None = None) -> str:
|
|
66
|
+
"""Prepend a path under project root to sys.path if not already present.
|
|
67
|
+
|
|
68
|
+
Use so that top-level packages are importable without installing.
|
|
69
|
+
Idempotent.
|
|
70
|
+
|
|
71
|
+
- algotrading-core (src layout): use subpath=\"src\" so ``import algotrading_core`` works.
|
|
72
|
+
- algotrading-research, algotrading-backend, algotrading-execution (packages
|
|
73
|
+
at root): use subpath=None so ``import ml``, ``import backend``, etc. work.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
subpath: Path relative to project root to add (e.g. "src"). None adds
|
|
77
|
+
project root.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The path that was ensured in sys.path (as string).
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
FileNotFoundError: If subpath is given and that directory does not exist.
|
|
84
|
+
"""
|
|
85
|
+
root = get_project_root()
|
|
86
|
+
if subpath is None:
|
|
87
|
+
path_to_add = root
|
|
88
|
+
else:
|
|
89
|
+
path_to_add = root / subpath
|
|
90
|
+
if not path_to_add.is_dir():
|
|
91
|
+
raise FileNotFoundError(
|
|
92
|
+
f"❌ Directory not found: {path_to_add}\n Subpath: {subpath!r}"
|
|
93
|
+
)
|
|
94
|
+
path_str = str(path_to_add)
|
|
95
|
+
if path_str not in sys.path:
|
|
96
|
+
sys.path.insert(0, path_str)
|
|
97
|
+
return path_str
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Logging setup for algotrading-core."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
_DEFAULT_LOG_FOLDER = "log"
|
|
9
|
+
_DEFAULT_LOG_LEVEL = logging.INFO
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _make_log_path(log_folder: str) -> str:
|
|
13
|
+
"""Build full path for today's log file.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
log_folder: Directory to write log files into.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Full path: log_folder/YYYY-MM-DD_HH-MM-SS.log
|
|
20
|
+
"""
|
|
21
|
+
now = datetime.now()
|
|
22
|
+
log_filename = f"{now.strftime('%Y-%m-%d_%H-%M-%S')}.log"
|
|
23
|
+
return os.path.join(log_folder, log_filename)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _create_formatter() -> logging.Formatter:
|
|
27
|
+
"""Create standard log formatter."""
|
|
28
|
+
return logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _add_handlers(
|
|
32
|
+
logger: logging.Logger,
|
|
33
|
+
log_path: str,
|
|
34
|
+
log_level: int,
|
|
35
|
+
formatter: logging.Formatter,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Add file and console handlers to root logger if none present.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
logger: Root logger to configure.
|
|
41
|
+
log_path: Full path for the log file.
|
|
42
|
+
log_level: Level for handlers.
|
|
43
|
+
formatter: Formatter for both handlers.
|
|
44
|
+
"""
|
|
45
|
+
file_handler = logging.FileHandler(log_path)
|
|
46
|
+
file_handler.setFormatter(formatter)
|
|
47
|
+
file_handler.setLevel(log_level)
|
|
48
|
+
logger.addHandler(file_handler)
|
|
49
|
+
|
|
50
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
51
|
+
console_handler.setFormatter(formatter)
|
|
52
|
+
console_handler.setLevel(log_level)
|
|
53
|
+
logger.addHandler(console_handler)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def setup_logger(
|
|
57
|
+
log_folder: str = _DEFAULT_LOG_FOLDER,
|
|
58
|
+
log_level: int = _DEFAULT_LOG_LEVEL,
|
|
59
|
+
) -> tuple[logging.Logger, str]:
|
|
60
|
+
"""Configure root logger with file and console handlers.
|
|
61
|
+
|
|
62
|
+
Creates log_folder if it does not exist. Uses a timestamped log filename.
|
|
63
|
+
Handlers are added only if the root logger has no handlers yet (idempotent).
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
log_folder: Directory for log files. Defaults to "log".
|
|
67
|
+
log_level: Logging level (e.g. logging.INFO). Defaults to INFO.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Tuple of (root logger, log filename only, e.g. "2025-01-29_12-00-00.log").
|
|
71
|
+
"""
|
|
72
|
+
log_path = _make_log_path(log_folder)
|
|
73
|
+
log_filename = os.path.basename(log_path)
|
|
74
|
+
os.makedirs(log_folder, exist_ok=True)
|
|
75
|
+
|
|
76
|
+
logger = logging.getLogger()
|
|
77
|
+
logger.setLevel(log_level)
|
|
78
|
+
|
|
79
|
+
if not logger.handlers:
|
|
80
|
+
formatter = _create_formatter()
|
|
81
|
+
_add_handlers(logger, log_path, log_level, formatter)
|
|
82
|
+
|
|
83
|
+
return logger, log_filename
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: algotrading-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared core package: schemas, preprocessing, features, and utils for the algorithmic trading platform
|
|
5
|
+
Requires-Python: <4,>=3.11
|
|
6
|
+
Requires-Dist: numpy<3,>=1.24
|
|
7
|
+
Requires-Dist: pandas<3,>=2.0
|
|
8
|
+
Requires-Dist: pydantic<3,>=2.0
|
|
9
|
+
Requires-Dist: pyyaml<7,>=6.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# algotrading-core
|
|
13
|
+
|
|
14
|
+
Shared core package for the algorithmic trading platform: **schemas**, **preprocessing**, **feature engineering**, and **utilities**. Single source of truth consumed by `algotrading-research` and `algotrading-backend` via pip—no duplicated core logic.
|
|
15
|
+
|
|
16
|
+
Aligned with [CODE_STRUCTURE_GUIDE.md](../CODE_STRUCTURE_GUIDE.md) (Core Layer) and [source/DEVELOPMENT_PHASES_GUIDE.md](../source/DEVELOPMENT_PHASES_GUIDE.md) (Phases 1–2).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Role in the platform
|
|
21
|
+
|
|
22
|
+
The platform uses a **multi-repository** layout:
|
|
23
|
+
|
|
24
|
+
| Repository | Role | Depends on core |
|
|
25
|
+
|-------------------------|------------------------------|------------------|
|
|
26
|
+
| **algotrading-core** | Schemas, preprocessing, features, utils | — |
|
|
27
|
+
| **algotrading-research**| Offline research, training, backtesting | ✅ |
|
|
28
|
+
| **algotrading-backend** | Signals, inference, API | ✅ |
|
|
29
|
+
| **algotrading-execution** | MT5 execution (signals only) | ❌ |
|
|
30
|
+
|
|
31
|
+
Core is **versioned and published** (private PyPI or Git). Research and backend pin a version and use the same feature logic for training and live inference.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Package structure
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
src/algotrading_core/
|
|
39
|
+
├── schemas/ # Pydantic data models (Phase 1)
|
|
40
|
+
│ ├── candle.py # OHLCV candle schema
|
|
41
|
+
│ ├── feature.py # Feature schema
|
|
42
|
+
│ ├── model.py # Model artifact schema
|
|
43
|
+
│ └── (config) # Configuration schemas
|
|
44
|
+
├── preprocessing/ # Data validation & transformation (Phase 2)
|
|
45
|
+
│ ├── candles.py # Candle validation, cleaning
|
|
46
|
+
│ ├── adjustments.py # Future contract adjustments
|
|
47
|
+
│ └── transformers.py # Aggregation, pipelines
|
|
48
|
+
├── features/ # Feature engineering (Phase 2)
|
|
49
|
+
│ ├── base.py # Base feature generator (abstract)
|
|
50
|
+
│ ├── levels.py # Support/resistance levels
|
|
51
|
+
│ ├── time_features.py # Time-based (e.g. trig encoding)
|
|
52
|
+
│ ├── volatility.py # Volatility features
|
|
53
|
+
│ └── aggregated.py # Aggregated timeframe features
|
|
54
|
+
└── utils/ # Shared utilities (Phase 1)
|
|
55
|
+
├── datetime_utils.py
|
|
56
|
+
└── path_utils.py
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- **Phase 1** (Foundation): schemas + utils + config loaders.
|
|
60
|
+
- **Phase 2** (Data & preprocessing): preprocessing + feature base and concrete generators.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
**Requires**: Python ≥3.11.
|
|
67
|
+
|
|
68
|
+
### From local path (development)
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv add /path/to/algotrading-core
|
|
72
|
+
# or
|
|
73
|
+
pip install -e /path/to/algotrading-core
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### From Git
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
uv add "algotrading-core @ git+https://github.com/org/algotrading-core.git@v0.1.0"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### From private PyPI
|
|
83
|
+
|
|
84
|
+
Configure your private index (see [Publishing to private PyPI](#publishing-to-private-pypi) for index URL and auth), then:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install algotrading-core==0.1.0
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Publishing to private PyPI
|
|
93
|
+
|
|
94
|
+
The package uses **semantic versioning** (e.g. `1.0.0`). To publish a release to a private PyPI server:
|
|
95
|
+
|
|
96
|
+
### 1. Bump version
|
|
97
|
+
|
|
98
|
+
Edit `version` in `pyproject.toml` (e.g. `0.1.0` → `0.2.0`). Tag the release in Git:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git tag v0.2.0
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 2. Build the package
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
make build
|
|
108
|
+
# or: uv run python -m build
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This produces `dist/algotrading-core-<version>.tar.gz` and `dist/algotrading_core-<version>-py3-none-any.whl`.
|
|
112
|
+
|
|
113
|
+
### 3. Configure credentials for your private index
|
|
114
|
+
|
|
115
|
+
**Option A — `.pypirc` (recommended)**
|
|
116
|
+
Create or edit `~/.pypirc`:
|
|
117
|
+
|
|
118
|
+
```ini
|
|
119
|
+
[distutils]
|
|
120
|
+
index-servers =
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
[private]
|
|
124
|
+
repository = https://your-private-pypi.example.com/pypi/
|
|
125
|
+
username = your-username
|
|
126
|
+
password = your-password-or-token
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Option B — Environment variables**
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
export TWINE_USERNAME=your-username
|
|
133
|
+
export TWINE_PASSWORD=your-password-or-token
|
|
134
|
+
export TWINE_REPOSITORY_URL=https://your-private-pypi.example.com/pypi/
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 4. Upload
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
make publish
|
|
141
|
+
# Uses .pypirc repo name "private" by default. Override: make publish REPO=myrepo
|
|
142
|
+
# Or use URL directly: make publish REPO_URL=https://your-private-pypi.example.com/pypi/
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Replace `your-private-pypi.example.com` with your actual private PyPI host (e.g. CodeArtifact, Artifactory, or self-hosted PyPI).
|
|
146
|
+
|
|
147
|
+
**Consumers** of the package must configure pip to use the same index (e.g. `pip.conf` or `pip install --index-url https://.../pypi/ algotrading-core`).
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Usage
|
|
152
|
+
|
|
153
|
+
Consumers import from the package; no copy-paste of core code.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from algotrading_core.schemas import Candle, Feature
|
|
157
|
+
from algotrading_core.preprocessing.candles import preprocess_candles
|
|
158
|
+
from algotrading_core.features.base import FeatureGenerator
|
|
159
|
+
from algotrading_core.features.levels import LevelsFeatureGenerator
|
|
160
|
+
from algotrading_core.utils.datetime_utils import parse_interval
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Research uses these for **training and backtesting**; the backend uses the **same** code for **live feature generation** and inference.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
### Setup
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
cd algotrading-core
|
|
173
|
+
uv sync
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Commands (Makefile)
|
|
177
|
+
|
|
178
|
+
| Target | Action |
|
|
179
|
+
|----------|----------------------------------|
|
|
180
|
+
| `make lint` | Ruff + black check |
|
|
181
|
+
| `make format` | Black + ruff --fix |
|
|
182
|
+
| `make test` | Pytest |
|
|
183
|
+
| `make coverage` | Pytest with coverage (≥75%) |
|
|
184
|
+
| `make check` | Lint + test (CI gate) |
|
|
185
|
+
|
|
186
|
+
### Versioning
|
|
187
|
+
|
|
188
|
+
Use **semantic versioning** (e.g. `0.1.0`, `1.0.0`). Tag releases so consumers can pin:
|
|
189
|
+
|
|
190
|
+
- `algotrading-core>=0.1.0,<1.0.0` in research/backend `pyproject.toml`.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Phases related to algotrading-core
|
|
195
|
+
|
|
196
|
+
The following phases from [source/DEVELOPMENT_PHASES_GUIDE.md](../source/DEVELOPMENT_PHASES_GUIDE.md) are implemented in this repository. Full guide: discovery tips, code examples, and phase-by-phase implementation.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### Phase 1: Foundation & Core Package Setup
|
|
201
|
+
|
|
202
|
+
**Goal**: Establish shared core package as its own repository and project infrastructure.
|
|
203
|
+
|
|
204
|
+
**Duration**: 1–2 weeks.
|
|
205
|
+
|
|
206
|
+
**Tasks**:
|
|
207
|
+
1. **Create core repository (`algotrading-core`)**
|
|
208
|
+
- Dedicated repo with installable package structure: `src/algotrading_core/` (src layout)
|
|
209
|
+
- Set up schemas, preprocessing, features, utils
|
|
210
|
+
- Define Pydantic schemas for all data types
|
|
211
|
+
- Create base classes and interfaces
|
|
212
|
+
- Configure `pyproject.toml` (package name, version, dependencies: pandas, numpy, pydantic, etc.)
|
|
213
|
+
|
|
214
|
+
2. **Publish core package**
|
|
215
|
+
- Publish to private PyPI (see [Publishing to private PyPI](#publishing-to-private-pypi)), or document Git URL for pip install
|
|
216
|
+
- Use semantic versioning (e.g. `1.0.0`) for releases
|
|
217
|
+
|
|
218
|
+
3. **Set up consumer repositories**
|
|
219
|
+
- In `algotrading-research` and `algotrading-backend`: add dependency on `algotrading-core` in `pyproject.toml` (version range or Git URL)
|
|
220
|
+
- Configure development tools (black, ruff, pytest) in each repo
|
|
221
|
+
|
|
222
|
+
4. **Implement core utilities** (in this repo)
|
|
223
|
+
- Date/time utilities
|
|
224
|
+
- Path utilities
|
|
225
|
+
- Logging setup
|
|
226
|
+
- Configuration loaders
|
|
227
|
+
|
|
228
|
+
5. **Create data schemas** (in this repo)
|
|
229
|
+
- `Candle` schema (OHLCV data)
|
|
230
|
+
- `Feature` schema
|
|
231
|
+
- `Signal` schema
|
|
232
|
+
- `Config` schemas
|
|
233
|
+
|
|
234
|
+
**Deliverables**:
|
|
235
|
+
- ✅ `algotrading-core` package with schemas and utilities, published (private index or Git)
|
|
236
|
+
- ✅ Research and backend depend on `algotrading-core` via pip; no copied core code
|
|
237
|
+
- ✅ Working dependency management in all repos
|
|
238
|
+
- ✅ Logging infrastructure
|
|
239
|
+
- ✅ Configuration system
|
|
240
|
+
|
|
241
|
+
**Files to create** (Phase 1):
|
|
242
|
+
```
|
|
243
|
+
algotrading-core/
|
|
244
|
+
├── src/algotrading_core/
|
|
245
|
+
│ ├── schemas/
|
|
246
|
+
│ │ ├── candle.py
|
|
247
|
+
│ │ ├── feature.py
|
|
248
|
+
│ │ ├── model.py
|
|
249
|
+
│ │ └── config.py
|
|
250
|
+
│ └── utils/
|
|
251
|
+
│ ├── datetime_utils.py
|
|
252
|
+
│ ├── path_utils.py
|
|
253
|
+
│ └── config_loader.py
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
### Phase 2: Data Layer & Preprocessing
|
|
259
|
+
|
|
260
|
+
**Goal**: Implement data ingestion, validation, and preprocessing.
|
|
261
|
+
|
|
262
|
+
**Duration**: 2–3 weeks.
|
|
263
|
+
|
|
264
|
+
**Tasks**:
|
|
265
|
+
1. **Implement preprocessing functions**
|
|
266
|
+
- Candle preprocessing (validation, cleaning)
|
|
267
|
+
- Future contract adjustments
|
|
268
|
+
- Data aggregation logic
|
|
269
|
+
- Data transformation pipelines
|
|
270
|
+
|
|
271
|
+
2. **Create data validation layer**
|
|
272
|
+
- Schema validation
|
|
273
|
+
- Data quality checks
|
|
274
|
+
- Missing data handling
|
|
275
|
+
|
|
276
|
+
3. **Implement feature engineering base**
|
|
277
|
+
- Base feature generator class
|
|
278
|
+
- Support/resistance level calculation
|
|
279
|
+
- Time-based features (trigonometric encoding)
|
|
280
|
+
- Volatility features
|
|
281
|
+
- Aggregated timeframe features
|
|
282
|
+
|
|
283
|
+
**Deliverables**:
|
|
284
|
+
- ✅ Preprocessing functions
|
|
285
|
+
- ✅ Data validation
|
|
286
|
+
- ✅ Feature generation functions
|
|
287
|
+
- ✅ Unit tests for all functions
|
|
288
|
+
|
|
289
|
+
**Files to create** (Phase 2, under `src/algotrading_core/`):
|
|
290
|
+
```
|
|
291
|
+
src/algotrading_core/
|
|
292
|
+
├── preprocessing/
|
|
293
|
+
│ ├── candles.py
|
|
294
|
+
│ ├── adjustments.py
|
|
295
|
+
│ └── transformers.py
|
|
296
|
+
└── features/
|
|
297
|
+
├── base.py
|
|
298
|
+
├── levels.py
|
|
299
|
+
├── time_features.py
|
|
300
|
+
├── volatility.py
|
|
301
|
+
└── aggregated.py
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
For **discovering what to put in core** (extract from existing app vs start minimal), code examples, and the rest of the platform phases, see [source/DEVELOPMENT_PHASES_GUIDE.md](../source/DEVELOPMENT_PHASES_GUIDE.md).
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## References
|
|
311
|
+
|
|
312
|
+
- [CODE_STRUCTURE_GUIDE.md](../CODE_STRUCTURE_GUIDE.md) — Layered architecture, Core Layer, phases.
|
|
313
|
+
- [source/DEVELOPMENT_PHASES_GUIDE.md](../source/DEVELOPMENT_PHASES_GUIDE.md) — Platform repos, Phase 1–2 tasks, repository layout.
|
|
314
|
+
- [.cursor/rules/algotrading-standards.mdc](.cursor/rules/algotrading-standards.mdc) — Code style, SOLID, testing (when opened in Cursor).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
algotrading_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
algotrading_core/features/__init__.py,sha256=LHVcRkMPA-nLM-e_g7wOnkL0X3tKxIfhcIgd_9UBTQg,167
|
|
3
|
+
algotrading_core/features/aggregated.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
algotrading_core/features/base.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
algotrading_core/features/levels.py,sha256=IzkybIgr_6k_-XX9t9fZTX5l-tzHWYlUgz4L61aY0mU,10836
|
|
6
|
+
algotrading_core/features/time_features.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
algotrading_core/features/volatility.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
algotrading_core/preprocessing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
algotrading_core/preprocessing/adjustments.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
algotrading_core/preprocessing/candles.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
algotrading_core/preprocessing/transformers.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
algotrading_core/schemas/__init__.py,sha256=6V1bBa1fdZ0tTzEzbK6t6iZpAOkxXIzrMS2VgTOEhh4,124
|
|
13
|
+
algotrading_core/schemas/candle.py,sha256=WFPeeaEGqHXhOdbelV-dAgUVn2EZ8ajVDnCdWDmrFlA,931
|
|
14
|
+
algotrading_core/schemas/feature.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
algotrading_core/schemas/model.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
algotrading_core/utils/__init__.py,sha256=I_peUFtfaVz0D46Y0PIY9Fp6WZYyDLrJrZcWtMHZCao,350
|
|
17
|
+
algotrading_core/utils/datetime_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
algotrading_core/utils/path_utils.py,sha256=xEz2SsIm3Rl664aEuorqJmIjqSaCm_23pB2_xRnJPmE,3196
|
|
19
|
+
algotrading_core/utils/setup_logger.py,sha256=Am7gSOjy1vALmo5MCylwDhkMqPknHrNuq_ZbWgYRgXE,2425
|
|
20
|
+
algotrading_core-0.1.0.dist-info/METADATA,sha256=TjBlyrRFtLco7Lnz5gagYJGfjSI6XbEzQcHWSZw_IuA,9912
|
|
21
|
+
algotrading_core-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
22
|
+
algotrading_core-0.1.0.dist-info/RECORD,,
|