chronopype 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.
- chronopype-0.1.0/LICENSE +21 -0
- chronopype-0.1.0/PKG-INFO +103 -0
- chronopype-0.1.0/README.md +89 -0
- chronopype-0.1.0/chronopype/__init__.py +0 -0
- chronopype-0.1.0/chronopype/clocks/__init__.py +32 -0
- chronopype-0.1.0/chronopype/clocks/backtest.py +168 -0
- chronopype-0.1.0/chronopype/clocks/base.py +508 -0
- chronopype-0.1.0/chronopype/clocks/modes.py +6 -0
- chronopype-0.1.0/chronopype/clocks/realtime.py +170 -0
- chronopype-0.1.0/chronopype/exceptions.py +22 -0
- chronopype-0.1.0/chronopype/models.py +203 -0
- chronopype-0.1.0/chronopype/processors/__init__.py +0 -0
- chronopype-0.1.0/chronopype/processors/base.py +51 -0
- chronopype-0.1.0/chronopype/processors/network.py +215 -0
- chronopype-0.1.0/pyproject.toml +70 -0
chronopype-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Gianluca Pagliara
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: chronopype
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A flexible clock implementation for real-time and backtesting scenarios
|
|
5
|
+
Author: Gianluca Pagliara
|
|
6
|
+
Author-email: pagliara.gianluca@gmail.com
|
|
7
|
+
Requires-Python: >=3.13,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Requires-Dist: eventspype (>=0.1.0,<0.2.0)
|
|
11
|
+
Requires-Dist: pydantic (>=2.10.4,<3.0.0)
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Chrono Pypeline
|
|
15
|
+
|
|
16
|
+
[](https://github.com/gianlucapagliara/chronopype/actions/workflows/ci.yml)
|
|
17
|
+
[](https://codecov.io/gh/gianlucapagliara/chronopype)
|
|
18
|
+
[](https://www.python.org/downloads/)
|
|
19
|
+
|
|
20
|
+
A flexible clock implementation for real-time and backtesting scenarios in Python. chronopype provides a robust framework for managing time-based operations with support for both real-time processing and historical data backtesting.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- ๐ **Flexible Clock System**: Support for both real-time and backtesting modes
|
|
25
|
+
- ๐ **Processor Framework**: Extensible system for implementing time-based operations
|
|
26
|
+
- ๐ **Network-Aware**: Built-in network processor with retry and backoff capabilities
|
|
27
|
+
- โก **Async Support**: Full async/await support for efficient I/O operations
|
|
28
|
+
- ๐ ๏ธ **Easy to Use**: Simple API for managing time-based operations
|
|
29
|
+
- ๐ **Performance Monitoring**: Built-in performance tracking and statistics
|
|
30
|
+
- ๐ **Type Safe**: Fully typed with MyPy strict mode
|
|
31
|
+
- ๐งช **Well Tested**: Comprehensive test suite with high coverage
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Using pip
|
|
37
|
+
pip install chronopype
|
|
38
|
+
|
|
39
|
+
# Using poetry
|
|
40
|
+
poetry add chronopype
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
Here's a simple example of using chronopype:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import asyncio
|
|
49
|
+
from chronopype import ClockConfig
|
|
50
|
+
from chronopype.clocks import RealtimeClock
|
|
51
|
+
from chronopype.processors import TickProcessor
|
|
52
|
+
|
|
53
|
+
class MyProcessor(TickProcessor):
|
|
54
|
+
async def async_tick(self, timestamp: float) -> None:
|
|
55
|
+
print(f"Processing at {timestamp}")
|
|
56
|
+
|
|
57
|
+
async def main():
|
|
58
|
+
# Configure the clock
|
|
59
|
+
config = ClockConfig(
|
|
60
|
+
start_time=time.time(),
|
|
61
|
+
tick_size=1.0 # 1 second ticks
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Create and configure the clock
|
|
65
|
+
async with RealtimeClock(config) as clock:
|
|
66
|
+
# Add your processor
|
|
67
|
+
clock.add_processor(MyProcessor())
|
|
68
|
+
|
|
69
|
+
# Run for 10 seconds
|
|
70
|
+
await clock.run_til(config.start_time + 10)
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Core Components
|
|
77
|
+
|
|
78
|
+
- **Clocks**: Base implementations for time management
|
|
79
|
+
- `RealtimeClock`: For real-time processing
|
|
80
|
+
- `BacktestClock`: For historical data processing
|
|
81
|
+
|
|
82
|
+
- **Processors**: Framework for implementing time-based operations
|
|
83
|
+
- `TickProcessor`: Base class for all processors
|
|
84
|
+
- `NetworkProcessor`: Network-aware processor with retry capabilities
|
|
85
|
+
|
|
86
|
+
## Development
|
|
87
|
+
|
|
88
|
+
chronopype uses Poetry for dependency management and packaging:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Install dependencies
|
|
92
|
+
poetry install
|
|
93
|
+
|
|
94
|
+
# Run tests
|
|
95
|
+
poetry run pytest
|
|
96
|
+
|
|
97
|
+
# Run type checks
|
|
98
|
+
poetry run mypy .
|
|
99
|
+
|
|
100
|
+
# Run linting
|
|
101
|
+
poetry run pre-commit run --all-files
|
|
102
|
+
```
|
|
103
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Chrono Pypeline
|
|
2
|
+
|
|
3
|
+
[](https://github.com/gianlucapagliara/chronopype/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/gianlucapagliara/chronopype)
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
|
|
7
|
+
A flexible clock implementation for real-time and backtesting scenarios in Python. chronopype provides a robust framework for managing time-based operations with support for both real-time processing and historical data backtesting.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- ๐ **Flexible Clock System**: Support for both real-time and backtesting modes
|
|
12
|
+
- ๐ **Processor Framework**: Extensible system for implementing time-based operations
|
|
13
|
+
- ๐ **Network-Aware**: Built-in network processor with retry and backoff capabilities
|
|
14
|
+
- โก **Async Support**: Full async/await support for efficient I/O operations
|
|
15
|
+
- ๐ ๏ธ **Easy to Use**: Simple API for managing time-based operations
|
|
16
|
+
- ๐ **Performance Monitoring**: Built-in performance tracking and statistics
|
|
17
|
+
- ๐ **Type Safe**: Fully typed with MyPy strict mode
|
|
18
|
+
- ๐งช **Well Tested**: Comprehensive test suite with high coverage
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Using pip
|
|
24
|
+
pip install chronopype
|
|
25
|
+
|
|
26
|
+
# Using poetry
|
|
27
|
+
poetry add chronopype
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
Here's a simple example of using chronopype:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import asyncio
|
|
36
|
+
from chronopype import ClockConfig
|
|
37
|
+
from chronopype.clocks import RealtimeClock
|
|
38
|
+
from chronopype.processors import TickProcessor
|
|
39
|
+
|
|
40
|
+
class MyProcessor(TickProcessor):
|
|
41
|
+
async def async_tick(self, timestamp: float) -> None:
|
|
42
|
+
print(f"Processing at {timestamp}")
|
|
43
|
+
|
|
44
|
+
async def main():
|
|
45
|
+
# Configure the clock
|
|
46
|
+
config = ClockConfig(
|
|
47
|
+
start_time=time.time(),
|
|
48
|
+
tick_size=1.0 # 1 second ticks
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Create and configure the clock
|
|
52
|
+
async with RealtimeClock(config) as clock:
|
|
53
|
+
# Add your processor
|
|
54
|
+
clock.add_processor(MyProcessor())
|
|
55
|
+
|
|
56
|
+
# Run for 10 seconds
|
|
57
|
+
await clock.run_til(config.start_time + 10)
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
asyncio.run(main())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Core Components
|
|
64
|
+
|
|
65
|
+
- **Clocks**: Base implementations for time management
|
|
66
|
+
- `RealtimeClock`: For real-time processing
|
|
67
|
+
- `BacktestClock`: For historical data processing
|
|
68
|
+
|
|
69
|
+
- **Processors**: Framework for implementing time-based operations
|
|
70
|
+
- `TickProcessor`: Base class for all processors
|
|
71
|
+
- `NetworkProcessor`: Network-aware processor with retry capabilities
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
chronopype uses Poetry for dependency management and packaging:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Install dependencies
|
|
79
|
+
poetry install
|
|
80
|
+
|
|
81
|
+
# Run tests
|
|
82
|
+
poetry run pytest
|
|
83
|
+
|
|
84
|
+
# Run type checks
|
|
85
|
+
poetry run mypy .
|
|
86
|
+
|
|
87
|
+
# Run linting
|
|
88
|
+
poetry run pre-commit run --all-files
|
|
89
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Clock implementations package."""
|
|
2
|
+
|
|
3
|
+
from .backtest import BacktestClock
|
|
4
|
+
from .base import BaseClock
|
|
5
|
+
from .modes import ClockMode
|
|
6
|
+
from .realtime import RealtimeClock
|
|
7
|
+
|
|
8
|
+
# Registry mapping clock modes to their implementations
|
|
9
|
+
CLOCK_REGISTRY: dict[ClockMode, type[BaseClock]] = {
|
|
10
|
+
ClockMode.BACKTEST: BacktestClock,
|
|
11
|
+
ClockMode.REALTIME: RealtimeClock,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_clock_class(mode: ClockMode) -> type[BaseClock]:
|
|
16
|
+
"""Get the clock class for a given mode.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
mode: The clock mode to get the implementation for.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
The clock class for the given mode.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: If no implementation exists for the given mode.
|
|
26
|
+
"""
|
|
27
|
+
if mode not in CLOCK_REGISTRY:
|
|
28
|
+
raise ValueError(f"No clock implementation found for mode: {mode}")
|
|
29
|
+
return CLOCK_REGISTRY[mode]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = ["BaseClock", "BacktestClock", "RealtimeClock", "ClockMode"]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
|
|
4
|
+
from ..exceptions import ClockError
|
|
5
|
+
from ..models import ClockConfig
|
|
6
|
+
from ..processors.base import TickProcessor
|
|
7
|
+
from .base import BaseClock
|
|
8
|
+
from .modes import ClockMode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BacktestClock(BaseClock):
|
|
12
|
+
"""Clock implementation for backtesting mode."""
|
|
13
|
+
|
|
14
|
+
start_publication = BaseClock.start_publication
|
|
15
|
+
tick_publication = BaseClock.tick_publication
|
|
16
|
+
stop_publication = BaseClock.stop_publication
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
config: ClockConfig,
|
|
21
|
+
error_callback: Callable[[TickProcessor, Exception], None] | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Initialize a new BacktestClock instance."""
|
|
24
|
+
if config.clock_mode != ClockMode.BACKTEST:
|
|
25
|
+
raise ClockError("BacktestClock requires BACKTEST mode")
|
|
26
|
+
if config.end_time <= 0:
|
|
27
|
+
raise ClockError("end_time must be set for backtest mode")
|
|
28
|
+
super().__init__(config, error_callback)
|
|
29
|
+
|
|
30
|
+
def start(self) -> None:
|
|
31
|
+
"""Start the clock."""
|
|
32
|
+
if not self._current_context:
|
|
33
|
+
raise ClockError("Clock must be started in a context")
|
|
34
|
+
self._started = True
|
|
35
|
+
|
|
36
|
+
def stop(self) -> None:
|
|
37
|
+
"""Stop the clock."""
|
|
38
|
+
self._started = False
|
|
39
|
+
self._running = False
|
|
40
|
+
|
|
41
|
+
def tick(self) -> None:
|
|
42
|
+
"""Process a clock tick."""
|
|
43
|
+
if not self._started:
|
|
44
|
+
raise ClockError("Clock not started")
|
|
45
|
+
self._tick_counter += 1
|
|
46
|
+
self._current_tick += self._config.tick_size
|
|
47
|
+
|
|
48
|
+
async def run(self) -> None:
|
|
49
|
+
"""Run the clock until end_time."""
|
|
50
|
+
await self.run_til(self._config.end_time)
|
|
51
|
+
|
|
52
|
+
async def run_til(self, target_time: float) -> None:
|
|
53
|
+
"""Run the clock until the target time."""
|
|
54
|
+
if self._task is not None:
|
|
55
|
+
raise ClockError("Clock is already running")
|
|
56
|
+
|
|
57
|
+
if not self._current_context:
|
|
58
|
+
raise ClockError("Clock must be started in a context")
|
|
59
|
+
|
|
60
|
+
if target_time > self._config.end_time:
|
|
61
|
+
raise ClockError("Cannot run past end_time in backtest mode")
|
|
62
|
+
|
|
63
|
+
self._running = True
|
|
64
|
+
self._started = True
|
|
65
|
+
self._task = asyncio.create_task(
|
|
66
|
+
self._run_til_impl(target_time, self._current_context)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Activate all processors
|
|
70
|
+
for processor in self._current_context:
|
|
71
|
+
state = self._processor_states[processor]
|
|
72
|
+
self._processor_states[processor] = state.model_copy(
|
|
73
|
+
update={"is_active": True}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
await self._task
|
|
78
|
+
finally:
|
|
79
|
+
self._task = None
|
|
80
|
+
|
|
81
|
+
async def _run_til_impl(
|
|
82
|
+
self, target_time: float, processors: list[TickProcessor]
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Run the clock until a specific timestamp."""
|
|
85
|
+
if not self._running:
|
|
86
|
+
raise ClockError("Clock must be started in a context.")
|
|
87
|
+
|
|
88
|
+
processors = [p for p in processors if self._processor_states[p].is_active]
|
|
89
|
+
if not processors:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Calculate number of ticks needed
|
|
93
|
+
num_ticks = int((target_time - self._current_tick) / self._config.tick_size)
|
|
94
|
+
if num_ticks <= 0:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# Execute ticks
|
|
98
|
+
for _ in range(num_ticks):
|
|
99
|
+
self._current_tick += self._config.tick_size
|
|
100
|
+
await self._execute_tick(processors)
|
|
101
|
+
self._tick_counter += 1
|
|
102
|
+
|
|
103
|
+
# Set final timestamp to exactly match target_time
|
|
104
|
+
if (
|
|
105
|
+
abs(self._current_tick - target_time) > 1e-10
|
|
106
|
+
): # Handle floating point precision
|
|
107
|
+
self._current_tick = target_time
|
|
108
|
+
await self._execute_tick(processors)
|
|
109
|
+
self._tick_counter += 1
|
|
110
|
+
|
|
111
|
+
async def _execute_tick(self, processors: list[TickProcessor]) -> None:
|
|
112
|
+
"""Execute a tick for all processors."""
|
|
113
|
+
if self._config.concurrent_processors:
|
|
114
|
+
# Execute processors concurrently
|
|
115
|
+
tasks = []
|
|
116
|
+
for processor in processors:
|
|
117
|
+
task = asyncio.create_task(
|
|
118
|
+
self._execute_processor(processor, self._current_tick)
|
|
119
|
+
)
|
|
120
|
+
tasks.append(task)
|
|
121
|
+
|
|
122
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
123
|
+
errors = []
|
|
124
|
+
|
|
125
|
+
# Update states for all processors first
|
|
126
|
+
for processor, result in zip(processors, results, strict=False):
|
|
127
|
+
if isinstance(result, Exception):
|
|
128
|
+
if self._error_callback:
|
|
129
|
+
self._error_callback(processor, result)
|
|
130
|
+
errors.append(result)
|
|
131
|
+
else:
|
|
132
|
+
# Update processor state after successful execution
|
|
133
|
+
state = self._processor_states[processor]
|
|
134
|
+
self._processor_states[processor] = state.model_copy(
|
|
135
|
+
update={"last_timestamp": self._current_tick}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Raise the first error if any occurred
|
|
139
|
+
if errors:
|
|
140
|
+
raise errors[0]
|
|
141
|
+
else:
|
|
142
|
+
# Execute processors sequentially
|
|
143
|
+
for processor in processors:
|
|
144
|
+
try:
|
|
145
|
+
await self._execute_processor(processor, self._current_tick)
|
|
146
|
+
# Update processor state after successful execution
|
|
147
|
+
state = self._processor_states[processor]
|
|
148
|
+
self._processor_states[processor] = state.model_copy(
|
|
149
|
+
update={"last_timestamp": self._current_tick}
|
|
150
|
+
)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
if self._error_callback:
|
|
153
|
+
self._error_callback(processor, e)
|
|
154
|
+
raise e
|
|
155
|
+
|
|
156
|
+
async def fast_forward(self, seconds: float) -> None:
|
|
157
|
+
"""Fast forward the clock by a specified number of seconds."""
|
|
158
|
+
if not self._current_context:
|
|
159
|
+
raise ClockError("Fast forward can only be used within a context")
|
|
160
|
+
|
|
161
|
+
if seconds <= 0:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
target_time = self._current_tick + seconds
|
|
165
|
+
if target_time > self._config.end_time:
|
|
166
|
+
raise ClockError("Cannot fast forward past end_time in backtest mode")
|
|
167
|
+
|
|
168
|
+
await self.run_til(target_time)
|