PyAlgoEngine 0.12.3__cp315-cp315-win_amd64.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.
- algo_engine/__infra__.pxd +3 -0
- algo_engine/__init__.pxd +3 -0
- algo_engine/__init__.py +107 -0
- algo_engine/apps/__init__.py +17 -0
- algo_engine/apps/backtest/__init__.py +20 -0
- algo_engine/apps/backtest/doc_server.py +328 -0
- algo_engine/apps/backtest/static/styles/dash.css +48 -0
- algo_engine/apps/backtest/templates/dash.html +20 -0
- algo_engine/apps/backtest/templates/index.html +40 -0
- algo_engine/apps/backtest/tester.py +252 -0
- algo_engine/apps/backtest/web_app.py +125 -0
- algo_engine/apps/bokeh_server.py +245 -0
- algo_engine/apps/demo/__init__.py +0 -0
- algo_engine/apps/demo/test.py +40 -0
- algo_engine/apps/sim_input/__init__.py +23 -0
- algo_engine/apps/sim_input/client.py +412 -0
- algo_engine/apps/sim_input/sim_keyboard.py +88 -0
- algo_engine/apps/sim_input/sim_mouse.py +137 -0
- algo_engine/apps/sim_input/window.py +162 -0
- algo_engine/backtest/__init__.py +19 -0
- algo_engine/backtest/__main__.py +51 -0
- algo_engine/backtest/metrics.py +179 -0
- algo_engine/backtest/replay.py +635 -0
- algo_engine/backtest/sim_match.py +506 -0
- algo_engine/base/__infra__.pxd +3 -0
- algo_engine/base/__init__.pxd +3 -0
- algo_engine/base/__init__.py +60 -0
- algo_engine/base/c_allocator_protocol.c +11608 -0
- algo_engine/base/c_allocator_protocol.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_allocator_protocol.pxd +24 -0
- algo_engine/base/c_allocator_protocol.pyi +68 -0
- algo_engine/base/c_allocator_protocol.pyx +111 -0
- algo_engine/base/c_intern_string.c +5908 -0
- algo_engine/base/c_intern_string.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_intern_string.pxd +14 -0
- algo_engine/base/c_intern_string.pyi +22 -0
- algo_engine/base/c_intern_string.pyx +17 -0
- algo_engine/base/c_market_data/__infra__.pxd +196 -0
- algo_engine/base/c_market_data/__init__.pxd +196 -0
- algo_engine/base/c_market_data/__init__.py +24 -0
- algo_engine/base/c_market_data/c_candlestick.c +18996 -0
- algo_engine/base/c_market_data/c_candlestick.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_candlestick.pxd +27 -0
- algo_engine/base/c_market_data/c_candlestick.pyi +217 -0
- algo_engine/base/c_market_data/c_candlestick.pyx +255 -0
- algo_engine/base/c_market_data/c_internal.c +14059 -0
- algo_engine/base/c_market_data/c_internal.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_internal.pxd +14 -0
- algo_engine/base/c_market_data/c_internal.pyi +47 -0
- algo_engine/base/c_market_data/c_internal.pyx +41 -0
- algo_engine/base/c_market_data/c_market_data.c +30420 -0
- algo_engine/base/c_market_data/c_market_data.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_market_data.h +1464 -0
- algo_engine/base/c_market_data/c_market_data.pxd +414 -0
- algo_engine/base/c_market_data/c_market_data.pyi +550 -0
- algo_engine/base/c_market_data/c_market_data.pyx +701 -0
- algo_engine/base/c_market_data/c_market_data_buffer.c +29407 -0
- algo_engine/base/c_market_data/c_market_data_buffer.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
- algo_engine/base/c_market_data/c_market_data_buffer.pxd +143 -0
- algo_engine/base/c_market_data/c_market_data_buffer.pyi +355 -0
- algo_engine/base/c_market_data/c_market_data_buffer.pyx +571 -0
- algo_engine/base/c_market_data/c_market_data_config.h +29 -0
- algo_engine/base/c_market_data/c_tick.c +44303 -0
- algo_engine/base/c_market_data/c_tick.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_tick.pxd +53 -0
- algo_engine/base/c_market_data/c_tick.pyi +466 -0
- algo_engine/base/c_market_data/c_tick.pyx +673 -0
- algo_engine/base/c_market_data/c_trade_utils.c +28702 -0
- algo_engine/base/c_market_data/c_trade_utils.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_trade_utils.pxd +53 -0
- algo_engine/base/c_market_data/c_trade_utils.pyi +602 -0
- algo_engine/base/c_market_data/c_trade_utils.pyx +609 -0
- algo_engine/base/c_market_data/c_transaction.c +23558 -0
- algo_engine/base/c_market_data/c_transaction.cp315-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_transaction.pxd +27 -0
- algo_engine/base/c_market_data/c_transaction.pyi +433 -0
- algo_engine/base/c_market_data/c_transaction.pyx +460 -0
- algo_engine/base/console_utils.py +1070 -0
- algo_engine/base/finance_decimal.py +258 -0
- algo_engine/base/telemetrics.py +18 -0
- algo_engine/engine/__infra__.pxd +10 -0
- algo_engine/engine/__init__.pxd +10 -0
- algo_engine/engine/__init__.py +40 -0
- algo_engine/engine/algo_engine.py +904 -0
- algo_engine/engine/c_event_engine.c +16325 -0
- algo_engine/engine/c_event_engine.cp315-win_amd64.pyd +0 -0
- algo_engine/engine/c_event_engine.pxd +25 -0
- algo_engine/engine/c_event_engine.pyi +68 -0
- algo_engine/engine/c_market_engine.c +24258 -0
- algo_engine/engine/c_market_engine.cp315-win_amd64.pyd +0 -0
- algo_engine/engine/c_market_engine.pxd +87 -0
- algo_engine/engine/c_market_engine.pyi +357 -0
- algo_engine/engine/event_engine.py +53 -0
- algo_engine/engine/trade_engine.py +2037 -0
- algo_engine/exchange_profile/__infra__.pxd +100 -0
- algo_engine/exchange_profile/__init__.pxd +100 -0
- algo_engine/exchange_profile/__init__.py +53 -0
- algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
- algo_engine/exchange_profile/c_ex_profile_base.h +1204 -0
- algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
- algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
- algo_engine/exchange_profile/c_exchange_profile.c +52375 -0
- algo_engine/exchange_profile/c_exchange_profile.cp315-win_amd64.pyd +0 -0
- algo_engine/exchange_profile/c_exchange_profile.pxd +336 -0
- algo_engine/exchange_profile/c_exchange_profile.pyi +883 -0
- algo_engine/exchange_profile/c_exchange_profile.pyx +1495 -0
- algo_engine/exchange_profile/c_profile_cn.c +7798 -0
- algo_engine/exchange_profile/c_profile_cn.cp315-win_amd64.pyd +0 -0
- algo_engine/exchange_profile/c_profile_cn.pxd +1 -0
- algo_engine/exchange_profile/c_profile_cn.pyi +3 -0
- algo_engine/exchange_profile/c_profile_cn.pyx +1 -0
- algo_engine/exchange_profile/c_profile_default.c +7798 -0
- algo_engine/exchange_profile/c_profile_default.cp315-win_amd64.pyd +0 -0
- algo_engine/exchange_profile/c_profile_default.pxd +1 -0
- algo_engine/exchange_profile/c_profile_default.pyi +3 -0
- algo_engine/exchange_profile/c_profile_default.pyx +1 -0
- algo_engine/exchange_profile/c_profile_dispatcher.c +7798 -0
- algo_engine/exchange_profile/c_profile_dispatcher.cp315-win_amd64.pyd +0 -0
- algo_engine/exchange_profile/c_profile_dispatcher.pxd +1 -0
- algo_engine/exchange_profile/c_profile_dispatcher.pyi +3 -0
- algo_engine/exchange_profile/c_profile_dispatcher.pyx +1 -0
- algo_engine/includes/algo_engine/base/c_allocator_protocol.c +11608 -0
- algo_engine/includes/algo_engine/base/c_intern_string.c +5908 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_candlestick.c +18996 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_internal.c +14059 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data.c +30420 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data.h +1464 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.c +29407 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data_config.h +29 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_tick.c +44303 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_trade_utils.c +28702 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_transaction.c +23558 -0
- algo_engine/includes/algo_engine/engine/c_event_engine.c +16325 -0
- algo_engine/includes/algo_engine/engine/c_market_engine.c +24258 -0
- algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
- algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.h +1204 -0
- algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
- algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
- algo_engine/includes/algo_engine/exchange_profile/c_exchange_profile.c +52375 -0
- algo_engine/includes/algo_engine/exchange_profile/c_profile_cn.c +7798 -0
- algo_engine/includes/algo_engine/exchange_profile/c_profile_default.c +7798 -0
- algo_engine/includes/algo_engine/exchange_profile/c_profile_dispatcher.c +7798 -0
- algo_engine/monitor/__init__.py +15 -0
- algo_engine/monitor/advanced_data_interface.py +334 -0
- algo_engine/strategy/__init__.py +44 -0
- algo_engine/strategy/strategy_engine.py +441 -0
- algo_engine/utils/__init__.py +3 -0
- algo_engine/utils/commit_regularizer.py +49 -0
- algo_engine/utils/data_utils.py +296 -0
- pyalgoengine-0.12.3.dist-info/METADATA +142 -0
- pyalgoengine-0.12.3.dist-info/RECORD +156 -0
- pyalgoengine-0.12.3.dist-info/WHEEL +5 -0
- pyalgoengine-0.12.3.dist-info/licenses/LICENSE +21 -0
- pyalgoengine-0.12.3.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
__package__ = 'algo_engine.apps.sim_input'
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import dataclasses
|
|
5
|
+
import enum
|
|
6
|
+
import os
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from . import LOGGER
|
|
10
|
+
|
|
11
|
+
LOGGER.getChild('Window')
|
|
12
|
+
|
|
13
|
+
# Define necessary Windows API functions and constants
|
|
14
|
+
user32 = ctypes.windll.user32
|
|
15
|
+
kernel32 = ctypes.windll.kernel32
|
|
16
|
+
psapi = ctypes.windll.psapi
|
|
17
|
+
|
|
18
|
+
# Define constants for access rights
|
|
19
|
+
PROCESS_QUERY_INFORMATION = 0x0400
|
|
20
|
+
PROCESS_VM_READ = 0x0010
|
|
21
|
+
|
|
22
|
+
# Define necessary Windows API function prototypes
|
|
23
|
+
WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclasses.dataclass(frozen=True)
|
|
27
|
+
class WindowInfo:
|
|
28
|
+
window_name: str
|
|
29
|
+
pid: int
|
|
30
|
+
hwnd: int
|
|
31
|
+
executable_name: str
|
|
32
|
+
executable_path: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class WindowState(enum.IntEnum):
|
|
36
|
+
SW_SHOWNORMAL = 1 # Show the window normally
|
|
37
|
+
SW_SHOWMINIMIZED = 2 # Minimize the window
|
|
38
|
+
SW_SHOWMAXIMIZED = 3 # Maximize the window
|
|
39
|
+
SW_SHOWNOACTIVATE = 4 # Show the window without activating it
|
|
40
|
+
SW_SHOW = 5 # Show the window and bring it to the foreground
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Function to get PID from window handle
|
|
44
|
+
def get_pid(hwnd):
|
|
45
|
+
pid = ctypes.c_ulong()
|
|
46
|
+
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
|
47
|
+
return pid.value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Function to retrieve the executable path and name of a process
|
|
51
|
+
def get_executable_info(pid):
|
|
52
|
+
# Open the process to get information
|
|
53
|
+
h_process = kernel32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid)
|
|
54
|
+
if not h_process:
|
|
55
|
+
return None, None
|
|
56
|
+
|
|
57
|
+
# Buffer to hold the path
|
|
58
|
+
path_buffer = ctypes.create_unicode_buffer(1024)
|
|
59
|
+
|
|
60
|
+
# Get the full executable path using GetModuleFileNameEx
|
|
61
|
+
if psapi.GetModuleFileNameExW(h_process, 0, path_buffer, ctypes.byref(ctypes.c_ulong(1024))):
|
|
62
|
+
executable_path = path_buffer.value
|
|
63
|
+
executable_name = os.path.basename(executable_path)
|
|
64
|
+
else:
|
|
65
|
+
executable_path = None
|
|
66
|
+
executable_name = None
|
|
67
|
+
|
|
68
|
+
# Close the process handle
|
|
69
|
+
kernel32.CloseHandle(h_process)
|
|
70
|
+
|
|
71
|
+
return executable_name, executable_path
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Function to check if a window is visible (including minimized)
|
|
75
|
+
def is_window_visible(hwnd):
|
|
76
|
+
return user32.IsWindowVisible(hwnd)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Function to enumerate windows
|
|
80
|
+
def get_windows() -> list[WindowInfo]:
|
|
81
|
+
windows = []
|
|
82
|
+
|
|
83
|
+
def enum_windows_proc(hwnd, _):
|
|
84
|
+
# Only include visible windows
|
|
85
|
+
if not is_window_visible(hwnd):
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
# Get window title (for name)
|
|
89
|
+
length = user32.GetWindowTextLengthW(hwnd)
|
|
90
|
+
if length > 0:
|
|
91
|
+
buffer = ctypes.create_unicode_buffer(length + 1)
|
|
92
|
+
user32.GetWindowTextW(hwnd, buffer, length + 1)
|
|
93
|
+
window_name = buffer.value
|
|
94
|
+
else:
|
|
95
|
+
window_name = "Untitled"
|
|
96
|
+
|
|
97
|
+
# Get the PID for the window
|
|
98
|
+
pid = get_pid(hwnd)
|
|
99
|
+
|
|
100
|
+
# Get executable name and path
|
|
101
|
+
executable_name, executable_path = get_executable_info(pid)
|
|
102
|
+
|
|
103
|
+
# Append to the windows list as a tuple (window_name, pid, hwnd, executable_name, executable_path)
|
|
104
|
+
windows.append(WindowInfo(window_name=window_name, pid=pid, hwnd=hwnd, executable_name=executable_name, executable_path=executable_path))
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
# Enumerate all windows (including child windows)
|
|
108
|
+
user32.EnumWindows(WNDENUMPROC(enum_windows_proc), 0)
|
|
109
|
+
|
|
110
|
+
return windows
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def find_window(name: str = None, executable: str = None) -> list[WindowInfo]:
|
|
114
|
+
windows = get_windows()
|
|
115
|
+
matched = []
|
|
116
|
+
|
|
117
|
+
for window in windows:
|
|
118
|
+
if name is not None and name.lower() not in window.name.lower():
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
if executable is not None and executable.lower() not in window.executable_name.lower():
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
matched.append(window)
|
|
125
|
+
|
|
126
|
+
return matched
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Function to set the window action (top, maximize, minimize)
|
|
130
|
+
def set_window(window_info: WindowInfo, action: Literal['top', 'maximize', 'minimize', 'max', 'min']):
|
|
131
|
+
hwnd = window_info.hwnd
|
|
132
|
+
|
|
133
|
+
match action:
|
|
134
|
+
case "top":
|
|
135
|
+
# Bring the window to the front (top)
|
|
136
|
+
user32.ShowWindow(hwnd, WindowState.SW_SHOWNORMAL)
|
|
137
|
+
user32.SetForegroundWindow(hwnd)
|
|
138
|
+
case "maximize" | 'max':
|
|
139
|
+
# Maximize the window
|
|
140
|
+
user32.ShowWindow(hwnd, WindowState.SW_SHOWMAXIMIZED)
|
|
141
|
+
case "minimize" | 'min':
|
|
142
|
+
# Minimize the window
|
|
143
|
+
user32.ShowWindow(hwnd, WindowState.SW_SHOWMINIMIZED)
|
|
144
|
+
case _:
|
|
145
|
+
raise ValueError(f"Unknown action: {action}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main():
|
|
149
|
+
# Example usage
|
|
150
|
+
windows = get_windows()
|
|
151
|
+
for _ in windows:
|
|
152
|
+
LOGGER.debug(_)
|
|
153
|
+
|
|
154
|
+
firefox = find_window(executable="firefox")[0]
|
|
155
|
+
LOGGER.info(firefox)
|
|
156
|
+
|
|
157
|
+
set_window(window_info=firefox, action='top')
|
|
158
|
+
set_window(window_info=firefox, action='maximize')
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .. import LOGGER
|
|
4
|
+
|
|
5
|
+
LOGGER = LOGGER.getChild('BackTest')
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def set_logger(logger: logging.Logger):
|
|
9
|
+
global LOGGER
|
|
10
|
+
LOGGER = logger
|
|
11
|
+
|
|
12
|
+
replay.LOGGER = LOGGER.getChild('Replay')
|
|
13
|
+
sim_match.LOGGER = LOGGER.getChild('SimMatch')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from .replay import PyDataScope, MarketDateCallable, MarketDataLoader, MarketDataBulkLoader, Replay, SimpleReplay, ProgressReplay
|
|
17
|
+
from .sim_match import SimMatch
|
|
18
|
+
|
|
19
|
+
__all__ = ['PyDataScope', 'MarketDateCallable', 'MarketDataLoader', 'MarketDataBulkLoader', 'Replay', 'SimpleReplay', 'ProgressReplay', 'SimMatch']
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
__package__ = 'algo_engine.backtest'
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
|
|
6
|
+
import event_engine
|
|
7
|
+
|
|
8
|
+
from ..engine import TOPIC, MarketDataService, Balance, RiskProfile, PositionManagementService
|
|
9
|
+
from ..engine.algo_engine import AlgoRegistry, AlgoEngine
|
|
10
|
+
from ..strategy import EventDMA
|
|
11
|
+
from ..strategy.strategy_engine import StrategyEngine
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_stop(code=0):
|
|
15
|
+
EVENT_ENGINE.stop()
|
|
16
|
+
# noinspection PyUnresolvedReferences, PyProtectedMember
|
|
17
|
+
# `import os`
|
|
18
|
+
# `os._exit(code)`
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_start(start_date: datetime.date, end_date: datetime.date, data_loader: Callable, **kwargs):
|
|
22
|
+
EVENT_ENGINE.start()
|
|
23
|
+
STRATEGY_ENGINE.back_test(
|
|
24
|
+
start_date=start_date,
|
|
25
|
+
end_date=end_date,
|
|
26
|
+
data_loader=data_loader,
|
|
27
|
+
**kwargs
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# in backtest, the global objects is newly inited to separate from production
|
|
32
|
+
EVENT_ENGINE = event_engine.EventEngine()
|
|
33
|
+
MDS = MarketDataService()
|
|
34
|
+
ALGO_REGISTRY = AlgoRegistry()
|
|
35
|
+
ALGO_ENGINE = AlgoEngine(mds=MDS, registry=ALGO_REGISTRY)
|
|
36
|
+
|
|
37
|
+
BALANCE = Balance()
|
|
38
|
+
RISK_PROFILE = RiskProfile(mds=MDS, balance=BALANCE)
|
|
39
|
+
DMA = EventDMA(event_engine=EVENT_ENGINE, mds=MDS, risk_profile=RISK_PROFILE)
|
|
40
|
+
POSITION_TRACKER = PositionManagementService(dma=DMA, algo_engine=ALGO_ENGINE)
|
|
41
|
+
STRATEGY_ENGINE = StrategyEngine(event_engine=EVENT_ENGINE, position_tracker=POSITION_TRACKER)
|
|
42
|
+
BALANCE.add(strategy=STRATEGY_ENGINE, position_tracker=POSITION_TRACKER)
|
|
43
|
+
|
|
44
|
+
EVENT_ENGINE.register_handler(topic=TOPIC.realtime, handler=MDS.on_market_data)
|
|
45
|
+
EVENT_ENGINE.register_handler(topic=TOPIC.on_report, handler=BALANCE.on_report)
|
|
46
|
+
EVENT_ENGINE.register_handler(topic=TOPIC.on_order, handler=BALANCE.on_order)
|
|
47
|
+
STRATEGY_ENGINE.register()
|
|
48
|
+
|
|
49
|
+
MDS.synthetic_orderbook = True
|
|
50
|
+
|
|
51
|
+
__all__ = ['BALANCE', 'RISK_PROFILE', 'DMA', 'POSITION_TRACKER', 'STRATEGY_ENGINE', 'BALANCE', 'EVENT_ENGINE', 'MDS']
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TradeMetrics(object):
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.trades = {}
|
|
10
|
+
self.trade_batch = []
|
|
11
|
+
|
|
12
|
+
self.exposure = 0.
|
|
13
|
+
self.total_pnl = 0.
|
|
14
|
+
self.total_cash_flow = 0.
|
|
15
|
+
|
|
16
|
+
self.current_pnl = 0.
|
|
17
|
+
self.current_cash_flow = 0.
|
|
18
|
+
self.current_trade_batch = {'cash_flow': 0., 'pnl': 0., 'turnover': 0., 'trades': []}
|
|
19
|
+
self.market_price = None
|
|
20
|
+
|
|
21
|
+
def update(self, market_price: float):
|
|
22
|
+
self.market_price = market_price
|
|
23
|
+
self.total_pnl = self.exposure * market_price + self.total_cash_flow
|
|
24
|
+
self.current_pnl = self.exposure * market_price + self.current_cash_flow
|
|
25
|
+
self.current_trade_batch['pnl'] = self.exposure * market_price + self.current_trade_batch['cash_flow']
|
|
26
|
+
|
|
27
|
+
def add_trades(self, side: int, price: float, timestamp: float, volume: float = None, trade_id: int | str = None):
|
|
28
|
+
assert side in {1, -1}, f"trade side must in {1, -1}, got {side}."
|
|
29
|
+
assert volume is None or volume >= 0, "volume must be positive."
|
|
30
|
+
|
|
31
|
+
if volume is None:
|
|
32
|
+
if self.exposure * side < 0:
|
|
33
|
+
volume = abs(self.exposure)
|
|
34
|
+
elif self.exposure * side > 0:
|
|
35
|
+
volume = 0.
|
|
36
|
+
else:
|
|
37
|
+
volume = 1.
|
|
38
|
+
|
|
39
|
+
if trade_id is None:
|
|
40
|
+
trade_id = uuid.uuid4().int
|
|
41
|
+
elif trade_id in self.trades:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
# split the trades
|
|
45
|
+
if (target_exposure := self.exposure + volume * side) * self.exposure < 0:
|
|
46
|
+
self.add_trades(side=side, volume=abs(self.exposure), price=price, timestamp=timestamp, trade_id=f'{trade_id}.0')
|
|
47
|
+
volume = volume - abs(self.exposure)
|
|
48
|
+
trade_id = f'{trade_id}.1'
|
|
49
|
+
|
|
50
|
+
self.exposure += volume * side
|
|
51
|
+
self.total_cash_flow -= volume * side * price
|
|
52
|
+
self.total_pnl = self.exposure * price + self.total_cash_flow
|
|
53
|
+
self.current_cash_flow -= volume * side * price
|
|
54
|
+
self.current_pnl = self.exposure * price + self.current_cash_flow
|
|
55
|
+
self.market_price = price
|
|
56
|
+
|
|
57
|
+
self.trades[trade_id] = trade_log = dict(
|
|
58
|
+
side=side,
|
|
59
|
+
volume=volume,
|
|
60
|
+
timestamp=timestamp,
|
|
61
|
+
price=price,
|
|
62
|
+
exposure=self.exposure,
|
|
63
|
+
cash_flow=self.current_cash_flow,
|
|
64
|
+
pnl=self.current_pnl
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if 'init_side' not in self.current_trade_batch:
|
|
68
|
+
self.current_trade_batch['init_side'] = side
|
|
69
|
+
|
|
70
|
+
self.current_trade_batch['cash_flow'] -= volume * side * price
|
|
71
|
+
self.current_trade_batch['pnl'] = self.exposure * price + self.current_trade_batch['cash_flow']
|
|
72
|
+
self.current_trade_batch['turnover'] += abs(volume) * price
|
|
73
|
+
self.current_trade_batch['trades'].append(trade_log)
|
|
74
|
+
|
|
75
|
+
if not self.exposure:
|
|
76
|
+
self.trade_batch.append(self.current_trade_batch)
|
|
77
|
+
self.current_trade_batch = {'cash_flow': 0., 'pnl': 0., 'turnover': 0., 'trades': []}
|
|
78
|
+
self.current_pnl = self.current_cash_flow = 0.
|
|
79
|
+
|
|
80
|
+
def add_trades_batch(self, trade_logs: pd.DataFrame):
|
|
81
|
+
for timestamp, row in trade_logs.iterrows(): # type: float, dict
|
|
82
|
+
side = row['side']
|
|
83
|
+
price = row['current_price']
|
|
84
|
+
volume = row['signal']
|
|
85
|
+
self.add_trades(side=side, volume=volume, price=price, timestamp=timestamp)
|
|
86
|
+
|
|
87
|
+
def clear(self):
|
|
88
|
+
self.trades.clear()
|
|
89
|
+
self.trade_batch.clear()
|
|
90
|
+
|
|
91
|
+
self.exposure = 0.
|
|
92
|
+
self.total_pnl = 0.
|
|
93
|
+
self.total_cash_flow = 0.
|
|
94
|
+
|
|
95
|
+
self.current_pnl = 0.
|
|
96
|
+
self.current_cash_flow = 0.
|
|
97
|
+
self.current_trade_batch = {'cash_flow': 0., 'pnl': 0., 'turnover': 0., 'trades': []}
|
|
98
|
+
self.market_price = None
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def summary(self):
|
|
102
|
+
info_dict = dict(
|
|
103
|
+
total_gain=0.,
|
|
104
|
+
total_loss=0.,
|
|
105
|
+
trade_count=0,
|
|
106
|
+
win_count=0,
|
|
107
|
+
lose_count=0,
|
|
108
|
+
turnover=0.,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
for trade_batch in self.trade_batch:
|
|
112
|
+
if trade_batch['pnl'] > 0:
|
|
113
|
+
info_dict['total_gain'] += trade_batch['pnl']
|
|
114
|
+
info_dict['trade_count'] += 1
|
|
115
|
+
info_dict['win_count'] += 1
|
|
116
|
+
info_dict['turnover'] += trade_batch['turnover']
|
|
117
|
+
else:
|
|
118
|
+
info_dict['total_loss'] += trade_batch['pnl']
|
|
119
|
+
info_dict['trade_count'] += 1
|
|
120
|
+
info_dict['lose_count'] += 1
|
|
121
|
+
info_dict['turnover'] += trade_batch['turnover']
|
|
122
|
+
|
|
123
|
+
info_dict['win_rate'] = info_dict['win_count'] / info_dict['trade_count'] if info_dict['trade_count'] else 0.
|
|
124
|
+
info_dict['average_gain'] = info_dict['total_gain'] / info_dict['win_count'] / self.market_price if info_dict['win_count'] else 0.
|
|
125
|
+
info_dict['average_loss'] = info_dict['total_loss'] / info_dict['lose_count'] / self.market_price if info_dict['lose_count'] else 0.
|
|
126
|
+
info_dict['gain_loss_ratio'] = -info_dict['average_gain'] / info_dict['average_loss'] if info_dict['average_loss'] else 1.
|
|
127
|
+
info_dict['long_avg_pnl'] = np.average([_['pnl'] for _ in long_trades]) / self.market_price if (long_trades := [_ for _ in self.trade_batch if _['init_side'] == 1]) else np.nan
|
|
128
|
+
info_dict['short_avg_pnl'] = np.average([_['pnl'] for _ in short_trades]) / self.market_price if (short_trades := [_ for _ in self.trade_batch if _['init_side'] == -1]) else np.nan
|
|
129
|
+
info_dict['ttl_pnl.no_leverage'] = np.sum([trade_batch['pnl'] for trade_batch in self.trade_batch])
|
|
130
|
+
info_dict['net_pnl.optimistic'] = info_dict['ttl_pnl.no_leverage'] - (0.00034 + 0.000023) / 2 * info_dict['turnover']
|
|
131
|
+
|
|
132
|
+
return info_dict
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def info(self):
|
|
136
|
+
trade_info = []
|
|
137
|
+
trade_index = []
|
|
138
|
+
for batch_id, trade_batch in enumerate(self.trade_batch):
|
|
139
|
+
for trade_id, trade_dict in enumerate(trade_batch['trades']):
|
|
140
|
+
trade_info.append(
|
|
141
|
+
dict(
|
|
142
|
+
timestamp=trade_dict['timestamp'],
|
|
143
|
+
side=trade_dict['side'],
|
|
144
|
+
volume=trade_dict['volume'],
|
|
145
|
+
price=trade_dict['price'],
|
|
146
|
+
exposure=trade_dict['exposure'],
|
|
147
|
+
pnl=trade_dict['pnl']
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
trade_index.append((f'batch.{batch_id}', f'trade.{trade_id}'))
|
|
151
|
+
|
|
152
|
+
df = pd.DataFrame(trade_info, index=trade_index)
|
|
153
|
+
return df
|
|
154
|
+
|
|
155
|
+
def to_string(self) -> str:
|
|
156
|
+
metric_info = self.summary
|
|
157
|
+
|
|
158
|
+
fmt_dict = {
|
|
159
|
+
'total_gain': f'{metric_info["total_gain"]:,.3f}',
|
|
160
|
+
'total_loss': f'{metric_info["total_loss"]:,.3f}',
|
|
161
|
+
'trade_count': f'{metric_info["trade_count"]:,}',
|
|
162
|
+
'win_count': f'{metric_info["win_count"]:,}',
|
|
163
|
+
'lose_count': f'{metric_info["lose_count"]:,}',
|
|
164
|
+
'turnover': f'{metric_info["turnover"]:,.3f}',
|
|
165
|
+
'win_rate': f'{metric_info["win_rate"]:.2%}',
|
|
166
|
+
'average_gain': f'{metric_info["average_gain"]:,.4%}',
|
|
167
|
+
'average_loss': f'{metric_info["average_loss"]:,.4%}',
|
|
168
|
+
'long_avg_pnl': f'{metric_info["long_avg_pnl"]:,.4%}',
|
|
169
|
+
'short_avg_pnl': f'{metric_info["short_avg_pnl"]:,.4%}',
|
|
170
|
+
'gain_loss_ratio': f'{metric_info["gain_loss_ratio"]:,.3%}'
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
info_str = (f'Trade Metrics Report:'
|
|
174
|
+
f'\n'
|
|
175
|
+
f'{pd.Series(fmt_dict).to_string()}'
|
|
176
|
+
f'\n'
|
|
177
|
+
f'{self.info.to_string()}')
|
|
178
|
+
|
|
179
|
+
return info_str
|