flox-py 0.5.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.
- flox_py-0.5.0/CMakeLists.txt +16 -0
- flox_py-0.5.0/PKG-INFO +80 -0
- flox_py-0.5.0/README.md +64 -0
- flox_py-0.5.0/aggregator_bindings.h +210 -0
- flox_py-0.5.0/backtest_bindings.h +210 -0
- flox_py-0.5.0/book_bindings.h +322 -0
- flox_py-0.5.0/composite_book_bindings.h +197 -0
- flox_py-0.5.0/flox_py.cpp +360 -0
- flox_py-0.5.0/indicator_bindings.h +473 -0
- flox_py-0.5.0/optimizer_bindings.h +192 -0
- flox_py-0.5.0/position_bindings.h +395 -0
- flox_py-0.5.0/profile_bindings.h +347 -0
- flox_py-0.5.0/pyproject.toml +29 -0
- flox_py-0.5.0/replay_bindings.h +517 -0
- flox_py-0.5.0/segment_ops_bindings.h +464 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
find_package(pybind11 REQUIRED)
|
|
2
|
+
|
|
3
|
+
pybind11_add_module(flox_py flox_py.cpp)
|
|
4
|
+
|
|
5
|
+
target_link_libraries(flox_py PRIVATE flox)
|
|
6
|
+
target_include_directories(flox_py PRIVATE
|
|
7
|
+
${CMAKE_CURRENT_SOURCE_DIR}/../include
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
if(NOT MSVC)
|
|
11
|
+
target_compile_options(flox_py PRIVATE
|
|
12
|
+
$<$<CONFIG:Release>:-O3 -march=native -flto>
|
|
13
|
+
)
|
|
14
|
+
endif()
|
|
15
|
+
|
|
16
|
+
install(TARGETS flox_py LIBRARY DESTINATION . COMPONENT python_module)
|
flox_py-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flox-py
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Python bindings for the Flox indicator library
|
|
5
|
+
Keywords: trading,backtest,indicators,technical-analysis
|
|
6
|
+
Author: FLOX Foundation
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
10
|
+
Classifier: Programming Language :: C++
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Project-URL: Repository, https://github.com/FLOX-Foundation/flox
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# flox-py
|
|
18
|
+
|
|
19
|
+
Python bindings for the Flox indicator library. Tested against TA-Lib.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install flox-py
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import flox_py as flox
|
|
31
|
+
import numpy as np
|
|
32
|
+
|
|
33
|
+
close = np.array([...])
|
|
34
|
+
high = np.array([...])
|
|
35
|
+
low = np.array([...])
|
|
36
|
+
|
|
37
|
+
ema50 = flox.ema(close, 50)
|
|
38
|
+
atr14 = flox.atr(high, low, close, 14)
|
|
39
|
+
rsi14 = flox.rsi(close, 14)
|
|
40
|
+
|
|
41
|
+
signal_long = (rsi14 > 70).astype(np.int8)
|
|
42
|
+
signal_short = ((rsi14 < 30) * -1).astype(np.int8)
|
|
43
|
+
|
|
44
|
+
returns = flox.bar_returns(signal_long, signal_short, log_returns)
|
|
45
|
+
trades = flox.trade_pnl(signal_long, signal_short, log_returns)
|
|
46
|
+
pf = flox.profit_factor(returns)
|
|
47
|
+
wr = flox.win_rate(trades)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Indicators
|
|
51
|
+
|
|
52
|
+
| Function | Description |
|
|
53
|
+
|----------|-------------|
|
|
54
|
+
| `flox.ema(input, period)` | Exponential Moving Average |
|
|
55
|
+
| `flox.sma(input, period)` | Simple Moving Average |
|
|
56
|
+
| `flox.rma(input, period)` | Wilder Moving Average |
|
|
57
|
+
| `flox.rsi(input, period)` | Relative Strength Index |
|
|
58
|
+
| `flox.atr(high, low, close, period)` | Average True Range |
|
|
59
|
+
| `flox.adx(high, low, close, period)` | ADX, +DI, -DI |
|
|
60
|
+
| `flox.macd(input, fast, slow, signal)` | MACD line, signal, histogram |
|
|
61
|
+
| `flox.bollinger(input, period, stddev)` | upper, middle, lower bands |
|
|
62
|
+
| `flox.stochastic(high, low, close, k, d)` | %K and %D |
|
|
63
|
+
| `flox.cci(high, low, close, period)` | Commodity Channel Index |
|
|
64
|
+
| `flox.slope(input, length)` | price slope |
|
|
65
|
+
| `flox.kama(input, period)` | Kaufman Adaptive MA |
|
|
66
|
+
| `flox.dema(input, period)` | Double EMA |
|
|
67
|
+
| `flox.tema(input, period)` | Triple EMA |
|
|
68
|
+
| `flox.chop(high, low, close, period)` | Choppiness Index |
|
|
69
|
+
| `flox.obv(close, volume)` | On-Balance Volume |
|
|
70
|
+
| `flox.vwap(close, volume, window)` | rolling VWAP |
|
|
71
|
+
| `flox.cvd(open, high, low, close, volume)` | Cumulative Volume Delta |
|
|
72
|
+
|
|
73
|
+
## Metrics
|
|
74
|
+
|
|
75
|
+
| Function | Description |
|
|
76
|
+
|----------|-------------|
|
|
77
|
+
| `flox.bar_returns(sig_long, sig_short, log_ret)` | per-bar returns with signal shift |
|
|
78
|
+
| `flox.trade_pnl(sig_long, sig_short, log_ret)` | per-trade PnL array |
|
|
79
|
+
| `flox.profit_factor(returns)` | sum positive / abs sum negative |
|
|
80
|
+
| `flox.win_rate(trade_pnls)` | fraction of winning trades |
|
flox_py-0.5.0/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# flox-py
|
|
2
|
+
|
|
3
|
+
Python bindings for the Flox indicator library. Tested against TA-Lib.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install flox-py
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import flox_py as flox
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
close = np.array([...])
|
|
18
|
+
high = np.array([...])
|
|
19
|
+
low = np.array([...])
|
|
20
|
+
|
|
21
|
+
ema50 = flox.ema(close, 50)
|
|
22
|
+
atr14 = flox.atr(high, low, close, 14)
|
|
23
|
+
rsi14 = flox.rsi(close, 14)
|
|
24
|
+
|
|
25
|
+
signal_long = (rsi14 > 70).astype(np.int8)
|
|
26
|
+
signal_short = ((rsi14 < 30) * -1).astype(np.int8)
|
|
27
|
+
|
|
28
|
+
returns = flox.bar_returns(signal_long, signal_short, log_returns)
|
|
29
|
+
trades = flox.trade_pnl(signal_long, signal_short, log_returns)
|
|
30
|
+
pf = flox.profit_factor(returns)
|
|
31
|
+
wr = flox.win_rate(trades)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Indicators
|
|
35
|
+
|
|
36
|
+
| Function | Description |
|
|
37
|
+
|----------|-------------|
|
|
38
|
+
| `flox.ema(input, period)` | Exponential Moving Average |
|
|
39
|
+
| `flox.sma(input, period)` | Simple Moving Average |
|
|
40
|
+
| `flox.rma(input, period)` | Wilder Moving Average |
|
|
41
|
+
| `flox.rsi(input, period)` | Relative Strength Index |
|
|
42
|
+
| `flox.atr(high, low, close, period)` | Average True Range |
|
|
43
|
+
| `flox.adx(high, low, close, period)` | ADX, +DI, -DI |
|
|
44
|
+
| `flox.macd(input, fast, slow, signal)` | MACD line, signal, histogram |
|
|
45
|
+
| `flox.bollinger(input, period, stddev)` | upper, middle, lower bands |
|
|
46
|
+
| `flox.stochastic(high, low, close, k, d)` | %K and %D |
|
|
47
|
+
| `flox.cci(high, low, close, period)` | Commodity Channel Index |
|
|
48
|
+
| `flox.slope(input, length)` | price slope |
|
|
49
|
+
| `flox.kama(input, period)` | Kaufman Adaptive MA |
|
|
50
|
+
| `flox.dema(input, period)` | Double EMA |
|
|
51
|
+
| `flox.tema(input, period)` | Triple EMA |
|
|
52
|
+
| `flox.chop(high, low, close, period)` | Choppiness Index |
|
|
53
|
+
| `flox.obv(close, volume)` | On-Balance Volume |
|
|
54
|
+
| `flox.vwap(close, volume, window)` | rolling VWAP |
|
|
55
|
+
| `flox.cvd(open, high, low, close, volume)` | Cumulative Volume Delta |
|
|
56
|
+
|
|
57
|
+
## Metrics
|
|
58
|
+
|
|
59
|
+
| Function | Description |
|
|
60
|
+
|----------|-------------|
|
|
61
|
+
| `flox.bar_returns(sig_long, sig_short, log_ret)` | per-bar returns with signal shift |
|
|
62
|
+
| `flox.trade_pnl(sig_long, sig_short, log_ret)` | per-trade PnL array |
|
|
63
|
+
| `flox.profit_factor(returns)` | sum positive / abs sum negative |
|
|
64
|
+
| `flox.win_rate(trade_pnls)` | fraction of winning trades |
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// python/aggregator_bindings.h
|
|
2
|
+
|
|
3
|
+
#pragma once
|
|
4
|
+
|
|
5
|
+
#include <pybind11/numpy.h>
|
|
6
|
+
#include <pybind11/pybind11.h>
|
|
7
|
+
#include <pybind11/stl.h>
|
|
8
|
+
|
|
9
|
+
#include "flox/aggregator/aggregation_policy.h"
|
|
10
|
+
#include "flox/aggregator/bar.h"
|
|
11
|
+
#include "flox/aggregator/policies/heikin_ashi_bar_policy.h"
|
|
12
|
+
#include "flox/aggregator/policies/range_bar_policy.h"
|
|
13
|
+
#include "flox/aggregator/policies/renko_bar_policy.h"
|
|
14
|
+
#include "flox/aggregator/policies/tick_bar_policy.h"
|
|
15
|
+
#include "flox/aggregator/policies/time_bar_policy.h"
|
|
16
|
+
#include "flox/aggregator/policies/volume_bar_policy.h"
|
|
17
|
+
#include "flox/book/events/trade_event.h"
|
|
18
|
+
#include "flox/book/trade.h"
|
|
19
|
+
#include "flox/common.h"
|
|
20
|
+
#include "flox/util/base/time.h"
|
|
21
|
+
|
|
22
|
+
#include <chrono>
|
|
23
|
+
#include <cstring>
|
|
24
|
+
#include <vector>
|
|
25
|
+
|
|
26
|
+
namespace py = pybind11;
|
|
27
|
+
|
|
28
|
+
namespace
|
|
29
|
+
{
|
|
30
|
+
|
|
31
|
+
using namespace flox;
|
|
32
|
+
|
|
33
|
+
#pragma pack(push, 1)
|
|
34
|
+
struct PyExtBar
|
|
35
|
+
{
|
|
36
|
+
int64_t start_time_ns;
|
|
37
|
+
int64_t end_time_ns;
|
|
38
|
+
int64_t open_raw;
|
|
39
|
+
int64_t high_raw;
|
|
40
|
+
int64_t low_raw;
|
|
41
|
+
int64_t close_raw;
|
|
42
|
+
int64_t volume_raw;
|
|
43
|
+
int64_t buy_volume_raw;
|
|
44
|
+
int64_t trade_count;
|
|
45
|
+
};
|
|
46
|
+
#pragma pack(pop)
|
|
47
|
+
static_assert(sizeof(PyExtBar) == 72);
|
|
48
|
+
|
|
49
|
+
inline int64_t toUnixNs(TimePoint tp)
|
|
50
|
+
{
|
|
51
|
+
return tp.time_since_epoch().count() - unix_to_flox_offset_ns().load(std::memory_order_relaxed);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
inline PyExtBar barToExtBar(const Bar& b)
|
|
55
|
+
{
|
|
56
|
+
return {.start_time_ns = toUnixNs(b.startTime),
|
|
57
|
+
.end_time_ns = toUnixNs(b.endTime),
|
|
58
|
+
.open_raw = b.open.raw(),
|
|
59
|
+
.high_raw = b.high.raw(),
|
|
60
|
+
.low_raw = b.low.raw(),
|
|
61
|
+
.close_raw = b.close.raw(),
|
|
62
|
+
.volume_raw = b.volume.raw(),
|
|
63
|
+
.buy_volume_raw = b.buyVolume.raw(),
|
|
64
|
+
.trade_count = b.tradeCount.raw()};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Batch aggregation: takes pre-extracted vectors (no GIL needed)
|
|
68
|
+
template <typename Policy>
|
|
69
|
+
std::vector<PyExtBar> doAggregate(Policy& policy, const int64_t* ts, const double* px,
|
|
70
|
+
const double* qty, const uint8_t* ib, size_t n)
|
|
71
|
+
{
|
|
72
|
+
std::vector<PyExtBar> bars;
|
|
73
|
+
bars.reserve(n / 10); // rough estimate
|
|
74
|
+
Bar currentBar;
|
|
75
|
+
bool initialized = false;
|
|
76
|
+
|
|
77
|
+
for (size_t i = 0; i < n; ++i)
|
|
78
|
+
{
|
|
79
|
+
TradeEvent trade;
|
|
80
|
+
trade.trade.price = Price::fromDouble(px[i]);
|
|
81
|
+
trade.trade.quantity = Quantity::fromDouble(qty[i]);
|
|
82
|
+
trade.trade.isBuy = (ib[i] != 0);
|
|
83
|
+
trade.trade.exchangeTsNs = ts[i];
|
|
84
|
+
trade.trade.symbol = 1;
|
|
85
|
+
trade.trade.instrument = InstrumentType::Spot;
|
|
86
|
+
|
|
87
|
+
if (!initialized)
|
|
88
|
+
{
|
|
89
|
+
policy.initBar(trade, currentBar);
|
|
90
|
+
initialized = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (policy.shouldClose(trade, currentBar))
|
|
95
|
+
{
|
|
96
|
+
bars.push_back(barToExtBar(currentBar));
|
|
97
|
+
policy.initBar(trade, currentBar);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
policy.update(trade, currentBar);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (initialized)
|
|
105
|
+
{
|
|
106
|
+
bars.push_back(barToExtBar(currentBar));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return bars;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
template <typename Policy>
|
|
113
|
+
py::array_t<PyExtBar> aggregateBars(Policy policy, py::array_t<int64_t> timestamps,
|
|
114
|
+
py::array_t<double> prices, py::array_t<double> quantities,
|
|
115
|
+
py::array_t<uint8_t> isBuy)
|
|
116
|
+
{
|
|
117
|
+
size_t n = timestamps.size();
|
|
118
|
+
const auto* ts = timestamps.data();
|
|
119
|
+
const auto* px = prices.data();
|
|
120
|
+
const auto* qt = quantities.data();
|
|
121
|
+
const auto* ib = isBuy.data();
|
|
122
|
+
|
|
123
|
+
std::vector<PyExtBar> bars;
|
|
124
|
+
{
|
|
125
|
+
py::gil_scoped_release release;
|
|
126
|
+
bars = doAggregate(policy, ts, px, qt, ib, n);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
py::array_t<PyExtBar> result(bars.size());
|
|
130
|
+
if (!bars.empty())
|
|
131
|
+
{
|
|
132
|
+
std::memcpy(result.mutable_data(), bars.data(), bars.size() * sizeof(PyExtBar));
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
} // namespace
|
|
138
|
+
|
|
139
|
+
inline void bindAggregators(py::module_& m)
|
|
140
|
+
{
|
|
141
|
+
PYBIND11_NUMPY_DTYPE(PyExtBar, start_time_ns, end_time_ns, open_raw, high_raw, low_raw,
|
|
142
|
+
close_raw, volume_raw, buy_volume_raw, trade_count);
|
|
143
|
+
|
|
144
|
+
// Ensure time mapping is initialized
|
|
145
|
+
flox::init_timebase_mapping();
|
|
146
|
+
|
|
147
|
+
m.def(
|
|
148
|
+
"aggregate_time_bars",
|
|
149
|
+
[](py::array_t<int64_t> ts, py::array_t<double> px, py::array_t<double> qty,
|
|
150
|
+
py::array_t<uint8_t> is_buy, double interval_seconds)
|
|
151
|
+
{
|
|
152
|
+
auto policy = flox::TimeBarPolicy(
|
|
153
|
+
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
154
|
+
std::chrono::duration<double>(interval_seconds)));
|
|
155
|
+
return aggregateBars(std::move(policy), ts, px, qty, is_buy);
|
|
156
|
+
},
|
|
157
|
+
"Aggregate trades into time bars",
|
|
158
|
+
py::arg("timestamps"), py::arg("prices"), py::arg("quantities"),
|
|
159
|
+
py::arg("is_buy"), py::arg("interval_seconds"));
|
|
160
|
+
|
|
161
|
+
m.def(
|
|
162
|
+
"aggregate_tick_bars",
|
|
163
|
+
[](py::array_t<int64_t> ts, py::array_t<double> px, py::array_t<double> qty,
|
|
164
|
+
py::array_t<uint8_t> is_buy, uint32_t tick_count)
|
|
165
|
+
{ return aggregateBars(flox::TickBarPolicy(tick_count), ts, px, qty, is_buy); },
|
|
166
|
+
"Aggregate trades into tick bars",
|
|
167
|
+
py::arg("timestamps"), py::arg("prices"), py::arg("quantities"),
|
|
168
|
+
py::arg("is_buy"), py::arg("tick_count"));
|
|
169
|
+
|
|
170
|
+
m.def(
|
|
171
|
+
"aggregate_volume_bars",
|
|
172
|
+
[](py::array_t<int64_t> ts, py::array_t<double> px, py::array_t<double> qty,
|
|
173
|
+
py::array_t<uint8_t> is_buy, double volume_threshold)
|
|
174
|
+
{ return aggregateBars(flox::VolumeBarPolicy::fromDouble(volume_threshold), ts, px, qty, is_buy); },
|
|
175
|
+
"Aggregate trades into volume bars",
|
|
176
|
+
py::arg("timestamps"), py::arg("prices"), py::arg("quantities"),
|
|
177
|
+
py::arg("is_buy"), py::arg("volume_threshold"));
|
|
178
|
+
|
|
179
|
+
m.def(
|
|
180
|
+
"aggregate_range_bars",
|
|
181
|
+
[](py::array_t<int64_t> ts, py::array_t<double> px, py::array_t<double> qty,
|
|
182
|
+
py::array_t<uint8_t> is_buy, double range_size)
|
|
183
|
+
{ return aggregateBars(flox::RangeBarPolicy::fromDouble(range_size), ts, px, qty, is_buy); },
|
|
184
|
+
"Aggregate trades into range bars",
|
|
185
|
+
py::arg("timestamps"), py::arg("prices"), py::arg("quantities"),
|
|
186
|
+
py::arg("is_buy"), py::arg("range_size"));
|
|
187
|
+
|
|
188
|
+
m.def(
|
|
189
|
+
"aggregate_renko_bars",
|
|
190
|
+
[](py::array_t<int64_t> ts, py::array_t<double> px, py::array_t<double> qty,
|
|
191
|
+
py::array_t<uint8_t> is_buy, double brick_size)
|
|
192
|
+
{ return aggregateBars(flox::RenkoBarPolicy::fromDouble(brick_size), ts, px, qty, is_buy); },
|
|
193
|
+
"Aggregate trades into renko bars",
|
|
194
|
+
py::arg("timestamps"), py::arg("prices"), py::arg("quantities"),
|
|
195
|
+
py::arg("is_buy"), py::arg("brick_size"));
|
|
196
|
+
|
|
197
|
+
m.def(
|
|
198
|
+
"aggregate_heikin_ashi_bars",
|
|
199
|
+
[](py::array_t<int64_t> ts, py::array_t<double> px, py::array_t<double> qty,
|
|
200
|
+
py::array_t<uint8_t> is_buy, double interval_seconds)
|
|
201
|
+
{
|
|
202
|
+
auto policy = flox::HeikinAshiBarPolicy(
|
|
203
|
+
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
204
|
+
std::chrono::duration<double>(interval_seconds)));
|
|
205
|
+
return aggregateBars(std::move(policy), ts, px, qty, is_buy);
|
|
206
|
+
},
|
|
207
|
+
"Aggregate trades into Heikin-Ashi bars",
|
|
208
|
+
py::arg("timestamps"), py::arg("prices"), py::arg("quantities"),
|
|
209
|
+
py::arg("is_buy"), py::arg("interval_seconds"));
|
|
210
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// python/backtest_bindings.h
|
|
2
|
+
|
|
3
|
+
#pragma once
|
|
4
|
+
|
|
5
|
+
#include <pybind11/numpy.h>
|
|
6
|
+
#include <pybind11/pybind11.h>
|
|
7
|
+
#include <pybind11/stl.h>
|
|
8
|
+
|
|
9
|
+
#include "flox/backtest/backtest_result.h"
|
|
10
|
+
#include "flox/backtest/simulated_clock.h"
|
|
11
|
+
#include "flox/backtest/simulated_executor.h"
|
|
12
|
+
#include "flox/common.h"
|
|
13
|
+
|
|
14
|
+
#include <cstring>
|
|
15
|
+
#include <memory>
|
|
16
|
+
#include <vector>
|
|
17
|
+
|
|
18
|
+
namespace py = pybind11;
|
|
19
|
+
|
|
20
|
+
namespace
|
|
21
|
+
{
|
|
22
|
+
|
|
23
|
+
using namespace flox;
|
|
24
|
+
|
|
25
|
+
#pragma pack(push, 1)
|
|
26
|
+
struct PyFill
|
|
27
|
+
{
|
|
28
|
+
uint64_t order_id;
|
|
29
|
+
uint32_t symbol;
|
|
30
|
+
uint8_t side;
|
|
31
|
+
uint8_t _pad[3];
|
|
32
|
+
int64_t price_raw;
|
|
33
|
+
int64_t quantity_raw;
|
|
34
|
+
int64_t timestamp_ns;
|
|
35
|
+
};
|
|
36
|
+
#pragma pack(pop)
|
|
37
|
+
static_assert(sizeof(PyFill) == 40);
|
|
38
|
+
|
|
39
|
+
#pragma pack(push, 1)
|
|
40
|
+
struct PyTradeRecord
|
|
41
|
+
{
|
|
42
|
+
uint32_t symbol;
|
|
43
|
+
uint8_t side;
|
|
44
|
+
uint8_t _pad[3];
|
|
45
|
+
int64_t entry_price_raw;
|
|
46
|
+
int64_t exit_price_raw;
|
|
47
|
+
int64_t quantity_raw;
|
|
48
|
+
int64_t entry_time_ns;
|
|
49
|
+
int64_t exit_time_ns;
|
|
50
|
+
int64_t pnl_raw;
|
|
51
|
+
int64_t fee_raw;
|
|
52
|
+
};
|
|
53
|
+
#pragma pack(pop)
|
|
54
|
+
static_assert(sizeof(PyTradeRecord) == 64);
|
|
55
|
+
|
|
56
|
+
inline PyFill fillToPyFill(const Fill& f)
|
|
57
|
+
{
|
|
58
|
+
return {.order_id = f.orderId,
|
|
59
|
+
.symbol = f.symbol,
|
|
60
|
+
.side = static_cast<uint8_t>(f.side == Side::BUY ? 0 : 1),
|
|
61
|
+
._pad = {},
|
|
62
|
+
.price_raw = f.price.raw(),
|
|
63
|
+
.quantity_raw = f.quantity.raw(),
|
|
64
|
+
.timestamp_ns = static_cast<int64_t>(f.timestampNs)};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
inline PyTradeRecord tradeRecToPy(const TradeRecord& t)
|
|
68
|
+
{
|
|
69
|
+
return {.symbol = t.symbol,
|
|
70
|
+
.side = static_cast<uint8_t>(t.side == Side::BUY ? 0 : 1),
|
|
71
|
+
._pad = {},
|
|
72
|
+
.entry_price_raw = t.entryPrice.raw(),
|
|
73
|
+
.exit_price_raw = t.exitPrice.raw(),
|
|
74
|
+
.quantity_raw = t.quantity.raw(),
|
|
75
|
+
.entry_time_ns = static_cast<int64_t>(t.entryTimeNs),
|
|
76
|
+
.exit_time_ns = static_cast<int64_t>(t.exitTimeNs),
|
|
77
|
+
.pnl_raw = t.pnl.raw(),
|
|
78
|
+
.fee_raw = t.fee.raw()};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Wraps SimulatedExecutor + SimulatedClock for standalone use
|
|
82
|
+
class PySimulatedExecutor
|
|
83
|
+
{
|
|
84
|
+
public:
|
|
85
|
+
PySimulatedExecutor() : _executor(_clock) { _executor.start(); }
|
|
86
|
+
|
|
87
|
+
void submitOrder(uint64_t id, const std::string& sideStr, double price, double qty,
|
|
88
|
+
const std::string& typeStr, uint32_t symbol)
|
|
89
|
+
{
|
|
90
|
+
Order order;
|
|
91
|
+
order.id = id;
|
|
92
|
+
order.side = (sideStr == "buy") ? Side::BUY : Side::SELL;
|
|
93
|
+
order.price = Price::fromDouble(price);
|
|
94
|
+
order.quantity = Quantity::fromDouble(qty);
|
|
95
|
+
order.symbol = symbol;
|
|
96
|
+
order.type = parseOrderType(typeStr);
|
|
97
|
+
_executor.submitOrder(order);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
void cancelOrder(uint64_t id) { _executor.cancelOrder(id); }
|
|
101
|
+
void cancelAll(uint32_t symbol) { _executor.cancelAllOrders(symbol); }
|
|
102
|
+
|
|
103
|
+
void onBar(uint32_t symbol, double closePrice)
|
|
104
|
+
{
|
|
105
|
+
_executor.onBar(symbol, Price::fromDouble(closePrice));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
void onTrade(uint32_t symbol, double price, bool isBuy)
|
|
109
|
+
{
|
|
110
|
+
_executor.onTrade(symbol, Price::fromDouble(price), isBuy);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
void advanceClock(int64_t timestampNs) { _clock.advanceTo(timestampNs); }
|
|
114
|
+
|
|
115
|
+
py::array_t<PyFill> fills() const
|
|
116
|
+
{
|
|
117
|
+
const auto& f = _executor.fills();
|
|
118
|
+
py::array_t<PyFill> result(f.size());
|
|
119
|
+
auto* out = result.mutable_data();
|
|
120
|
+
for (size_t i = 0; i < f.size(); ++i)
|
|
121
|
+
{
|
|
122
|
+
out[i] = fillToPyFill(f[i]);
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
py::list fillsList() const
|
|
128
|
+
{
|
|
129
|
+
py::list result;
|
|
130
|
+
for (const auto& f : _executor.fills())
|
|
131
|
+
{
|
|
132
|
+
py::dict d;
|
|
133
|
+
d["order_id"] = f.orderId;
|
|
134
|
+
d["symbol"] = f.symbol;
|
|
135
|
+
d["side"] = (f.side == Side::BUY) ? "buy" : "sell";
|
|
136
|
+
d["price"] = f.price.toDouble();
|
|
137
|
+
d["quantity"] = f.quantity.toDouble();
|
|
138
|
+
d["timestamp_ns"] = static_cast<int64_t>(f.timestampNs);
|
|
139
|
+
result.append(d);
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
size_t fillCount() const { return _executor.fills().size(); }
|
|
145
|
+
|
|
146
|
+
private:
|
|
147
|
+
static OrderType parseOrderType(const std::string& s)
|
|
148
|
+
{
|
|
149
|
+
if (s == "limit")
|
|
150
|
+
{
|
|
151
|
+
return OrderType::LIMIT;
|
|
152
|
+
}
|
|
153
|
+
if (s == "stop_market")
|
|
154
|
+
{
|
|
155
|
+
return OrderType::STOP_MARKET;
|
|
156
|
+
}
|
|
157
|
+
if (s == "stop_limit")
|
|
158
|
+
{
|
|
159
|
+
return OrderType::STOP_LIMIT;
|
|
160
|
+
}
|
|
161
|
+
if (s == "take_profit_market")
|
|
162
|
+
{
|
|
163
|
+
return OrderType::TAKE_PROFIT_MARKET;
|
|
164
|
+
}
|
|
165
|
+
if (s == "take_profit_limit")
|
|
166
|
+
{
|
|
167
|
+
return OrderType::TAKE_PROFIT_LIMIT;
|
|
168
|
+
}
|
|
169
|
+
if (s == "trailing_stop")
|
|
170
|
+
{
|
|
171
|
+
return OrderType::TRAILING_STOP;
|
|
172
|
+
}
|
|
173
|
+
return OrderType::MARKET;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
SimulatedClock _clock;
|
|
177
|
+
SimulatedExecutor _executor;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
} // namespace
|
|
181
|
+
|
|
182
|
+
inline void bindBacktest(py::module_& m)
|
|
183
|
+
{
|
|
184
|
+
PYBIND11_NUMPY_DTYPE(PyFill, order_id, symbol, side, price_raw, quantity_raw, timestamp_ns);
|
|
185
|
+
PYBIND11_NUMPY_DTYPE(PyTradeRecord, symbol, side, entry_price_raw, exit_price_raw,
|
|
186
|
+
quantity_raw, entry_time_ns, exit_time_ns, pnl_raw, fee_raw);
|
|
187
|
+
|
|
188
|
+
py::class_<PySimulatedExecutor>(m, "SimulatedExecutor")
|
|
189
|
+
.def(py::init<>())
|
|
190
|
+
.def("submit_order", &PySimulatedExecutor::submitOrder,
|
|
191
|
+
"Submit an order to the simulated exchange",
|
|
192
|
+
py::arg("id"), py::arg("side"), py::arg("price"), py::arg("quantity"),
|
|
193
|
+
py::arg("type") = "market", py::arg("symbol") = 1)
|
|
194
|
+
.def("cancel_order", &PySimulatedExecutor::cancelOrder, py::arg("order_id"))
|
|
195
|
+
.def("cancel_all", &PySimulatedExecutor::cancelAll, py::arg("symbol"))
|
|
196
|
+
.def("on_bar", &PySimulatedExecutor::onBar,
|
|
197
|
+
"Feed a bar close price for order matching",
|
|
198
|
+
py::arg("symbol"), py::arg("close_price"))
|
|
199
|
+
.def("on_trade", &PySimulatedExecutor::onTrade,
|
|
200
|
+
"Feed a trade for order matching",
|
|
201
|
+
py::arg("symbol"), py::arg("price"), py::arg("is_buy"))
|
|
202
|
+
.def("advance_clock", &PySimulatedExecutor::advanceClock,
|
|
203
|
+
"Advance simulation clock to timestamp",
|
|
204
|
+
py::arg("timestamp_ns"))
|
|
205
|
+
.def("fills", &PySimulatedExecutor::fills,
|
|
206
|
+
"Get all fills as numpy structured array")
|
|
207
|
+
.def("fills_list", &PySimulatedExecutor::fillsList,
|
|
208
|
+
"Get all fills as list of dicts")
|
|
209
|
+
.def_property_readonly("fill_count", &PySimulatedExecutor::fillCount);
|
|
210
|
+
}
|