signalflow-trading 0.2.1__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.
- signalflow/__init__.py +21 -0
- signalflow/analytics/__init__.py +0 -0
- signalflow/core/__init__.py +46 -0
- signalflow/core/base_mixin.py +232 -0
- signalflow/core/containers/__init__.py +21 -0
- signalflow/core/containers/order.py +216 -0
- signalflow/core/containers/portfolio.py +211 -0
- signalflow/core/containers/position.py +296 -0
- signalflow/core/containers/raw_data.py +167 -0
- signalflow/core/containers/raw_data_view.py +169 -0
- signalflow/core/containers/signals.py +198 -0
- signalflow/core/containers/strategy_state.py +147 -0
- signalflow/core/containers/trade.py +112 -0
- signalflow/core/decorators.py +103 -0
- signalflow/core/enums.py +270 -0
- signalflow/core/registry.py +322 -0
- signalflow/core/rolling_aggregator.py +362 -0
- signalflow/core/signal_transforms/__init__.py +5 -0
- signalflow/core/signal_transforms/base_signal_transform.py +186 -0
- signalflow/data/__init__.py +11 -0
- signalflow/data/raw_data_factory.py +225 -0
- signalflow/data/raw_store/__init__.py +7 -0
- signalflow/data/raw_store/base.py +271 -0
- signalflow/data/raw_store/duckdb_stores.py +696 -0
- signalflow/data/source/__init__.py +10 -0
- signalflow/data/source/base.py +300 -0
- signalflow/data/source/binance.py +442 -0
- signalflow/data/strategy_store/__init__.py +8 -0
- signalflow/data/strategy_store/base.py +278 -0
- signalflow/data/strategy_store/duckdb.py +409 -0
- signalflow/data/strategy_store/schema.py +36 -0
- signalflow/detector/__init__.py +7 -0
- signalflow/detector/adapter/__init__.py +5 -0
- signalflow/detector/adapter/pandas_detector.py +46 -0
- signalflow/detector/base.py +390 -0
- signalflow/detector/sma_cross.py +105 -0
- signalflow/feature/__init__.py +16 -0
- signalflow/feature/adapter/__init__.py +5 -0
- signalflow/feature/adapter/pandas_feature_extractor.py +54 -0
- signalflow/feature/base.py +330 -0
- signalflow/feature/feature_set.py +286 -0
- signalflow/feature/oscillator/__init__.py +5 -0
- signalflow/feature/oscillator/rsi_extractor.py +42 -0
- signalflow/feature/pandasta/__init__.py +10 -0
- signalflow/feature/pandasta/pandas_ta_extractor.py +141 -0
- signalflow/feature/pandasta/top_pandasta_extractors.py +64 -0
- signalflow/feature/smoother/__init__.py +5 -0
- signalflow/feature/smoother/sma_extractor.py +46 -0
- signalflow/strategy/__init__.py +9 -0
- signalflow/strategy/broker/__init__.py +15 -0
- signalflow/strategy/broker/backtest.py +172 -0
- signalflow/strategy/broker/base.py +186 -0
- signalflow/strategy/broker/executor/__init__.py +9 -0
- signalflow/strategy/broker/executor/base.py +35 -0
- signalflow/strategy/broker/executor/binance_spot.py +12 -0
- signalflow/strategy/broker/executor/virtual_spot.py +81 -0
- signalflow/strategy/broker/realtime_spot.py +12 -0
- signalflow/strategy/component/__init__.py +9 -0
- signalflow/strategy/component/base.py +65 -0
- signalflow/strategy/component/entry/__init__.py +7 -0
- signalflow/strategy/component/entry/fixed_size.py +57 -0
- signalflow/strategy/component/entry/signal.py +127 -0
- signalflow/strategy/component/exit/__init__.py +5 -0
- signalflow/strategy/component/exit/time_based.py +47 -0
- signalflow/strategy/component/exit/tp_sl.py +80 -0
- signalflow/strategy/component/metric/__init__.py +8 -0
- signalflow/strategy/component/metric/main_metrics.py +181 -0
- signalflow/strategy/runner/__init__.py +8 -0
- signalflow/strategy/runner/backtest_runner.py +208 -0
- signalflow/strategy/runner/base.py +19 -0
- signalflow/strategy/runner/optimized_backtest_runner.py +178 -0
- signalflow/strategy/runner/realtime_runner.py +0 -0
- signalflow/target/__init__.py +14 -0
- signalflow/target/adapter/__init__.py +5 -0
- signalflow/target/adapter/pandas_labeler.py +45 -0
- signalflow/target/base.py +409 -0
- signalflow/target/fixed_horizon_labeler.py +93 -0
- signalflow/target/static_triple_barrier.py +162 -0
- signalflow/target/triple_barrier.py +188 -0
- signalflow/utils/__init__.py +7 -0
- signalflow/utils/import_utils.py +11 -0
- signalflow/utils/tune_utils.py +19 -0
- signalflow/validator/__init__.py +6 -0
- signalflow/validator/base.py +139 -0
- signalflow/validator/sklearn_validator.py +527 -0
- signalflow_trading-0.2.1.dist-info/METADATA +149 -0
- signalflow_trading-0.2.1.dist-info/RECORD +90 -0
- signalflow_trading-0.2.1.dist-info/WHEEL +5 -0
- signalflow_trading-0.2.1.dist-info/licenses/LICENSE +21 -0
- signalflow_trading-0.2.1.dist-info/top_level.txt +1 -0
signalflow/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import signalflow.core as core
|
|
2
|
+
import signalflow.data as data
|
|
3
|
+
import signalflow.detector as detector
|
|
4
|
+
import signalflow.feature as feature
|
|
5
|
+
import signalflow.target as target
|
|
6
|
+
import signalflow.strategy as strategy
|
|
7
|
+
import signalflow.utils as utils
|
|
8
|
+
import signalflow.validator as validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"core",
|
|
14
|
+
"data",
|
|
15
|
+
"detector",
|
|
16
|
+
"feature",
|
|
17
|
+
"target",
|
|
18
|
+
"strategy"
|
|
19
|
+
"utils",
|
|
20
|
+
"validator",
|
|
21
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from signalflow.core.containers import (
|
|
2
|
+
RawData,
|
|
3
|
+
Signals,
|
|
4
|
+
RawDataView,
|
|
5
|
+
Position,
|
|
6
|
+
Trade,
|
|
7
|
+
Portfolio,
|
|
8
|
+
StrategyState,
|
|
9
|
+
Order,
|
|
10
|
+
OrderFill
|
|
11
|
+
)
|
|
12
|
+
from signalflow.core.enums import (
|
|
13
|
+
SignalType,
|
|
14
|
+
PositionType,
|
|
15
|
+
SfComponentType,
|
|
16
|
+
DataFrameType,
|
|
17
|
+
RawDataType
|
|
18
|
+
)
|
|
19
|
+
from signalflow.core.decorators import sf_component
|
|
20
|
+
from signalflow.core.registry import default_registry, SignalFlowRegistry
|
|
21
|
+
from signalflow.core.signal_transforms import SignalsTransform
|
|
22
|
+
from signalflow.core.rolling_aggregator import RollingAggregator
|
|
23
|
+
from signalflow.core.base_mixin import SfTorchModuleMixin
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"RawData",
|
|
27
|
+
"Signals",
|
|
28
|
+
"RawDataView",
|
|
29
|
+
"Position",
|
|
30
|
+
"Trade",
|
|
31
|
+
"Order",
|
|
32
|
+
"OrderFill",
|
|
33
|
+
"Portfolio",
|
|
34
|
+
"StrategyState",
|
|
35
|
+
"SignalType",
|
|
36
|
+
"PositionType",
|
|
37
|
+
"SfComponentType",
|
|
38
|
+
"DataFrameType",
|
|
39
|
+
"RawDataType",
|
|
40
|
+
"sf_component",
|
|
41
|
+
"default_registry",
|
|
42
|
+
"SignalFlowRegistry",
|
|
43
|
+
"RollingAggregator",
|
|
44
|
+
"SignalsTransform",
|
|
45
|
+
"SfTorchModuleMixin",
|
|
46
|
+
]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import optuna
|
|
3
|
+
from signalflow.core.enums import SfComponentType
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SfTorchModuleMixin(ABC):
|
|
8
|
+
"""Mixin for all SignalFlow neural network modules.
|
|
9
|
+
|
|
10
|
+
Provides standardized interface for PyTorch modules used in SignalFlow,
|
|
11
|
+
including default parameters and hyperparameter tuning via Optuna.
|
|
12
|
+
|
|
13
|
+
All neural network modules (detectors, validators, etc.) should inherit
|
|
14
|
+
from this mixin to ensure consistent configuration and tuning interfaces.
|
|
15
|
+
|
|
16
|
+
Key features:
|
|
17
|
+
- Automatic component type registration
|
|
18
|
+
- Standardized parameter interface
|
|
19
|
+
- Built-in Optuna integration for hyperparameter tuning
|
|
20
|
+
- Size-based architecture variants (small, medium, large)
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
component_type (SfComponentType): Always set to TORCH_MODULE for registry.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
```python
|
|
27
|
+
import torch
|
|
28
|
+
import torch.nn as nn
|
|
29
|
+
import optuna
|
|
30
|
+
from signalflow.core import SfTorchModuleMixin
|
|
31
|
+
|
|
32
|
+
class MyLSTMDetector(nn.Module, SfTorchModuleMixin):
|
|
33
|
+
def __init__(self, input_size: int, hidden_size: int, num_layers: int):
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.lstm = nn.LSTM(input_size, hidden_size, num_layers)
|
|
36
|
+
self.fc = nn.Linear(hidden_size, 3) # 3 classes
|
|
37
|
+
|
|
38
|
+
def forward(self, x):
|
|
39
|
+
out, _ = self.lstm(x)
|
|
40
|
+
return self.fc(out[:, -1, :])
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def default_params(cls) -> dict:
|
|
44
|
+
return {
|
|
45
|
+
"input_size": 10,
|
|
46
|
+
"hidden_size": 64,
|
|
47
|
+
"num_layers": 2
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def tune(cls, trial: optuna.Trial, model_size: str = "small") -> dict:
|
|
52
|
+
if model_size == "small":
|
|
53
|
+
hidden_range = (32, 64)
|
|
54
|
+
layers_range = (1, 2)
|
|
55
|
+
elif model_size == "medium":
|
|
56
|
+
hidden_range = (64, 128)
|
|
57
|
+
layers_range = (2, 3)
|
|
58
|
+
else: # large
|
|
59
|
+
hidden_range = (128, 256)
|
|
60
|
+
layers_range = (3, 4)
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"input_size": 10,
|
|
64
|
+
"hidden_size": trial.suggest_int("hidden_size", *hidden_range),
|
|
65
|
+
"num_layers": trial.suggest_int("num_layers", *layers_range)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Use default params
|
|
69
|
+
model = MyLSTMDetector(**MyLSTMDetector.default_params())
|
|
70
|
+
|
|
71
|
+
# Hyperparameter tuning
|
|
72
|
+
study = optuna.create_study()
|
|
73
|
+
|
|
74
|
+
def objective(trial):
|
|
75
|
+
params = MyLSTMDetector.tune(trial, model_size="medium")
|
|
76
|
+
model = MyLSTMDetector(**params)
|
|
77
|
+
# ... train and evaluate model ...
|
|
78
|
+
return validation_loss
|
|
79
|
+
|
|
80
|
+
study.optimize(objective, n_trials=100)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Note:
|
|
84
|
+
Classes inheriting this mixin must implement both abstract methods.
|
|
85
|
+
The component_type is automatically set for registry integration.
|
|
86
|
+
|
|
87
|
+
See Also:
|
|
88
|
+
sf_component: Decorator for registering modules in the registry.
|
|
89
|
+
SfComponentType: Enum of all component types including TORCH_MODULE.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
component_type: SfComponentType = SfComponentType.TORCH_MODULE
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def default_params(cls) -> dict:
|
|
97
|
+
"""Get default parameters for module instantiation.
|
|
98
|
+
|
|
99
|
+
Provides sensible defaults for quick prototyping and baseline comparisons.
|
|
100
|
+
These parameters should work reasonably well out-of-the-box.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
dict: Dictionary of parameter names and default values.
|
|
104
|
+
Keys match constructor argument names.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
```python
|
|
108
|
+
class MyTransformer(nn.Module, SfTorchModuleMixin):
|
|
109
|
+
def __init__(self, d_model: int, nhead: int, num_layers: int):
|
|
110
|
+
super().__init__()
|
|
111
|
+
# ... initialization ...
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def default_params(cls) -> dict:
|
|
115
|
+
return {
|
|
116
|
+
"d_model": 128,
|
|
117
|
+
"nhead": 8,
|
|
118
|
+
"num_layers": 3
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Instantiate with defaults
|
|
122
|
+
model = MyTransformer(**MyTransformer.default_params())
|
|
123
|
+
|
|
124
|
+
# Override specific params
|
|
125
|
+
params = MyTransformer.default_params()
|
|
126
|
+
params["d_model"] = 256
|
|
127
|
+
model = MyTransformer(**params)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Note:
|
|
131
|
+
Should be comprehensive - include all constructor parameters.
|
|
132
|
+
Consider computational constraints when setting defaults.
|
|
133
|
+
"""
|
|
134
|
+
...
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
@abstractmethod
|
|
138
|
+
def tune(cls, trial: optuna.Trial, model_size: Literal["small", "medium", "large"] = "small") -> dict:
|
|
139
|
+
"""Define Optuna hyperparameter search space.
|
|
140
|
+
|
|
141
|
+
Provides size-based architecture variants for different computational budgets:
|
|
142
|
+
- small: Fast training, limited capacity
|
|
143
|
+
- medium: Balanced performance/speed
|
|
144
|
+
- large: Maximum capacity, slower training
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
trial (optuna.Trial): Optuna trial object for suggesting hyperparameters.
|
|
148
|
+
model_size (Literal["small", "medium", "large"]): Architecture size variant.
|
|
149
|
+
Default: "small".
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
dict: Dictionary of hyperparameters sampled from search space.
|
|
153
|
+
Keys match constructor argument names.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
```python
|
|
157
|
+
class MyRNN(nn.Module, SfTorchModuleMixin):
|
|
158
|
+
def __init__(self, input_size: int, hidden_size: int,
|
|
159
|
+
num_layers: int, dropout: float):
|
|
160
|
+
super().__init__()
|
|
161
|
+
# ... initialization ...
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def default_params(cls) -> dict:
|
|
165
|
+
return {
|
|
166
|
+
"input_size": 20,
|
|
167
|
+
"hidden_size": 64,
|
|
168
|
+
"num_layers": 2,
|
|
169
|
+
"dropout": 0.1
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def tune(cls, trial: optuna.Trial, model_size: str = "small") -> dict:
|
|
174
|
+
# Size-based ranges
|
|
175
|
+
size_config = {
|
|
176
|
+
"small": {
|
|
177
|
+
"hidden": (32, 64),
|
|
178
|
+
"layers": (1, 2)
|
|
179
|
+
},
|
|
180
|
+
"medium": {
|
|
181
|
+
"hidden": (64, 128),
|
|
182
|
+
"layers": (2, 3)
|
|
183
|
+
},
|
|
184
|
+
"large": {
|
|
185
|
+
"hidden": (128, 256),
|
|
186
|
+
"layers": (3, 5)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
config = size_config[model_size]
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
"input_size": 20,
|
|
194
|
+
"hidden_size": trial.suggest_int(
|
|
195
|
+
"hidden_size",
|
|
196
|
+
*config["hidden"]
|
|
197
|
+
),
|
|
198
|
+
"num_layers": trial.suggest_int(
|
|
199
|
+
"num_layers",
|
|
200
|
+
*config["layers"]
|
|
201
|
+
),
|
|
202
|
+
"dropout": trial.suggest_float("dropout", 0.0, 0.5)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Run hyperparameter search
|
|
206
|
+
import optuna
|
|
207
|
+
|
|
208
|
+
def objective(trial):
|
|
209
|
+
params = MyRNN.tune(trial, model_size="medium")
|
|
210
|
+
model = MyRNN(**params)
|
|
211
|
+
|
|
212
|
+
# Train model
|
|
213
|
+
trainer = Trainer(model, train_data, val_data)
|
|
214
|
+
val_loss = trainer.train()
|
|
215
|
+
|
|
216
|
+
return val_loss
|
|
217
|
+
|
|
218
|
+
study = optuna.create_study(direction="minimize")
|
|
219
|
+
study.optimize(objective, n_trials=50)
|
|
220
|
+
|
|
221
|
+
# Get best params
|
|
222
|
+
best_params = study.best_params
|
|
223
|
+
best_model = MyRNN(**MyRNN.tune(study.best_trial, "medium"))
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Note:
|
|
227
|
+
Search space should be tailored to model_size.
|
|
228
|
+
Use trial.suggest_* methods (int, float, categorical).
|
|
229
|
+
Return dict must be compatible with constructor.
|
|
230
|
+
Consider pruning for early stopping of poor trials.
|
|
231
|
+
"""
|
|
232
|
+
...
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from signalflow.core.containers.raw_data import RawData
|
|
2
|
+
from signalflow.core.containers.raw_data_view import RawDataView
|
|
3
|
+
from signalflow.core.containers.signals import Signals
|
|
4
|
+
from signalflow.core.containers.position import Position
|
|
5
|
+
from signalflow.core.containers.trade import Trade
|
|
6
|
+
from signalflow.core.containers.portfolio import Portfolio
|
|
7
|
+
from signalflow.core.containers.strategy_state import StrategyState
|
|
8
|
+
from signalflow.core.containers.order import Order, OrderFill
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"RawData",
|
|
13
|
+
"RawDataView",
|
|
14
|
+
"Signals",
|
|
15
|
+
"Position",
|
|
16
|
+
"Trade",
|
|
17
|
+
"Portfolio",
|
|
18
|
+
"StrategyState",
|
|
19
|
+
"Order",
|
|
20
|
+
"OrderFill",
|
|
21
|
+
]
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Order and OrderFill containers for strategy execution."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
OrderSide = Literal['BUY', 'SELL']
|
|
9
|
+
OrderType = Literal['MARKET', 'LIMIT']
|
|
10
|
+
OrderStatus = Literal['NEW', 'FILLED', 'PARTIALLY_FILLED', 'CANCELLED', 'REJECTED']
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True)
|
|
14
|
+
class Order:
|
|
15
|
+
"""Trading order intent (mutable).
|
|
16
|
+
|
|
17
|
+
Represents the intent to trade. Orders are instructions that may or may not
|
|
18
|
+
be filled by the executor.
|
|
19
|
+
|
|
20
|
+
Order lifecycle:
|
|
21
|
+
NEW → PARTIALLY_FILLED → FILLED
|
|
22
|
+
→ CANCELLED
|
|
23
|
+
→ REJECTED
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
id (str): Unique order identifier.
|
|
27
|
+
pair (str): Trading pair (e.g. "BTCUSDT").
|
|
28
|
+
side (OrderSide): Order side - "BUY" or "SELL".
|
|
29
|
+
order_type (OrderType): Order type - "MARKET" or "LIMIT".
|
|
30
|
+
qty (float): Order quantity (always positive).
|
|
31
|
+
price (float | None): Limit price (None for MARKET orders).
|
|
32
|
+
created_at (datetime | None): Order creation timestamp.
|
|
33
|
+
status (OrderStatus): Current order status.
|
|
34
|
+
position_id (str | None): ID of position this order affects.
|
|
35
|
+
signal_strength (float): Strength of signal triggering order (0-1).
|
|
36
|
+
meta (dict[str, Any]): Additional metadata (e.g., signal info, exit reason).
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
```python
|
|
40
|
+
from signalflow.core import Order
|
|
41
|
+
|
|
42
|
+
# Market buy order
|
|
43
|
+
market_order = Order(
|
|
44
|
+
pair="BTCUSDT",
|
|
45
|
+
side="BUY",
|
|
46
|
+
order_type="MARKET",
|
|
47
|
+
qty=0.5,
|
|
48
|
+
position_id="pos_123",
|
|
49
|
+
signal_strength=0.85,
|
|
50
|
+
meta={"type": "entry", "signal": "sma_cross"}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Limit sell order
|
|
54
|
+
limit_order = Order(
|
|
55
|
+
pair="BTCUSDT",
|
|
56
|
+
side="SELL",
|
|
57
|
+
order_type="LIMIT",
|
|
58
|
+
qty=0.5,
|
|
59
|
+
price=46000.0,
|
|
60
|
+
position_id="pos_123",
|
|
61
|
+
meta={"type": "exit", "reason": "take_profit"}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Check order properties
|
|
65
|
+
assert market_order.is_buy
|
|
66
|
+
assert market_order.is_market
|
|
67
|
+
assert limit_order.is_sell
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Note:
|
|
71
|
+
Orders are mutable to allow status updates.
|
|
72
|
+
Not all orders result in fills (e.g., insufficient liquidity, rejected).
|
|
73
|
+
"""
|
|
74
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
75
|
+
pair: str = ''
|
|
76
|
+
side: OrderSide = 'BUY'
|
|
77
|
+
order_type: OrderType = 'MARKET'
|
|
78
|
+
qty: float = 0.0
|
|
79
|
+
price: float | None = None
|
|
80
|
+
created_at: datetime | None = None
|
|
81
|
+
status: OrderStatus = 'NEW'
|
|
82
|
+
position_id: str | None = None
|
|
83
|
+
signal_strength: float = 1.0
|
|
84
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def is_buy(self) -> bool:
|
|
88
|
+
"""Check if order is a buy order.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
bool: True if side is "BUY".
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
```python
|
|
95
|
+
order = Order(side="BUY")
|
|
96
|
+
assert order.is_buy
|
|
97
|
+
```
|
|
98
|
+
"""
|
|
99
|
+
return self.side == 'BUY'
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def is_sell(self) -> bool:
|
|
103
|
+
"""Check if order is a sell order.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
bool: True if side is "SELL".
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
```python
|
|
110
|
+
order = Order(side="SELL")
|
|
111
|
+
assert order.is_sell
|
|
112
|
+
```
|
|
113
|
+
"""
|
|
114
|
+
return self.side == 'SELL'
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def is_market(self) -> bool:
|
|
118
|
+
"""Check if order is a market order.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
bool: True if order_type is "MARKET".
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
```python
|
|
125
|
+
order = Order(order_type="MARKET")
|
|
126
|
+
assert order.is_market
|
|
127
|
+
```
|
|
128
|
+
"""
|
|
129
|
+
return self.order_type == 'MARKET'
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True, slots=True)
|
|
133
|
+
class OrderFill:
|
|
134
|
+
"""Order execution result (immutable).
|
|
135
|
+
|
|
136
|
+
Represents the actual execution of an order. Maps directly to trade(s).
|
|
137
|
+
OrderFill is the bridge between order intent and executed trades.
|
|
138
|
+
|
|
139
|
+
Attributes:
|
|
140
|
+
id (str): Unique fill identifier.
|
|
141
|
+
order_id (str): ID of the order that was filled.
|
|
142
|
+
pair (str): Trading pair.
|
|
143
|
+
side (OrderSide): Fill side - "BUY" or "SELL".
|
|
144
|
+
ts (datetime | None): Fill timestamp.
|
|
145
|
+
price (float): Execution price.
|
|
146
|
+
qty (float): Filled quantity (may differ from order qty for partial fills).
|
|
147
|
+
fee (float): Transaction fee for this fill.
|
|
148
|
+
position_id (str | None): ID of the position affected.
|
|
149
|
+
meta (dict[str, Any]): Additional fill metadata.
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
```python
|
|
153
|
+
from signalflow.core import OrderFill
|
|
154
|
+
from datetime import datetime
|
|
155
|
+
|
|
156
|
+
# Complete fill
|
|
157
|
+
fill = OrderFill(
|
|
158
|
+
order_id="order_123",
|
|
159
|
+
pair="BTCUSDT",
|
|
160
|
+
side="BUY",
|
|
161
|
+
ts=datetime.now(),
|
|
162
|
+
price=45000.0,
|
|
163
|
+
qty=0.5,
|
|
164
|
+
fee=22.5,
|
|
165
|
+
position_id="pos_123"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Check fill details
|
|
169
|
+
print(f"Notional: ${fill.notional:.2f}")
|
|
170
|
+
print(f"Fee: ${fill.fee:.2f}")
|
|
171
|
+
|
|
172
|
+
# Partial fill
|
|
173
|
+
partial_fill = OrderFill(
|
|
174
|
+
order_id="order_456",
|
|
175
|
+
pair="BTCUSDT",
|
|
176
|
+
side="BUY",
|
|
177
|
+
ts=datetime.now(),
|
|
178
|
+
price=45100.0,
|
|
179
|
+
qty=0.3, # Order was for 0.5
|
|
180
|
+
fee=13.53,
|
|
181
|
+
position_id="pos_123",
|
|
182
|
+
meta={"status": "partial", "remaining": 0.2}
|
|
183
|
+
)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Note:
|
|
187
|
+
OrderFill.qty may be less than Order.qty (partial fills).
|
|
188
|
+
Always check fills to update accounting correctly.
|
|
189
|
+
"""
|
|
190
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
191
|
+
order_id: str = ''
|
|
192
|
+
pair: str = ''
|
|
193
|
+
side: OrderSide = 'BUY'
|
|
194
|
+
ts: datetime | None = None
|
|
195
|
+
price: float = 0.0
|
|
196
|
+
qty: float = 0.0
|
|
197
|
+
fee: float = 0.0
|
|
198
|
+
position_id: str | None = None
|
|
199
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def notional(self) -> float:
|
|
203
|
+
"""Calculate notional value of the fill.
|
|
204
|
+
|
|
205
|
+
Notional = price * qty
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
float: Notional value in currency units.
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
```python
|
|
212
|
+
fill = OrderFill(price=45000.0, qty=0.5)
|
|
213
|
+
assert fill.notional == 22500.0 # 45000 * 0.5
|
|
214
|
+
```
|
|
215
|
+
"""
|
|
216
|
+
return self.price * self.qty
|