PyAlgoEngine 0.7.6.post7__tar.gz → 0.7.7__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.
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/PKG-INFO +1 -1
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/PyAlgoEngine.egg-info/PKG-INFO +1 -1
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/__init__.py +1 -1
- pyalgoengine-0.7.7/algo_engine/apps/sim_input/client.py +412 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/backtest/replay.py +17 -4
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/backtest/sim_match.py +50 -12
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/market_utils_posix.py +7 -1
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/engine/market_engine.py +1 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/setup.py +35 -0
- pyalgoengine-0.7.6.post7/algo_engine/apps/sim_input/client.py +0 -253
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/LICENSE +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/PyAlgoEngine.egg-info/SOURCES.txt +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/PyAlgoEngine.egg-info/dependency_links.txt +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/PyAlgoEngine.egg-info/requires.txt +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/PyAlgoEngine.egg-info/top_level.txt +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/README.md +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/__init__.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/backtest/__init__.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/backtest/doc_server.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/backtest/tester.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/backtest/web_app.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/bokeh_server.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/demo/__init__.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/demo/test.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/sim_input/__init__.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/sim_input/sim_keyboard.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/sim_input/sim_mouse.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/sim_input/window.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/backtest/__init__.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/backtest/__main__.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/backtest/metrics.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/__init__.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/console_utils.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/finance_decimal.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/market_buffer.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/market_utils.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/market_utils_nt.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/technical_analysis.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/telemetrics.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/trade_utils.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/engine/__init__.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/engine/algo_engine.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/engine/event_engine.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/engine/trade_engine.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/monitor/__init__.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/monitor/advanced_data_interface.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/profile/__init__.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/profile/cn.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/strategy/__init__.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/strategy/strategy_engine.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/utils/__init__.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/utils/commit_regularizer.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/utils/data_utils.py +0 -0
- {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/setup.cfg +0 -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()
|
|
@@ -107,6 +107,7 @@ class ProgressiveReplay(Replay):
|
|
|
107
107
|
**kwargs
|
|
108
108
|
):
|
|
109
109
|
self.loader = loader
|
|
110
|
+
self.market_date: datetime.date | None = kwargs.pop('market_date', None)
|
|
110
111
|
self.start_date: datetime.date | None = kwargs.pop('start_date', None)
|
|
111
112
|
self.end_date: datetime.date | None = kwargs.pop('end_date', None)
|
|
112
113
|
self.calendar: list[datetime.date] | None = kwargs.pop('calendar', None)
|
|
@@ -117,6 +118,7 @@ class ProgressiveReplay(Replay):
|
|
|
117
118
|
self.replay_subscription = {}
|
|
118
119
|
self.replay_calendar = []
|
|
119
120
|
self.replay_task = []
|
|
121
|
+
self.replay_status = {}
|
|
120
122
|
|
|
121
123
|
self.date_progress = 0
|
|
122
124
|
self.task_progress = 0
|
|
@@ -182,8 +184,16 @@ class ProgressiveReplay(Replay):
|
|
|
182
184
|
else:
|
|
183
185
|
self.replay_calendar = self.calendar
|
|
184
186
|
|
|
187
|
+
if self.market_date is None:
|
|
188
|
+
self.market_date = self.replay_calendar[0] if self.replay_calendar else self.start_date
|
|
189
|
+
else:
|
|
190
|
+
date_to_replay = [_ for _ in self.replay_calendar if _ >= self.market_date]
|
|
191
|
+
self.market_date = date_to_replay[0] if date_to_replay else self.end_date
|
|
192
|
+
|
|
193
|
+
self.replay_status = {market_date: 'skipped' if market_date < self.market_date else 'idle' for market_date in self.replay_calendar}
|
|
194
|
+
|
|
185
195
|
self.task_progress = 0
|
|
186
|
-
self.date_progress = sum([1 for _ in self.replay_calendar if _ < self.
|
|
196
|
+
self.date_progress = sum([1 for _ in self.replay_calendar if _ < self.market_date])
|
|
187
197
|
self.progress.reset()
|
|
188
198
|
|
|
189
199
|
if self.date_progress:
|
|
@@ -191,7 +201,8 @@ class ProgressiveReplay(Replay):
|
|
|
191
201
|
|
|
192
202
|
def next_trade_day(self):
|
|
193
203
|
if self.date_progress < len(self.replay_calendar):
|
|
194
|
-
market_date = self.replay_calendar[self.date_progress]
|
|
204
|
+
market_date = self.market_date = self.replay_calendar[self.date_progress]
|
|
205
|
+
self.replay_status[market_date] = 'started'
|
|
195
206
|
self.progress.prompt = f'Replay {market_date:%Y-%m-%d} ({self.date_progress + 1} / {len(self.replay_calendar)}):'
|
|
196
207
|
for topic in self.replay_subscription:
|
|
197
208
|
ticker, dtype = self.replay_subscription[topic]
|
|
@@ -214,8 +225,9 @@ class ProgressiveReplay(Replay):
|
|
|
214
225
|
data = self.replay_task[self.task_progress]
|
|
215
226
|
self.task_progress += 1
|
|
216
227
|
else:
|
|
217
|
-
if self.eod is not None and self.
|
|
218
|
-
self.eod(market_date=self.
|
|
228
|
+
if self.eod is not None and self.replay_status[self.market_date] == 'started':
|
|
229
|
+
self.eod(market_date=self.market_date, replay=self)
|
|
230
|
+
self.replay_status[self.market_date] = 'done'
|
|
219
231
|
|
|
220
232
|
self.replay_task.clear()
|
|
221
233
|
self.task_progress = 0
|
|
@@ -223,6 +235,7 @@ class ProgressiveReplay(Replay):
|
|
|
223
235
|
if self.bod is not None and self.date_progress < len(self.replay_calendar):
|
|
224
236
|
self.bod(market_date=self.replay_calendar[self.date_progress], replay=self)
|
|
225
237
|
|
|
238
|
+
# this is by designed, to load the new data after the bod is done.
|
|
226
239
|
self.next_trade_day()
|
|
227
240
|
|
|
228
241
|
# the bod process should be moved here!
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
3
5
|
from . import LOGGER
|
|
4
|
-
from ..base import OrderType, MarketData, BarData, TradeData, TickData, OrderState, OrderBook, TradeReport, TradeInstruction
|
|
6
|
+
from ..base import OrderType, MarketData, BarData, TradeData, TickData, OrderState, OrderBook, TradeReport, TradeInstruction, TransactionSide
|
|
5
7
|
from ..engine.event_engine import TOPIC, EVENT_ENGINE
|
|
6
8
|
from ..profile import PROFILE
|
|
7
9
|
|
|
@@ -9,17 +11,26 @@ LOGGER = LOGGER.getChild('SimMatch')
|
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class SimMatch(object):
|
|
12
|
-
def __init__(self, ticker,
|
|
14
|
+
def __init__(self, ticker, event_engine=None, topic_set=None, **kwargs):
|
|
13
15
|
self.ticker = ticker
|
|
14
|
-
self.instant_fill = instant_fill
|
|
15
16
|
self.event_engine = event_engine if event_engine is not None else EVENT_ENGINE
|
|
16
17
|
self.topic_set = topic_set if topic_set is not None else TOPIC
|
|
17
|
-
self.fee_rate = fee_rate
|
|
18
|
+
self.fee_rate = kwargs.get('fee_rate', 0.)
|
|
18
19
|
|
|
19
20
|
self.working: dict[str, TradeInstruction] = {}
|
|
20
21
|
self.history: dict[str, TradeInstruction] = {}
|
|
21
22
|
|
|
22
23
|
self.timestamp = 0.
|
|
24
|
+
self.last_price = None
|
|
25
|
+
self.matching_config = {
|
|
26
|
+
'instant_fill': kwargs.get('instant_fill', False),
|
|
27
|
+
'lag': {
|
|
28
|
+
'ts': kwargs.get('lag_ts', 0.),
|
|
29
|
+
'n_transaction': kwargs.get('lag_n_transaction', 0)
|
|
30
|
+
},
|
|
31
|
+
'hit_prob': kwargs.get('hit_prob', 1.), # affecting FoK
|
|
32
|
+
'slippery_rate': kwargs.get('slippery_rate', 0.0001)
|
|
33
|
+
}
|
|
23
34
|
|
|
24
35
|
def __call__(self, **kwargs):
|
|
25
36
|
order: TradeInstruction | None = kwargs.pop('order', None)
|
|
@@ -35,6 +46,7 @@ class SimMatch(object):
|
|
|
35
46
|
|
|
36
47
|
if market_data is not None:
|
|
37
48
|
self.timestamp = market_data.timestamp
|
|
49
|
+
self.last_price = market_data.market_price
|
|
38
50
|
|
|
39
51
|
if isinstance(market_data, BarData):
|
|
40
52
|
self._check_bar_data(market_data=market_data)
|
|
@@ -69,17 +81,33 @@ class SimMatch(object):
|
|
|
69
81
|
# raise ValueError(f'Invalid instruction {order}, instruction must have a LimitPrice')
|
|
70
82
|
|
|
71
83
|
order.set_order_state(order_state=OrderState.Placed, timestamp=self.timestamp)
|
|
84
|
+
short_circuit = self._check_short_circuit(order=order)
|
|
72
85
|
|
|
73
|
-
if
|
|
74
|
-
self.
|
|
86
|
+
if short_circuit:
|
|
87
|
+
self.on_order(order=order, **kwargs)
|
|
88
|
+
# in short circuit mode, the worst price will be applied.
|
|
89
|
+
self._match(order=order, match_price=self.worst_price(order.limit_price, self.last_price, side=order.side))
|
|
75
90
|
|
|
91
|
+
self.working[order.order_id] = order
|
|
76
92
|
self.on_order(order=order, **kwargs)
|
|
77
93
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
@classmethod
|
|
95
|
+
def best_price(cls, *price, side: TransactionSide | int):
|
|
96
|
+
if side > 0:
|
|
97
|
+
return min(_ for _ in price if _ is not None and np.isfinite(_))
|
|
98
|
+
elif side < 0:
|
|
99
|
+
return max(_ for _ in price if _ is not None and np.isfinite(_))
|
|
100
|
+
|
|
101
|
+
raise ValueError(f'Invalid side {side}!')
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def worst_price(cls, *price, side: TransactionSide | int):
|
|
105
|
+
if side > 0:
|
|
106
|
+
return max(_ for _ in price if _ is not None and np.isfinite(_))
|
|
107
|
+
elif side < 0:
|
|
108
|
+
return min(_ for _ in price if _ is not None and np.isfinite(_))
|
|
109
|
+
|
|
110
|
+
raise ValueError(f'Invalid side {side}!')
|
|
83
111
|
|
|
84
112
|
def cancel_order(self, order: TradeInstruction = None, order_id: str = None, **kwargs):
|
|
85
113
|
if order is None and order_id is None:
|
|
@@ -104,6 +132,15 @@ class SimMatch(object):
|
|
|
104
132
|
self.history[order_id] = order
|
|
105
133
|
self.on_order(order=order, **kwargs)
|
|
106
134
|
|
|
135
|
+
def _check_short_circuit(self, order: TradeInstruction, **kwargs):
|
|
136
|
+
if order.limit_price is None and self.last_price is None:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
if self.matching_config['instant_fill'] and all(not _ for _ in self.matching_config['lag'].values()):
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
return False
|
|
143
|
+
|
|
107
144
|
def _check_bar_data(self, market_data: BarData):
|
|
108
145
|
for order_id in list(self.working):
|
|
109
146
|
order = self.working.get(order_id)
|
|
@@ -285,10 +322,11 @@ class SimMatch(object):
|
|
|
285
322
|
self.cancel_order(order_id=order_id)
|
|
286
323
|
|
|
287
324
|
def clear(self):
|
|
288
|
-
# self.fee_rate = 0.
|
|
289
325
|
self.working.clear()
|
|
290
326
|
self.history.clear()
|
|
327
|
+
|
|
291
328
|
self.timestamp = 0.
|
|
329
|
+
self.last_price = None
|
|
292
330
|
|
|
293
331
|
@property
|
|
294
332
|
def market_time(self) -> datetime.datetime:
|
|
@@ -2,6 +2,7 @@ import abc
|
|
|
2
2
|
import ctypes
|
|
3
3
|
import datetime
|
|
4
4
|
import enum
|
|
5
|
+
import inspect
|
|
5
6
|
import json
|
|
6
7
|
import math
|
|
7
8
|
import re
|
|
@@ -159,7 +160,12 @@ class TransactionSide(enum.IntEnum):
|
|
|
159
160
|
elif self.value == 0:
|
|
160
161
|
return 0
|
|
161
162
|
else:
|
|
162
|
-
|
|
163
|
+
frame = inspect.currentframe()
|
|
164
|
+
caller = inspect.getframeinfo(frame.f_back)
|
|
165
|
+
LOGGER.warning(
|
|
166
|
+
f"Requesting .sign of {self.name} is not recommended, use .order_sign instead. "
|
|
167
|
+
f"Called from {caller.filename}, line {caller.lineno}."
|
|
168
|
+
)
|
|
163
169
|
return self.order_sign
|
|
164
170
|
|
|
165
171
|
@property
|
|
@@ -189,6 +189,7 @@ class MarketDataService(object, metaclass=Singleton):
|
|
|
189
189
|
def add_monitor(self, monitor: MarketDataMonitor):
|
|
190
190
|
self.monitor[monitor.monitor_id] = monitor
|
|
191
191
|
self.monitor_manager.add_monitor(monitor)
|
|
192
|
+
monitor.mds = self
|
|
192
193
|
|
|
193
194
|
def pop_monitor(self, monitor: MarketDataMonitor = None, monitor_id: str = None, monitor_name: str = None):
|
|
194
195
|
if monitor_id is not None:
|
|
@@ -2,6 +2,8 @@ import codecs
|
|
|
2
2
|
import os
|
|
3
3
|
|
|
4
4
|
import setuptools
|
|
5
|
+
from setuptools import Extension
|
|
6
|
+
from setuptools.command.build_ext import build_ext
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
def read(rel_path):
|
|
@@ -21,6 +23,32 @@ def get_version(rel_path):
|
|
|
21
23
|
raise RuntimeError("Unable to find version string.")
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
class BuildExtWithFallback(build_ext):
|
|
27
|
+
"""Custom build_ext to handle Cython compilation with fallback."""
|
|
28
|
+
|
|
29
|
+
def run(self):
|
|
30
|
+
try:
|
|
31
|
+
print("Attempting to compile Cython modules...")
|
|
32
|
+
super().run()
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print("Cython compilation failed:", e)
|
|
35
|
+
print("Falling back to pure Python implementation.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Skip building ext_modules in CI
|
|
39
|
+
if os.getenv('GITHUB_ACTIONS') == 'true':
|
|
40
|
+
print("Skipping ext_modules as we're in a CI environment.")
|
|
41
|
+
ext_modules = []
|
|
42
|
+
else:
|
|
43
|
+
# Define Cython extension (use the .pyx file)
|
|
44
|
+
ext_modules = [
|
|
45
|
+
Extension(
|
|
46
|
+
"algo_engine.base.market_utils_posix",
|
|
47
|
+
["algo_engine/base/market_utils_posix.pyx"],
|
|
48
|
+
)
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
24
52
|
long_description = read("README.md")
|
|
25
53
|
|
|
26
54
|
setuptools.setup(
|
|
@@ -58,6 +86,8 @@ setuptools.setup(
|
|
|
58
86
|
"bokeh"
|
|
59
87
|
],
|
|
60
88
|
},
|
|
89
|
+
ext_modules=ext_modules,
|
|
90
|
+
cmdclass={"build_ext": BuildExtWithFallback},
|
|
61
91
|
command_options={
|
|
62
92
|
'nuitka': {
|
|
63
93
|
# boolean option, e.g. if you cared for C compilation commands
|
|
@@ -68,6 +98,11 @@ setuptools.setup(
|
|
|
68
98
|
# '--enable-plugin': ("setup.py", "pyside2"),
|
|
69
99
|
# options with several values, e.g. avoiding including modules
|
|
70
100
|
# '--nofollow-import-to': ("setup.py", ["*.tests", "*.distutils"]),
|
|
101
|
+
# disable LTO
|
|
102
|
+
'--lto': ("setup.py", 'yes'),
|
|
103
|
+
# include some common 3rd party packages
|
|
104
|
+
'--include-package': ("setup.py", ['ctypes', 'datetime', 'typing', 'multiprocessing']),
|
|
105
|
+
# '--mode': ("setup.py", 'standalone')
|
|
71
106
|
}
|
|
72
107
|
}
|
|
73
108
|
)
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
__package__ = 'algo_engine.apps.sim_input'
|
|
2
|
-
|
|
3
|
-
import abc
|
|
4
|
-
import threading
|
|
5
|
-
import time
|
|
6
|
-
import tkinter
|
|
7
|
-
from collections.abc import Callable, Iterable
|
|
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[[..., ...], None], 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):
|
|
67
|
-
self.root = root if root is not None else tkinter.Tk()
|
|
68
|
-
self.root.title("Structured GUI Client")
|
|
69
|
-
self.root.geometry("600x500")
|
|
70
|
-
|
|
71
|
-
# State variables
|
|
72
|
-
self.worker_thread = None
|
|
73
|
-
self.running = False
|
|
74
|
-
|
|
75
|
-
# Create button
|
|
76
|
-
self.button = ttk.Button(root, text="Takeover Input", command=self.toggle_auto_work)
|
|
77
|
-
self.button.pack(pady=10)
|
|
78
|
-
|
|
79
|
-
# Create table
|
|
80
|
-
self.table = ttk.Treeview(
|
|
81
|
-
root,
|
|
82
|
-
columns=("Action", "Status", "Comments"),
|
|
83
|
-
show="tree headings",
|
|
84
|
-
height=20
|
|
85
|
-
)
|
|
86
|
-
self.table.heading("Action", text="Action")
|
|
87
|
-
self.table.heading("Status", text="Status")
|
|
88
|
-
self.table.heading("Comments", text="Comments")
|
|
89
|
-
self.table.pack(padx=10, pady=10, fill="both", expand=True)
|
|
90
|
-
|
|
91
|
-
self.actions: dict[int, list[Action]] = {}
|
|
92
|
-
|
|
93
|
-
@abc.abstractmethod
|
|
94
|
-
def listen_signal(self) -> int:
|
|
95
|
-
...
|
|
96
|
-
|
|
97
|
-
def register_action(self, action: Action, signal: int):
|
|
98
|
-
if signal in self.actions:
|
|
99
|
-
self.actions[signal].append(action)
|
|
100
|
-
else:
|
|
101
|
-
self.actions[signal] = [action]
|
|
102
|
-
|
|
103
|
-
def toggle_auto_work(self):
|
|
104
|
-
"""Toggle the daemon thread to start or stop auto work."""
|
|
105
|
-
if not self.running:
|
|
106
|
-
self.running = True
|
|
107
|
-
self.button.config(text="Release Control")
|
|
108
|
-
self.worker_thread = threading.Thread(target=self.auto_work, daemon=True)
|
|
109
|
-
self.worker_thread.start()
|
|
110
|
-
else:
|
|
111
|
-
self.running = False
|
|
112
|
-
self.button.config(text="Takeover Input")
|
|
113
|
-
|
|
114
|
-
def auto_work(self):
|
|
115
|
-
"""Generate data for the table in a loop."""
|
|
116
|
-
while self.running:
|
|
117
|
-
signal = self.listen_signal()
|
|
118
|
-
actions = self.actions.get(signal, [])
|
|
119
|
-
|
|
120
|
-
# Update table for the current signal
|
|
121
|
-
self.update_table(actions)
|
|
122
|
-
|
|
123
|
-
for action_idx, action in enumerate(actions):
|
|
124
|
-
if not self.running:
|
|
125
|
-
break
|
|
126
|
-
|
|
127
|
-
# Update action status to "Executing"
|
|
128
|
-
self.update_status(parent_id=action_idx, status="Executing")
|
|
129
|
-
action_done = False
|
|
130
|
-
|
|
131
|
-
for step_idx, step in enumerate(action.steps):
|
|
132
|
-
if not self.running:
|
|
133
|
-
break
|
|
134
|
-
|
|
135
|
-
# Update step status to "Executing"
|
|
136
|
-
self.update_status(parent_id=action_idx, child_id=step_idx, status="Executing")
|
|
137
|
-
|
|
138
|
-
# Execute the step
|
|
139
|
-
step["procedure"](*step.get("args", []), **step.get("kwargs", {}))
|
|
140
|
-
|
|
141
|
-
# Mark step as "Done"
|
|
142
|
-
self.update_status(parent_id=action_idx, child_id=step_idx, status="Done")
|
|
143
|
-
else:
|
|
144
|
-
# Mark action as "Done" after all steps
|
|
145
|
-
self.update_status(parent_id=action_idx, status="Done")
|
|
146
|
-
action_done = True
|
|
147
|
-
|
|
148
|
-
if not action_done:
|
|
149
|
-
self.update_status(parent_id=action_idx, status="Stopped")
|
|
150
|
-
|
|
151
|
-
def update_table(self, actions: list[Action]):
|
|
152
|
-
"""Render the table based on the provided actions."""
|
|
153
|
-
self.table.delete(*self.table.get_children()) # Clear existing rows
|
|
154
|
-
|
|
155
|
-
for action_idx, action in enumerate(actions):
|
|
156
|
-
prefix = chr(0x250C)
|
|
157
|
-
# Insert the parent row
|
|
158
|
-
parent_id = self.table.insert(
|
|
159
|
-
"", "end", iid=action_idx, values=(f"{prefix} {action.name}", "Pending", "")
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
# Insert child rows for steps
|
|
163
|
-
for step_idx, step in enumerate(action.steps):
|
|
164
|
-
prefix = f'{chr(0x251C) if step_idx < len(action.steps) - 1 else chr(0x2514)}{chr(0x2500)}'
|
|
165
|
-
self.table.insert(
|
|
166
|
-
parent_id,
|
|
167
|
-
"end",
|
|
168
|
-
iid=f"{action_idx}-{step_idx}",
|
|
169
|
-
text=f"Step {step_idx + 1}",
|
|
170
|
-
values=(f"{prefix} Step {step_idx + 1}", "Pending", step.get("comments", "")),
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
# Expand parent row by default
|
|
174
|
-
self.table.item(parent_id, open=True)
|
|
175
|
-
|
|
176
|
-
# Auto-adjust column widths
|
|
177
|
-
self.adjust_column_widths()
|
|
178
|
-
|
|
179
|
-
def update_status(self, parent_id: int, child_id: int = None, status: str = "Pending"):
|
|
180
|
-
"""Update the status of the specified row."""
|
|
181
|
-
row_id = f"{parent_id}" if child_id is None else f"{parent_id}-{child_id}"
|
|
182
|
-
values = self.table.item(row_id, "values")
|
|
183
|
-
|
|
184
|
-
# Select the row if the status is "Executing"
|
|
185
|
-
match status:
|
|
186
|
-
case "Executing":
|
|
187
|
-
self.table.selection_set(row_id)
|
|
188
|
-
self.table.see(row_id) # Ensure the row is visible
|
|
189
|
-
|
|
190
|
-
self.table.item(row_id, values=(values[0], status, values[2]))
|
|
191
|
-
|
|
192
|
-
def adjust_column_widths(self):
|
|
193
|
-
"""Automatically adjust column widths based on content."""
|
|
194
|
-
|
|
195
|
-
self.table.column("#0", width=30, minwidth=30, stretch=False)
|
|
196
|
-
|
|
197
|
-
for col in self.table["columns"]:
|
|
198
|
-
max_width = max(
|
|
199
|
-
[len(str(self.table.set(item, col))) for item in self.table.get_children()] + [len(col)]
|
|
200
|
-
)
|
|
201
|
-
self.table.column(col, width=max_width * 10, minwidth=30, stretch=True) # Scale width for readability
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
class ExampleClient(AutoWorkClient):
|
|
205
|
-
"""An example implementation of the AutoWorkClient."""
|
|
206
|
-
dummy_signal = 0
|
|
207
|
-
is_init = False
|
|
208
|
-
|
|
209
|
-
@staticmethod
|
|
210
|
-
def dummy_action(name: str, msg: str) -> None:
|
|
211
|
-
LOGGER.info(f'start working on {name}, {msg}')
|
|
212
|
-
time.sleep(2)
|
|
213
|
-
LOGGER.info('working completed!')
|
|
214
|
-
|
|
215
|
-
def listen_signal(self) -> int:
|
|
216
|
-
"""Simulate listening for a signal (e.g., return random signal)."""
|
|
217
|
-
import random
|
|
218
|
-
if self.is_init:
|
|
219
|
-
delay = 5 + 5 * random.random()
|
|
220
|
-
time.sleep(delay)
|
|
221
|
-
self.dummy_signal += 1
|
|
222
|
-
self.is_init = True
|
|
223
|
-
return 1 + self.dummy_signal % 2
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def main():
|
|
227
|
-
client = ExampleClient()
|
|
228
|
-
|
|
229
|
-
# Example usage: Register actions for signal 1
|
|
230
|
-
action1 = Action("Example Action 1")
|
|
231
|
-
action1.append(name='S1A1s1', action=ExampleClient.dummy_action, args=("Signal 1 Action 1", "Step 1"))
|
|
232
|
-
action1.append(name='S1A1s2', action=ExampleClient.dummy_action, args=("Signal 1 Action 1", "Step 2"))
|
|
233
|
-
|
|
234
|
-
action2 = Action("Example Action 2")
|
|
235
|
-
action2.append(name='S1A2s1', action=ExampleClient.dummy_action, args=("Signal 1 Action 2", "Step 1"))
|
|
236
|
-
action2.append(name='S1A2s2', action=ExampleClient.dummy_action, args=("Signal 1 Action 2", "Step 2"))
|
|
237
|
-
|
|
238
|
-
client.register_action(action1, signal=1)
|
|
239
|
-
client.register_action(action2, signal=1)
|
|
240
|
-
|
|
241
|
-
action3 = Action("Example Action 3")
|
|
242
|
-
action3.append(name='S2A1s1', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 1"))
|
|
243
|
-
action3.append(name='S2A1s2', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 2"))
|
|
244
|
-
action3.append(name='S2A1s3', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 3"))
|
|
245
|
-
|
|
246
|
-
client.register_action(action3, signal=2)
|
|
247
|
-
|
|
248
|
-
client.root.mainloop()
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if __name__ == "__main__":
|
|
252
|
-
t = threading.Thread(target=main, daemon=False)
|
|
253
|
-
t.start()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/monitor/advanced_data_interface.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|