algotrading-core 0.1.0__tar.gz

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.
Files changed (23) hide show
  1. algotrading_core-0.1.0/.gitignore +61 -0
  2. algotrading_core-0.1.0/PKG-INFO +314 -0
  3. algotrading_core-0.1.0/README.md +303 -0
  4. algotrading_core-0.1.0/pyproject.toml +48 -0
  5. algotrading_core-0.1.0/src/algotrading_core/__init__.py +0 -0
  6. algotrading_core-0.1.0/src/algotrading_core/features/__init__.py +7 -0
  7. algotrading_core-0.1.0/src/algotrading_core/features/aggregated.py +0 -0
  8. algotrading_core-0.1.0/src/algotrading_core/features/base.py +0 -0
  9. algotrading_core-0.1.0/src/algotrading_core/features/levels.py +297 -0
  10. algotrading_core-0.1.0/src/algotrading_core/features/time_features.py +0 -0
  11. algotrading_core-0.1.0/src/algotrading_core/features/volatility.py +0 -0
  12. algotrading_core-0.1.0/src/algotrading_core/preprocessing/__init__.py +0 -0
  13. algotrading_core-0.1.0/src/algotrading_core/preprocessing/adjustments.py +0 -0
  14. algotrading_core-0.1.0/src/algotrading_core/preprocessing/candles.py +0 -0
  15. algotrading_core-0.1.0/src/algotrading_core/preprocessing/transformers.py +0 -0
  16. algotrading_core-0.1.0/src/algotrading_core/schemas/__init__.py +7 -0
  17. algotrading_core-0.1.0/src/algotrading_core/schemas/candle.py +26 -0
  18. algotrading_core-0.1.0/src/algotrading_core/schemas/feature.py +0 -0
  19. algotrading_core-0.1.0/src/algotrading_core/schemas/model.py +0 -0
  20. algotrading_core-0.1.0/src/algotrading_core/utils/__init__.py +15 -0
  21. algotrading_core-0.1.0/src/algotrading_core/utils/datetime_utils.py +0 -0
  22. algotrading_core-0.1.0/src/algotrading_core/utils/path_utils.py +97 -0
  23. algotrading_core-0.1.0/src/algotrading_core/utils/setup_logger.py +83 -0
@@ -0,0 +1,61 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ .venv/
25
+ venv/
26
+ ENV/
27
+ env/
28
+
29
+ # uv
30
+ .uv/
31
+ uv.lock
32
+
33
+ # IDE
34
+ .idea/
35
+ .vscode/
36
+ *.swp
37
+ *.swo
38
+ *~
39
+
40
+ # Cursor
41
+ .cursor/
42
+
43
+ # Testing / coverage
44
+ .coverage
45
+ .pytest_cache/
46
+ htmlcov/
47
+ .tox/
48
+ .nox/
49
+
50
+ # Environment
51
+ .env
52
+ .env.local
53
+ *.local
54
+
55
+ # OS
56
+ .DS_Store
57
+ Thumbs.db
58
+
59
+ # Logs
60
+ *.log
61
+ logs/
@@ -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,303 @@
1
+ # algotrading-core
2
+
3
+ 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.
4
+
5
+ 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).
6
+
7
+ ---
8
+
9
+ ## Role in the platform
10
+
11
+ The platform uses a **multi-repository** layout:
12
+
13
+ | Repository | Role | Depends on core |
14
+ |-------------------------|------------------------------|------------------|
15
+ | **algotrading-core** | Schemas, preprocessing, features, utils | — |
16
+ | **algotrading-research**| Offline research, training, backtesting | ✅ |
17
+ | **algotrading-backend** | Signals, inference, API | ✅ |
18
+ | **algotrading-execution** | MT5 execution (signals only) | ❌ |
19
+
20
+ 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.
21
+
22
+ ---
23
+
24
+ ## Package structure
25
+
26
+ ```
27
+ src/algotrading_core/
28
+ ├── schemas/ # Pydantic data models (Phase 1)
29
+ │ ├── candle.py # OHLCV candle schema
30
+ │ ├── feature.py # Feature schema
31
+ │ ├── model.py # Model artifact schema
32
+ │ └── (config) # Configuration schemas
33
+ ├── preprocessing/ # Data validation & transformation (Phase 2)
34
+ │ ├── candles.py # Candle validation, cleaning
35
+ │ ├── adjustments.py # Future contract adjustments
36
+ │ └── transformers.py # Aggregation, pipelines
37
+ ├── features/ # Feature engineering (Phase 2)
38
+ │ ├── base.py # Base feature generator (abstract)
39
+ │ ├── levels.py # Support/resistance levels
40
+ │ ├── time_features.py # Time-based (e.g. trig encoding)
41
+ │ ├── volatility.py # Volatility features
42
+ │ └── aggregated.py # Aggregated timeframe features
43
+ └── utils/ # Shared utilities (Phase 1)
44
+ ├── datetime_utils.py
45
+ └── path_utils.py
46
+ ```
47
+
48
+ - **Phase 1** (Foundation): schemas + utils + config loaders.
49
+ - **Phase 2** (Data & preprocessing): preprocessing + feature base and concrete generators.
50
+
51
+ ---
52
+
53
+ ## Installation
54
+
55
+ **Requires**: Python ≥3.11.
56
+
57
+ ### From local path (development)
58
+
59
+ ```bash
60
+ uv add /path/to/algotrading-core
61
+ # or
62
+ pip install -e /path/to/algotrading-core
63
+ ```
64
+
65
+ ### From Git
66
+
67
+ ```bash
68
+ uv add "algotrading-core @ git+https://github.com/org/algotrading-core.git@v0.1.0"
69
+ ```
70
+
71
+ ### From private PyPI
72
+
73
+ Configure your private index (see [Publishing to private PyPI](#publishing-to-private-pypi) for index URL and auth), then:
74
+
75
+ ```bash
76
+ pip install algotrading-core==0.1.0
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Publishing to private PyPI
82
+
83
+ The package uses **semantic versioning** (e.g. `1.0.0`). To publish a release to a private PyPI server:
84
+
85
+ ### 1. Bump version
86
+
87
+ Edit `version` in `pyproject.toml` (e.g. `0.1.0` → `0.2.0`). Tag the release in Git:
88
+
89
+ ```bash
90
+ git tag v0.2.0
91
+ ```
92
+
93
+ ### 2. Build the package
94
+
95
+ ```bash
96
+ make build
97
+ # or: uv run python -m build
98
+ ```
99
+
100
+ This produces `dist/algotrading-core-<version>.tar.gz` and `dist/algotrading_core-<version>-py3-none-any.whl`.
101
+
102
+ ### 3. Configure credentials for your private index
103
+
104
+ **Option A — `.pypirc` (recommended)**
105
+ Create or edit `~/.pypirc`:
106
+
107
+ ```ini
108
+ [distutils]
109
+ index-servers =
110
+ private
111
+
112
+ [private]
113
+ repository = https://your-private-pypi.example.com/pypi/
114
+ username = your-username
115
+ password = your-password-or-token
116
+ ```
117
+
118
+ **Option B — Environment variables**
119
+
120
+ ```bash
121
+ export TWINE_USERNAME=your-username
122
+ export TWINE_PASSWORD=your-password-or-token
123
+ export TWINE_REPOSITORY_URL=https://your-private-pypi.example.com/pypi/
124
+ ```
125
+
126
+ ### 4. Upload
127
+
128
+ ```bash
129
+ make publish
130
+ # Uses .pypirc repo name "private" by default. Override: make publish REPO=myrepo
131
+ # Or use URL directly: make publish REPO_URL=https://your-private-pypi.example.com/pypi/
132
+ ```
133
+
134
+ Replace `your-private-pypi.example.com` with your actual private PyPI host (e.g. CodeArtifact, Artifactory, or self-hosted PyPI).
135
+
136
+ **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`).
137
+
138
+ ---
139
+
140
+ ## Usage
141
+
142
+ Consumers import from the package; no copy-paste of core code.
143
+
144
+ ```python
145
+ from algotrading_core.schemas import Candle, Feature
146
+ from algotrading_core.preprocessing.candles import preprocess_candles
147
+ from algotrading_core.features.base import FeatureGenerator
148
+ from algotrading_core.features.levels import LevelsFeatureGenerator
149
+ from algotrading_core.utils.datetime_utils import parse_interval
150
+ ```
151
+
152
+ Research uses these for **training and backtesting**; the backend uses the **same** code for **live feature generation** and inference.
153
+
154
+ ---
155
+
156
+ ## Development
157
+
158
+ ### Setup
159
+
160
+ ```bash
161
+ cd algotrading-core
162
+ uv sync
163
+ ```
164
+
165
+ ### Commands (Makefile)
166
+
167
+ | Target | Action |
168
+ |----------|----------------------------------|
169
+ | `make lint` | Ruff + black check |
170
+ | `make format` | Black + ruff --fix |
171
+ | `make test` | Pytest |
172
+ | `make coverage` | Pytest with coverage (≥75%) |
173
+ | `make check` | Lint + test (CI gate) |
174
+
175
+ ### Versioning
176
+
177
+ Use **semantic versioning** (e.g. `0.1.0`, `1.0.0`). Tag releases so consumers can pin:
178
+
179
+ - `algotrading-core>=0.1.0,<1.0.0` in research/backend `pyproject.toml`.
180
+
181
+ ---
182
+
183
+ ## Phases related to algotrading-core
184
+
185
+ 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.
186
+
187
+ ---
188
+
189
+ ### Phase 1: Foundation & Core Package Setup
190
+
191
+ **Goal**: Establish shared core package as its own repository and project infrastructure.
192
+
193
+ **Duration**: 1–2 weeks.
194
+
195
+ **Tasks**:
196
+ 1. **Create core repository (`algotrading-core`)**
197
+ - Dedicated repo with installable package structure: `src/algotrading_core/` (src layout)
198
+ - Set up schemas, preprocessing, features, utils
199
+ - Define Pydantic schemas for all data types
200
+ - Create base classes and interfaces
201
+ - Configure `pyproject.toml` (package name, version, dependencies: pandas, numpy, pydantic, etc.)
202
+
203
+ 2. **Publish core package**
204
+ - Publish to private PyPI (see [Publishing to private PyPI](#publishing-to-private-pypi)), or document Git URL for pip install
205
+ - Use semantic versioning (e.g. `1.0.0`) for releases
206
+
207
+ 3. **Set up consumer repositories**
208
+ - In `algotrading-research` and `algotrading-backend`: add dependency on `algotrading-core` in `pyproject.toml` (version range or Git URL)
209
+ - Configure development tools (black, ruff, pytest) in each repo
210
+
211
+ 4. **Implement core utilities** (in this repo)
212
+ - Date/time utilities
213
+ - Path utilities
214
+ - Logging setup
215
+ - Configuration loaders
216
+
217
+ 5. **Create data schemas** (in this repo)
218
+ - `Candle` schema (OHLCV data)
219
+ - `Feature` schema
220
+ - `Signal` schema
221
+ - `Config` schemas
222
+
223
+ **Deliverables**:
224
+ - ✅ `algotrading-core` package with schemas and utilities, published (private index or Git)
225
+ - ✅ Research and backend depend on `algotrading-core` via pip; no copied core code
226
+ - ✅ Working dependency management in all repos
227
+ - ✅ Logging infrastructure
228
+ - ✅ Configuration system
229
+
230
+ **Files to create** (Phase 1):
231
+ ```
232
+ algotrading-core/
233
+ ├── src/algotrading_core/
234
+ │ ├── schemas/
235
+ │ │ ├── candle.py
236
+ │ │ ├── feature.py
237
+ │ │ ├── model.py
238
+ │ │ └── config.py
239
+ │ └── utils/
240
+ │ ├── datetime_utils.py
241
+ │ ├── path_utils.py
242
+ │ └── config_loader.py
243
+ ```
244
+
245
+ ---
246
+
247
+ ### Phase 2: Data Layer & Preprocessing
248
+
249
+ **Goal**: Implement data ingestion, validation, and preprocessing.
250
+
251
+ **Duration**: 2–3 weeks.
252
+
253
+ **Tasks**:
254
+ 1. **Implement preprocessing functions**
255
+ - Candle preprocessing (validation, cleaning)
256
+ - Future contract adjustments
257
+ - Data aggregation logic
258
+ - Data transformation pipelines
259
+
260
+ 2. **Create data validation layer**
261
+ - Schema validation
262
+ - Data quality checks
263
+ - Missing data handling
264
+
265
+ 3. **Implement feature engineering base**
266
+ - Base feature generator class
267
+ - Support/resistance level calculation
268
+ - Time-based features (trigonometric encoding)
269
+ - Volatility features
270
+ - Aggregated timeframe features
271
+
272
+ **Deliverables**:
273
+ - ✅ Preprocessing functions
274
+ - ✅ Data validation
275
+ - ✅ Feature generation functions
276
+ - ✅ Unit tests for all functions
277
+
278
+ **Files to create** (Phase 2, under `src/algotrading_core/`):
279
+ ```
280
+ src/algotrading_core/
281
+ ├── preprocessing/
282
+ │ ├── candles.py
283
+ │ ├── adjustments.py
284
+ │ └── transformers.py
285
+ └── features/
286
+ ├── base.py
287
+ ├── levels.py
288
+ ├── time_features.py
289
+ ├── volatility.py
290
+ └── aggregated.py
291
+ ```
292
+
293
+ ---
294
+
295
+ 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).
296
+
297
+ ---
298
+
299
+ ## References
300
+
301
+ - [CODE_STRUCTURE_GUIDE.md](../CODE_STRUCTURE_GUIDE.md) — Layered architecture, Core Layer, phases.
302
+ - [source/DEVELOPMENT_PHASES_GUIDE.md](../source/DEVELOPMENT_PHASES_GUIDE.md) — Platform repos, Phase 1–2 tasks, repository layout.
303
+ - [.cursor/rules/algotrading-standards.mdc](.cursor/rules/algotrading-standards.mdc) — Code style, SOLID, testing (when opened in Cursor).
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "algotrading-core"
3
+ version = "0.1.0"
4
+ description = "Shared core package: schemas, preprocessing, features, and utils for the algorithmic trading platform"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11,<4"
7
+ dependencies = [
8
+ "pandas>=2.0,<3",
9
+ "numpy>=1.24,<3",
10
+ "pydantic>=2.0,<3",
11
+ "pyyaml>=6.0,<7",
12
+ ]
13
+
14
+ [build-system]
15
+ requires = ["hatchling"]
16
+ build-backend = "hatchling.build"
17
+
18
+ [tool.hatch.build.targets.wheel]
19
+ packages = ["src/algotrading_core"]
20
+
21
+ [tool.hatch.build.targets.sdist]
22
+ include = ["src/algotrading_core"]
23
+
24
+ [tool.pytest.ini_options]
25
+ minversion = "8.0"
26
+ addopts = "--strict-markers -v"
27
+ testpaths = ["tests"]
28
+
29
+ [tool.ruff]
30
+ line-length = 100
31
+ target-version = "py311"
32
+
33
+ [tool.ruff.lint]
34
+ select = ["E", "F", "I", "UP"]
35
+
36
+ [tool.black]
37
+ line-length = 100
38
+ target-version = ["py311"]
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "black>=26.1.0",
43
+ "build>=1.2.0",
44
+ "pytest>=9.0.2",
45
+ "pytest-cov>=7.0.0",
46
+ "ruff>=0.14.14",
47
+ "twine>=6.0.0",
48
+ ]
@@ -0,0 +1,7 @@
1
+ """Feature engineering for algorithmic trading."""
2
+
3
+ from algotrading_core.features.levels import generate_levels_feature
4
+
5
+ __all__ = [
6
+ "generate_levels_feature",
7
+ ]
@@ -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
@@ -0,0 +1,7 @@
1
+ """Shared schemas for algotrading-core."""
2
+
3
+ from algotrading_core.schemas.candle import Candle
4
+
5
+ __all__ = [
6
+ "Candle",
7
+ ]
@@ -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
@@ -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
+ ]
@@ -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