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,412 @@
|
|
|
1
|
+
__package__ = 'algo_engine.apps.sim_input'
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import time
|
|
5
|
+
import tkinter
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
|
+
from threading import Thread
|
|
8
|
+
from tkinter import ttk
|
|
9
|
+
from typing import TypedDict, NotRequired, Any
|
|
10
|
+
|
|
11
|
+
from . import LOGGER
|
|
12
|
+
|
|
13
|
+
LOGGER.getChild('Client')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Action(object):
|
|
17
|
+
class Step(TypedDict):
|
|
18
|
+
name: str
|
|
19
|
+
procedure: Callable
|
|
20
|
+
args: NotRequired[Iterable]
|
|
21
|
+
kwargs: NotRequired[dict[str, Any]]
|
|
22
|
+
comments: NotRequired[str]
|
|
23
|
+
|
|
24
|
+
def __init__(self, name: str):
|
|
25
|
+
self.name = name
|
|
26
|
+
self.steps: list[Action.Step] = []
|
|
27
|
+
|
|
28
|
+
def append(self, name: str, action: Callable, args=None, kwargs=None, comments: str = None) -> None:
|
|
29
|
+
step = Action.Step(name=name, procedure=action)
|
|
30
|
+
|
|
31
|
+
if args is not None:
|
|
32
|
+
step['args'] = args
|
|
33
|
+
|
|
34
|
+
if kwargs is not None:
|
|
35
|
+
step['kwargs'] = kwargs
|
|
36
|
+
|
|
37
|
+
if comments is not None:
|
|
38
|
+
step['comments'] = comments
|
|
39
|
+
|
|
40
|
+
self.steps.append(step)
|
|
41
|
+
|
|
42
|
+
def __call__(self, ignore_error: bool = False) -> None:
|
|
43
|
+
n = len(self.steps)
|
|
44
|
+
for i, step in enumerate(self.steps, start=1):
|
|
45
|
+
procedure = step['procedure']
|
|
46
|
+
args = step.get('args', [])
|
|
47
|
+
kwargs = step.get('kwargs', {})
|
|
48
|
+
comments = step.get('comments', '')
|
|
49
|
+
try:
|
|
50
|
+
procedure(*args, **kwargs)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
LOGGER.error(f'<Action {self.name}> <step {comments}>({i} / {n}) Failed!')
|
|
53
|
+
|
|
54
|
+
if not ignore_error:
|
|
55
|
+
raise e
|
|
56
|
+
|
|
57
|
+
LOGGER.debug(f'<Action {self.name}> <step {comments}>({i} / {n}) Completed!')
|
|
58
|
+
|
|
59
|
+
LOGGER.info(f'<Action {self.name}> completed!')
|
|
60
|
+
|
|
61
|
+
def __len__(self):
|
|
62
|
+
return len(self.steps)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AutoWorkClient(object, metaclass=abc.ABCMeta):
|
|
66
|
+
def __init__(self, root=None, **kwargs):
|
|
67
|
+
self.root = root if root is not None else tkinter.Tk()
|
|
68
|
+
self.root.title(kwargs.get('title', "Structured GUI Client"))
|
|
69
|
+
|
|
70
|
+
self.actions: dict[int, list[Action]] = {}
|
|
71
|
+
self.layout = {}
|
|
72
|
+
|
|
73
|
+
# State variables
|
|
74
|
+
self.worker_thread = None
|
|
75
|
+
self.running = False
|
|
76
|
+
self.recording = False
|
|
77
|
+
|
|
78
|
+
self.render_layout()
|
|
79
|
+
|
|
80
|
+
def render_layout(self):
|
|
81
|
+
# No fixed geometry, letting it resize
|
|
82
|
+
# self.root.geometry("600x500")
|
|
83
|
+
|
|
84
|
+
# Grid(0, 0): Create button
|
|
85
|
+
button_takeover = self.layout['button_takeover'] = ttk.Button(self.root, text="Takeover Input", command=self.toggle_auto_work)
|
|
86
|
+
button_takeover.grid(row=0, column=0, pady=10, sticky="ew")
|
|
87
|
+
|
|
88
|
+
# Grid(0, 1): Mock button
|
|
89
|
+
button_record = self.layout['button_record'] = ttk.Button(self.root, text="Record Action", command=self.record_action)
|
|
90
|
+
button_record.grid(row=0, column=1, pady=10, sticky="ew")
|
|
91
|
+
|
|
92
|
+
# Grid(1, 0): Mock button
|
|
93
|
+
button_mock = self.layout['button_mock'] = ttk.Button(self.root, text="Mock Action", command=self.toggle_mock)
|
|
94
|
+
button_mock.grid(row=1, column=0, padx=10, pady=10, sticky="ew")
|
|
95
|
+
|
|
96
|
+
# Grid(1, 1): Dropdown for action selection
|
|
97
|
+
action_selected = self.layout['action_selected'] = tkinter.StringVar()
|
|
98
|
+
action_dropdown = self.layout['action_dropdown'] = ttk.Combobox(self.root, textvariable=action_selected, state="readonly")
|
|
99
|
+
action_dropdown.grid(row=1, column=1, padx=10, pady=10, sticky="ew")
|
|
100
|
+
|
|
101
|
+
# Grid(2, 0): Create table
|
|
102
|
+
action_table = self.layout['action_table'] = ttk.Treeview(self.root, columns=("Action", "Status", "Comments"), show="tree headings")
|
|
103
|
+
action_table.heading("Action", text="Action")
|
|
104
|
+
action_table.heading("Status", text="Status")
|
|
105
|
+
action_table.heading("Comments", text="Comments")
|
|
106
|
+
action_table.grid(row=2, column=0, columnspan=2, padx=10, pady=10, sticky="nsew")
|
|
107
|
+
|
|
108
|
+
# Make the window resize properly by configuring grid weights
|
|
109
|
+
self.root.grid_columnconfigure(0, weight=1, minsize=200) # Adjust minsize for column 0
|
|
110
|
+
self.root.grid_columnconfigure(1, weight=1, minsize=200) # Adjust minsize for column 1
|
|
111
|
+
self.root.grid_rowconfigure(2, weight=1) # Make row 2 expand with the window
|
|
112
|
+
|
|
113
|
+
@abc.abstractmethod
|
|
114
|
+
def listen_signal(self) -> int:
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
def register_action(self, action: Action, signal: int):
|
|
118
|
+
self.actions.setdefault(signal, []).append(action)
|
|
119
|
+
|
|
120
|
+
# Update the dropdown with all registered actions.
|
|
121
|
+
all_actions = [f"Signal {signal} - {action.name}" for signal, actions in self.actions.items() for action in actions]
|
|
122
|
+
action_dropdown = self.layout['action_dropdown']
|
|
123
|
+
action_dropdown["values"] = all_actions
|
|
124
|
+
|
|
125
|
+
def mock_action(self, action: Action, timeout: float = 3.):
|
|
126
|
+
"""Mock the execution of an action on a transparent test ground."""
|
|
127
|
+
self.root.iconify()
|
|
128
|
+
|
|
129
|
+
# Create a borderless, transparent test ground window
|
|
130
|
+
testground = tkinter.Toplevel(self.root)
|
|
131
|
+
# testground.overrideredirect(True) # Remove all window borders and decorations
|
|
132
|
+
|
|
133
|
+
# Get screen dimensions and set the testground window to cover the entire screen
|
|
134
|
+
# screen_width = self.root.winfo_screenwidth()
|
|
135
|
+
# screen_height = self.root.winfo_screenheight()
|
|
136
|
+
# testground.geometry(f"{screen_width}x{screen_height}+0+0")
|
|
137
|
+
|
|
138
|
+
# Make the window semi-transparent and ensure it stays on top
|
|
139
|
+
testground.attributes("-alpha", 0.5) # Set transparency to 50%
|
|
140
|
+
testground.attributes("-fullscreen", True)
|
|
141
|
+
testground.attributes("-topmost", True) # Ensure it stays above other windows
|
|
142
|
+
|
|
143
|
+
# Force the window manager to render the window completely
|
|
144
|
+
testground.lift() # Bring it to the front
|
|
145
|
+
testground.focus_force() # Force focus to this window
|
|
146
|
+
|
|
147
|
+
# Add a label to display input history
|
|
148
|
+
input_history = tkinter.Listbox(testground, font=("Courier", 14))
|
|
149
|
+
input_history.pack(fill="both", expand=True, padx=20, pady=20)
|
|
150
|
+
input_history.insert("active", f"Mocking {action.name}")
|
|
151
|
+
testground.update_idletasks()
|
|
152
|
+
|
|
153
|
+
# Simulate action execution
|
|
154
|
+
for step in action.steps:
|
|
155
|
+
# Display the step name in the input history
|
|
156
|
+
input_history.insert("end", f"Executing Step: {step['name']}")
|
|
157
|
+
input_history.insert("end", f"Comments: {step.get('comments', 'No comments')}")
|
|
158
|
+
input_history.insert("end", "-" * 50)
|
|
159
|
+
input_history.see("end")
|
|
160
|
+
testground.update_idletasks()
|
|
161
|
+
|
|
162
|
+
# Simulate mouse and keyboard inputs
|
|
163
|
+
procedure = step['procedure']
|
|
164
|
+
args = step.get('args', [])
|
|
165
|
+
kwargs = step.get('kwargs', {})
|
|
166
|
+
try:
|
|
167
|
+
procedure(*args, **kwargs)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
input_history.insert("end", f"Error: {str(e)}")
|
|
170
|
+
input_history.see("end")
|
|
171
|
+
|
|
172
|
+
# Simulate delay between steps
|
|
173
|
+
|
|
174
|
+
# Wait 3 seconds, then close the test ground and restore the client window
|
|
175
|
+
input_history.insert("end", "-" * 50)
|
|
176
|
+
input_history.insert("active", f"Mocking {action.name} Completed! Exiting in {timeout} seconds...")
|
|
177
|
+
|
|
178
|
+
time.sleep(timeout)
|
|
179
|
+
testground.destroy()
|
|
180
|
+
self.root.deiconify()
|
|
181
|
+
|
|
182
|
+
def toggle_auto_work(self):
|
|
183
|
+
"""Toggle the daemon thread to start or stop auto work."""
|
|
184
|
+
if not self.running:
|
|
185
|
+
self.takeover_control()
|
|
186
|
+
else:
|
|
187
|
+
self.release_control()
|
|
188
|
+
|
|
189
|
+
def takeover_control(self):
|
|
190
|
+
if self.running:
|
|
191
|
+
LOGGER.info('Autoworker already running!')
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
self.running = True
|
|
195
|
+
self.layout['button_takeover'].config(text="Release Control")
|
|
196
|
+
self.worker_thread = Thread(target=self.auto_work, daemon=True)
|
|
197
|
+
self.worker_thread.start()
|
|
198
|
+
|
|
199
|
+
def release_control(self):
|
|
200
|
+
if not self.running:
|
|
201
|
+
LOGGER.info('Autoworker already stopped!')
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
self.running = False
|
|
205
|
+
self.layout['button_takeover'].config(text="Takeover Input")
|
|
206
|
+
|
|
207
|
+
def toggle_mock(self):
|
|
208
|
+
"""Mock the action selected in the dropdown."""
|
|
209
|
+
selected_name = self.layout['action_selected'].get()
|
|
210
|
+
if not selected_name:
|
|
211
|
+
LOGGER.warning("No action selected to mock!")
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
selected_action = None
|
|
215
|
+
for signal, actions in self.actions.items():
|
|
216
|
+
for action in actions:
|
|
217
|
+
if f"Signal {signal} - {action.name}" == selected_name:
|
|
218
|
+
selected_action = action
|
|
219
|
+
break
|
|
220
|
+
|
|
221
|
+
if selected_action is None:
|
|
222
|
+
LOGGER.warning(f"Action '{selected_name}' not found!")
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
self.mock_action(action=selected_action)
|
|
226
|
+
|
|
227
|
+
def auto_work(self):
|
|
228
|
+
"""Generate data for the table in a loop."""
|
|
229
|
+
while self.running:
|
|
230
|
+
signal = self.listen_signal()
|
|
231
|
+
actions = self.actions.get(signal, [])
|
|
232
|
+
|
|
233
|
+
# Update table for the current signal
|
|
234
|
+
self.update_table(actions)
|
|
235
|
+
|
|
236
|
+
for action_idx, action in enumerate(actions):
|
|
237
|
+
if not self.running:
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
# Update action status to "Executing"
|
|
241
|
+
self.update_status(parent_id=action_idx, status="Executing")
|
|
242
|
+
action_done = False
|
|
243
|
+
|
|
244
|
+
for step_idx, step in enumerate(action.steps):
|
|
245
|
+
if not self.running:
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
# Update step status to "Executing"
|
|
249
|
+
self.update_status(parent_id=action_idx, child_id=step_idx, status="Executing")
|
|
250
|
+
|
|
251
|
+
# Execute the step
|
|
252
|
+
step["procedure"](*step.get("args", []), **step.get("kwargs", {}))
|
|
253
|
+
|
|
254
|
+
# Mark step as "Done"
|
|
255
|
+
self.update_status(parent_id=action_idx, child_id=step_idx, status="Done")
|
|
256
|
+
else:
|
|
257
|
+
# Mark action as "Done" after all steps
|
|
258
|
+
self.update_status(parent_id=action_idx, status="Done")
|
|
259
|
+
action_done = True
|
|
260
|
+
|
|
261
|
+
if not action_done:
|
|
262
|
+
self.update_status(parent_id=action_idx, status="Stopped")
|
|
263
|
+
|
|
264
|
+
def update_table(self, actions: list[Action]):
|
|
265
|
+
"""Render the table based on the provided actions."""
|
|
266
|
+
action_table = self.layout['action_table']
|
|
267
|
+
action_table.delete(*action_table.get_children()) # Clear existing rows
|
|
268
|
+
|
|
269
|
+
for action_idx, action in enumerate(actions):
|
|
270
|
+
prefix = chr(0x250C)
|
|
271
|
+
# Insert the parent row
|
|
272
|
+
parent_id = action_table.insert(
|
|
273
|
+
"", "end", iid=action_idx, values=(f"{prefix} {action.name}", "Pending", "")
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Insert child rows for steps
|
|
277
|
+
for step_idx, step in enumerate(action.steps):
|
|
278
|
+
prefix = f'{chr(0x251C) if step_idx < len(action.steps) - 1 else chr(0x2514)}{chr(0x2500)}'
|
|
279
|
+
action_table.insert(
|
|
280
|
+
parent_id,
|
|
281
|
+
"end",
|
|
282
|
+
iid=f"{action_idx}-{step_idx}",
|
|
283
|
+
text=f"Step {step_idx + 1}",
|
|
284
|
+
values=(f"{prefix} Step {step_idx + 1}", "Pending", step.get("comments", "")),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Expand parent row by default
|
|
288
|
+
action_table.item(parent_id, open=True)
|
|
289
|
+
|
|
290
|
+
# Auto-adjust column widths
|
|
291
|
+
self.adjust_column_widths()
|
|
292
|
+
|
|
293
|
+
def update_status(self, parent_id: int, child_id: int = None, status: str = "Pending"):
|
|
294
|
+
"""Update the status of the specified row."""
|
|
295
|
+
row_id = f"{parent_id}" if child_id is None else f"{parent_id}-{child_id}"
|
|
296
|
+
action_table = self.layout['action_table']
|
|
297
|
+
values = action_table.item(row_id, "values")
|
|
298
|
+
|
|
299
|
+
# Select the row if the status is "Executing"
|
|
300
|
+
match status:
|
|
301
|
+
case "Executing":
|
|
302
|
+
action_table.selection_set(row_id)
|
|
303
|
+
action_table.see(row_id) # Ensure the row is visible
|
|
304
|
+
|
|
305
|
+
action_table.item(row_id, values=(values[0], status, values[2]))
|
|
306
|
+
|
|
307
|
+
def adjust_column_widths(self):
|
|
308
|
+
"""Automatically adjust column widths based on content."""
|
|
309
|
+
action_table = self.layout['action_table']
|
|
310
|
+
action_table.column("#0", width=30, minwidth=30, stretch=False)
|
|
311
|
+
|
|
312
|
+
for col in action_table["columns"]:
|
|
313
|
+
max_width = max(
|
|
314
|
+
[len(str(action_table.set(item, col))) for item in action_table.get_children()] + [len(col)]
|
|
315
|
+
)
|
|
316
|
+
action_table.column(col, width=max_width * 10, minwidth=30, stretch=True) # Scale width for readability
|
|
317
|
+
|
|
318
|
+
def record_action(self):
|
|
319
|
+
"""Capture and log mouse and keyboard events in the test ground."""
|
|
320
|
+
self.root.iconify()
|
|
321
|
+
|
|
322
|
+
# Create a semi-transparent test ground window
|
|
323
|
+
testground = tkinter.Toplevel(self.root)
|
|
324
|
+
testground.attributes("-alpha", 0.5) # Set transparency
|
|
325
|
+
testground.attributes("-fullscreen", True)
|
|
326
|
+
testground.attributes("-topmost", True)
|
|
327
|
+
testground.lift()
|
|
328
|
+
testground.focus_force()
|
|
329
|
+
|
|
330
|
+
# Listbox to display recorded events
|
|
331
|
+
event_log = tkinter.Listbox(testground, font=("Courier", 14), selectmode="none", state=tkinter.DISABLED)
|
|
332
|
+
# event_log.bindtags((event_log, self.root, "all"))
|
|
333
|
+
event_log.pack(fill="both", expand=True, padx=0, pady=0)
|
|
334
|
+
|
|
335
|
+
def log_event(event_type, event):
|
|
336
|
+
"""Log the event details to the console and the event log."""
|
|
337
|
+
if event_type == "Key":
|
|
338
|
+
event_details = f"{event_type}: {event.keysym} (char: {event.char})"
|
|
339
|
+
else:
|
|
340
|
+
event_details = f"{event_type}: Button-{event.num} @ ({event.x}, {event.y})"
|
|
341
|
+
LOGGER.info(event_details)
|
|
342
|
+
event_log.configure(state=tkinter.NORMAL)
|
|
343
|
+
event_log.insert("end", event_details)
|
|
344
|
+
event_log.see("end")
|
|
345
|
+
event_log.configure(state=tkinter.DISABLED)
|
|
346
|
+
|
|
347
|
+
# Bind events for mouse clicks, double-clicks, and keyboard inputs
|
|
348
|
+
testground.bind("<Button-1>", lambda e: log_event("Click", e))
|
|
349
|
+
testground.bind("<Double-1>", lambda e: log_event("Double Click", e))
|
|
350
|
+
testground.bind("<Button-3>", lambda e: log_event("Right Click", e))
|
|
351
|
+
testground.bind("<Key>", lambda e: log_event("Key", e))
|
|
352
|
+
|
|
353
|
+
# Exit recording when the ESC key is pressed
|
|
354
|
+
def exit_record(event):
|
|
355
|
+
if event.keysym == "Escape":
|
|
356
|
+
LOGGER.info("Exiting recording mode.")
|
|
357
|
+
testground.destroy()
|
|
358
|
+
self.root.deiconify()
|
|
359
|
+
|
|
360
|
+
testground.bind("<Escape>", exit_record)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class ExampleClient(AutoWorkClient):
|
|
364
|
+
"""An example implementation of the AutoWorkClient."""
|
|
365
|
+
dummy_signal = 0
|
|
366
|
+
is_init = False
|
|
367
|
+
|
|
368
|
+
@staticmethod
|
|
369
|
+
def dummy_action(name: str, msg: str) -> None:
|
|
370
|
+
LOGGER.info(f'start working on {name}, {msg}')
|
|
371
|
+
time.sleep(2)
|
|
372
|
+
LOGGER.info('working completed!')
|
|
373
|
+
|
|
374
|
+
def listen_signal(self) -> int:
|
|
375
|
+
"""Simulate listening for a signal (e.g., return random signal)."""
|
|
376
|
+
import random
|
|
377
|
+
if self.is_init:
|
|
378
|
+
delay = 5 + 5 * random.random()
|
|
379
|
+
time.sleep(delay)
|
|
380
|
+
self.dummy_signal += 1
|
|
381
|
+
self.is_init = True
|
|
382
|
+
return 1 + self.dummy_signal % 2
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def main():
|
|
386
|
+
client = ExampleClient()
|
|
387
|
+
|
|
388
|
+
# Example usage: Register actions for signal 1
|
|
389
|
+
action1 = Action("Example Action 1")
|
|
390
|
+
action1.append(name='S1A1s1', action=ExampleClient.dummy_action, args=("Signal 1 Action 1", "Step 1"))
|
|
391
|
+
action1.append(name='S1A1s2', action=ExampleClient.dummy_action, args=("Signal 1 Action 1", "Step 2"))
|
|
392
|
+
|
|
393
|
+
action2 = Action("Example Action 2")
|
|
394
|
+
action2.append(name='S1A2s1', action=ExampleClient.dummy_action, args=("Signal 1 Action 2", "Step 1"))
|
|
395
|
+
action2.append(name='S1A2s2', action=ExampleClient.dummy_action, args=("Signal 1 Action 2", "Step 2"))
|
|
396
|
+
|
|
397
|
+
client.register_action(action1, signal=1)
|
|
398
|
+
client.register_action(action2, signal=1)
|
|
399
|
+
|
|
400
|
+
action3 = Action("Example Action 3")
|
|
401
|
+
action3.append(name='S2A1s1', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 1"))
|
|
402
|
+
action3.append(name='S2A1s2', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 2"))
|
|
403
|
+
action3.append(name='S2A1s3', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 3"))
|
|
404
|
+
|
|
405
|
+
client.register_action(action3, signal=2)
|
|
406
|
+
|
|
407
|
+
client.root.mainloop()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
if __name__ == "__main__":
|
|
411
|
+
t = Thread(target=main, daemon=False)
|
|
412
|
+
t.start()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
__package__ = 'algo_engine.apps.sim_input'
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import enum
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from . import LOGGER, check_windows_version
|
|
8
|
+
|
|
9
|
+
check_windows_version((6, 1))
|
|
10
|
+
LOGGER.getChild('Keyboard')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Constants for key event types
|
|
14
|
+
class KeyEvent(enum.IntEnum):
|
|
15
|
+
F_KEYDOWN = 0x0000
|
|
16
|
+
F_KEYUP = 0x0002
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Windows API functions
|
|
20
|
+
user32 = ctypes.windll.user32
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Map character to virtual key code and shift state
|
|
24
|
+
def get_keycode(char: str):
|
|
25
|
+
"""
|
|
26
|
+
Get the virtual key code (VK_CODE) and shift state for a given character.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
tuple: (VK_CODE, needs_shift)
|
|
30
|
+
"""
|
|
31
|
+
vk_combo = user32.VkKeyScanW(ord(char))
|
|
32
|
+
vk_code = vk_combo & 0xFF # Lower byte is VK_CODE
|
|
33
|
+
shift_state = (vk_combo >> 8) & 0xFF # Higher byte indicates shift state
|
|
34
|
+
needs_shift = shift_state & 0x01 # Check if Shift key is required
|
|
35
|
+
return vk_code, needs_shift
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Simulate key press
|
|
39
|
+
def press_key(vk_code):
|
|
40
|
+
"""Press a key using its VK_CODE."""
|
|
41
|
+
ctypes.windll.user32.keybd_event(vk_code, 0, KeyEvent.F_KEYDOWN, 0)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Simulate key release
|
|
45
|
+
def release_key(vk_code):
|
|
46
|
+
"""Release a key using its VK_CODE."""
|
|
47
|
+
ctypes.windll.user32.keybd_event(vk_code, 0, KeyEvent.F_KEYUP, 0)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Advanced function to simulate a key press for a given duration
|
|
51
|
+
def simulate_keypress(char, ts=0.1):
|
|
52
|
+
"""
|
|
53
|
+
Simulate pressing a key for a given duration.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
char (str): The character to simulate key press for.
|
|
57
|
+
ts (float): The duration (in seconds) to hold the key. Default is 0.1s.
|
|
58
|
+
"""
|
|
59
|
+
vk_code, needs_shift = get_keycode(char) # Get VK_CODE and shift state
|
|
60
|
+
if vk_code == 0xFF:
|
|
61
|
+
raise ValueError(f"Invalid character '{char}'. Cannot map to a virtual key code.")
|
|
62
|
+
|
|
63
|
+
# If the character needs Shift, press Shift first
|
|
64
|
+
if needs_shift:
|
|
65
|
+
press_key(0x10) # VK_CODE for Shift
|
|
66
|
+
|
|
67
|
+
press_key(vk_code) # Press the key
|
|
68
|
+
time.sleep(ts) # Hold the key for the given duration
|
|
69
|
+
release_key(vk_code) # Release the key
|
|
70
|
+
|
|
71
|
+
# If Shift was pressed, release it
|
|
72
|
+
if needs_shift:
|
|
73
|
+
release_key(0x10) # VK_CODE for Shift
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main():
|
|
77
|
+
LOGGER.info('Staring sim input sequence in 5 seconds...')
|
|
78
|
+
time.sleep(5)
|
|
79
|
+
key_strikes = list('hellow world!')
|
|
80
|
+
for char in key_strikes:
|
|
81
|
+
LOGGER.info(f'Pressing {char}...')
|
|
82
|
+
simulate_keypress(char=char, ts=.5)
|
|
83
|
+
time.sleep(.5)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# Example usage
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
__package__ = 'algo_engine.apps.sim_input'
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import enum
|
|
5
|
+
import time
|
|
6
|
+
from ctypes import wintypes
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from . import LOGGER, check_windows_version
|
|
10
|
+
|
|
11
|
+
check_windows_version((6, 1))
|
|
12
|
+
LOGGER.getChild('Mouse')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Constants for mouse events
|
|
16
|
+
class MouseEvent(enum.IntEnum):
|
|
17
|
+
MOUSEEVENTF_MOVE = 0x0001
|
|
18
|
+
MOUSEEVENTF_LEFTDOWN = 0x0002
|
|
19
|
+
MOUSEEVENTF_LEFTUP = 0x0004
|
|
20
|
+
MOUSEEVENTF_RIGHTDOWN = 0x0008
|
|
21
|
+
MOUSEEVENTF_RIGHTUP = 0x0010
|
|
22
|
+
MOUSEEVENTF_ABSOLUTE = 0x8000
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Structure to store point coordinates
|
|
26
|
+
class POINT(ctypes.Structure):
|
|
27
|
+
_fields_ = [("x", wintypes.LONG), ("y", wintypes.LONG)]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Windows API functions
|
|
31
|
+
user32 = ctypes.windll.user32
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Get the current mouse location
|
|
35
|
+
def get_location() -> POINT:
|
|
36
|
+
"""
|
|
37
|
+
Get the current mouse cursor position.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
tuple: (x, y) coordinates of the mouse cursor.
|
|
41
|
+
"""
|
|
42
|
+
point = POINT()
|
|
43
|
+
user32.GetCursorPos(ctypes.byref(point))
|
|
44
|
+
return point
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def move_mouse(x: int, y: int):
|
|
48
|
+
"""
|
|
49
|
+
Move the mouse cursor to a specific screen location.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
x (int): The target x-coordinate.
|
|
53
|
+
y (int): The target y-coordinate.
|
|
54
|
+
"""
|
|
55
|
+
user32.SetCursorPos(x, y)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Simulate mouse press
|
|
59
|
+
def press_mouse(button: Literal['l', 'r', 'left', 'right'] = 'l'):
|
|
60
|
+
"""
|
|
61
|
+
Simulate a mouse button press.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
button (str): The button to press ("left" or "right").
|
|
65
|
+
"""
|
|
66
|
+
match button:
|
|
67
|
+
case 'l' | "left":
|
|
68
|
+
user32.mouse_event(MouseEvent.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
|
|
69
|
+
case 'r' | "right":
|
|
70
|
+
user32.mouse_event(MouseEvent.MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0)
|
|
71
|
+
case _:
|
|
72
|
+
raise ValueError("Invalid button. Use 'left' or 'right'.")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Simulate mouse release
|
|
76
|
+
def release_mouse(button: Literal['l', 'r', 'left', 'right'] = 'l'):
|
|
77
|
+
"""
|
|
78
|
+
Simulate a mouse button release.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
button (str): The button to release ("left" or "right").
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
match button:
|
|
85
|
+
case 'l' | "left":
|
|
86
|
+
user32.mouse_event(MouseEvent.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
|
|
87
|
+
case 'r' | "right":
|
|
88
|
+
user32.mouse_event(MouseEvent.MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0)
|
|
89
|
+
case _:
|
|
90
|
+
raise ValueError("Invalid button. Use 'left' or 'right'.")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Simulate a mouse click
|
|
94
|
+
def click_mouse(button: Literal['l', 'r', 'left', 'right'] = 'l'):
|
|
95
|
+
"""
|
|
96
|
+
Simulate a mouse click (press + release).
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
button (str): The button to click ("left" or "right").
|
|
100
|
+
"""
|
|
101
|
+
press_mouse(button)
|
|
102
|
+
release_mouse(button)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Simulate a mouse double click
|
|
106
|
+
def double_click_mouse(button: Literal['l', 'r', 'left', 'right'] = 'l', interval: float = 0.1):
|
|
107
|
+
"""
|
|
108
|
+
Simulate a mouse double click.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
button (str): The button to double-click ("left" or "right").
|
|
112
|
+
interval (float): Time between clicks (in seconds). Default is 0.1s.
|
|
113
|
+
"""
|
|
114
|
+
click_mouse(button)
|
|
115
|
+
time.sleep(interval)
|
|
116
|
+
click_mouse(button)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Example usage
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
LOGGER.info('Staring sim mouse input sequence in 5 seconds...')
|
|
122
|
+
time.sleep(5)
|
|
123
|
+
|
|
124
|
+
p = get_location()
|
|
125
|
+
LOGGER.info(f"Current mouse location: {p.x, p.y}")
|
|
126
|
+
|
|
127
|
+
LOGGER.info('Move to diagonal location...')
|
|
128
|
+
move_mouse(x=p.y, y=p.x)
|
|
129
|
+
|
|
130
|
+
LOGGER.info(f"Simulating left click...")
|
|
131
|
+
click_mouse(button="l")
|
|
132
|
+
|
|
133
|
+
LOGGER.info(f"Simulating right click...")
|
|
134
|
+
click_mouse(button="r")
|
|
135
|
+
|
|
136
|
+
LOGGER.info("Simulating double click...")
|
|
137
|
+
double_click_mouse()
|