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.
Files changed (156) hide show
  1. algo_engine/__infra__.pxd +3 -0
  2. algo_engine/__init__.pxd +3 -0
  3. algo_engine/__init__.py +107 -0
  4. algo_engine/apps/__init__.py +17 -0
  5. algo_engine/apps/backtest/__init__.py +20 -0
  6. algo_engine/apps/backtest/doc_server.py +328 -0
  7. algo_engine/apps/backtest/static/styles/dash.css +48 -0
  8. algo_engine/apps/backtest/templates/dash.html +20 -0
  9. algo_engine/apps/backtest/templates/index.html +40 -0
  10. algo_engine/apps/backtest/tester.py +252 -0
  11. algo_engine/apps/backtest/web_app.py +125 -0
  12. algo_engine/apps/bokeh_server.py +245 -0
  13. algo_engine/apps/demo/__init__.py +0 -0
  14. algo_engine/apps/demo/test.py +40 -0
  15. algo_engine/apps/sim_input/__init__.py +23 -0
  16. algo_engine/apps/sim_input/client.py +412 -0
  17. algo_engine/apps/sim_input/sim_keyboard.py +88 -0
  18. algo_engine/apps/sim_input/sim_mouse.py +137 -0
  19. algo_engine/apps/sim_input/window.py +162 -0
  20. algo_engine/backtest/__init__.py +19 -0
  21. algo_engine/backtest/__main__.py +51 -0
  22. algo_engine/backtest/metrics.py +179 -0
  23. algo_engine/backtest/replay.py +635 -0
  24. algo_engine/backtest/sim_match.py +506 -0
  25. algo_engine/base/__infra__.pxd +3 -0
  26. algo_engine/base/__init__.pxd +3 -0
  27. algo_engine/base/__init__.py +60 -0
  28. algo_engine/base/c_allocator_protocol.c +11608 -0
  29. algo_engine/base/c_allocator_protocol.cp315-win_amd64.pyd +0 -0
  30. algo_engine/base/c_allocator_protocol.pxd +24 -0
  31. algo_engine/base/c_allocator_protocol.pyi +68 -0
  32. algo_engine/base/c_allocator_protocol.pyx +111 -0
  33. algo_engine/base/c_intern_string.c +5908 -0
  34. algo_engine/base/c_intern_string.cp315-win_amd64.pyd +0 -0
  35. algo_engine/base/c_intern_string.pxd +14 -0
  36. algo_engine/base/c_intern_string.pyi +22 -0
  37. algo_engine/base/c_intern_string.pyx +17 -0
  38. algo_engine/base/c_market_data/__infra__.pxd +196 -0
  39. algo_engine/base/c_market_data/__init__.pxd +196 -0
  40. algo_engine/base/c_market_data/__init__.py +24 -0
  41. algo_engine/base/c_market_data/c_candlestick.c +18996 -0
  42. algo_engine/base/c_market_data/c_candlestick.cp315-win_amd64.pyd +0 -0
  43. algo_engine/base/c_market_data/c_candlestick.pxd +27 -0
  44. algo_engine/base/c_market_data/c_candlestick.pyi +217 -0
  45. algo_engine/base/c_market_data/c_candlestick.pyx +255 -0
  46. algo_engine/base/c_market_data/c_internal.c +14059 -0
  47. algo_engine/base/c_market_data/c_internal.cp315-win_amd64.pyd +0 -0
  48. algo_engine/base/c_market_data/c_internal.pxd +14 -0
  49. algo_engine/base/c_market_data/c_internal.pyi +47 -0
  50. algo_engine/base/c_market_data/c_internal.pyx +41 -0
  51. algo_engine/base/c_market_data/c_market_data.c +30420 -0
  52. algo_engine/base/c_market_data/c_market_data.cp315-win_amd64.pyd +0 -0
  53. algo_engine/base/c_market_data/c_market_data.h +1464 -0
  54. algo_engine/base/c_market_data/c_market_data.pxd +414 -0
  55. algo_engine/base/c_market_data/c_market_data.pyi +550 -0
  56. algo_engine/base/c_market_data/c_market_data.pyx +701 -0
  57. algo_engine/base/c_market_data/c_market_data_buffer.c +29407 -0
  58. algo_engine/base/c_market_data/c_market_data_buffer.cp315-win_amd64.pyd +0 -0
  59. algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
  60. algo_engine/base/c_market_data/c_market_data_buffer.pxd +143 -0
  61. algo_engine/base/c_market_data/c_market_data_buffer.pyi +355 -0
  62. algo_engine/base/c_market_data/c_market_data_buffer.pyx +571 -0
  63. algo_engine/base/c_market_data/c_market_data_config.h +29 -0
  64. algo_engine/base/c_market_data/c_tick.c +44303 -0
  65. algo_engine/base/c_market_data/c_tick.cp315-win_amd64.pyd +0 -0
  66. algo_engine/base/c_market_data/c_tick.pxd +53 -0
  67. algo_engine/base/c_market_data/c_tick.pyi +466 -0
  68. algo_engine/base/c_market_data/c_tick.pyx +673 -0
  69. algo_engine/base/c_market_data/c_trade_utils.c +28702 -0
  70. algo_engine/base/c_market_data/c_trade_utils.cp315-win_amd64.pyd +0 -0
  71. algo_engine/base/c_market_data/c_trade_utils.pxd +53 -0
  72. algo_engine/base/c_market_data/c_trade_utils.pyi +602 -0
  73. algo_engine/base/c_market_data/c_trade_utils.pyx +609 -0
  74. algo_engine/base/c_market_data/c_transaction.c +23558 -0
  75. algo_engine/base/c_market_data/c_transaction.cp315-win_amd64.pyd +0 -0
  76. algo_engine/base/c_market_data/c_transaction.pxd +27 -0
  77. algo_engine/base/c_market_data/c_transaction.pyi +433 -0
  78. algo_engine/base/c_market_data/c_transaction.pyx +460 -0
  79. algo_engine/base/console_utils.py +1070 -0
  80. algo_engine/base/finance_decimal.py +258 -0
  81. algo_engine/base/telemetrics.py +18 -0
  82. algo_engine/engine/__infra__.pxd +10 -0
  83. algo_engine/engine/__init__.pxd +10 -0
  84. algo_engine/engine/__init__.py +40 -0
  85. algo_engine/engine/algo_engine.py +904 -0
  86. algo_engine/engine/c_event_engine.c +16325 -0
  87. algo_engine/engine/c_event_engine.cp315-win_amd64.pyd +0 -0
  88. algo_engine/engine/c_event_engine.pxd +25 -0
  89. algo_engine/engine/c_event_engine.pyi +68 -0
  90. algo_engine/engine/c_market_engine.c +24258 -0
  91. algo_engine/engine/c_market_engine.cp315-win_amd64.pyd +0 -0
  92. algo_engine/engine/c_market_engine.pxd +87 -0
  93. algo_engine/engine/c_market_engine.pyi +357 -0
  94. algo_engine/engine/event_engine.py +53 -0
  95. algo_engine/engine/trade_engine.py +2037 -0
  96. algo_engine/exchange_profile/__infra__.pxd +100 -0
  97. algo_engine/exchange_profile/__init__.pxd +100 -0
  98. algo_engine/exchange_profile/__init__.py +53 -0
  99. algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
  100. algo_engine/exchange_profile/c_ex_profile_base.h +1204 -0
  101. algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
  102. algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
  103. algo_engine/exchange_profile/c_exchange_profile.c +52375 -0
  104. algo_engine/exchange_profile/c_exchange_profile.cp315-win_amd64.pyd +0 -0
  105. algo_engine/exchange_profile/c_exchange_profile.pxd +336 -0
  106. algo_engine/exchange_profile/c_exchange_profile.pyi +883 -0
  107. algo_engine/exchange_profile/c_exchange_profile.pyx +1495 -0
  108. algo_engine/exchange_profile/c_profile_cn.c +7798 -0
  109. algo_engine/exchange_profile/c_profile_cn.cp315-win_amd64.pyd +0 -0
  110. algo_engine/exchange_profile/c_profile_cn.pxd +1 -0
  111. algo_engine/exchange_profile/c_profile_cn.pyi +3 -0
  112. algo_engine/exchange_profile/c_profile_cn.pyx +1 -0
  113. algo_engine/exchange_profile/c_profile_default.c +7798 -0
  114. algo_engine/exchange_profile/c_profile_default.cp315-win_amd64.pyd +0 -0
  115. algo_engine/exchange_profile/c_profile_default.pxd +1 -0
  116. algo_engine/exchange_profile/c_profile_default.pyi +3 -0
  117. algo_engine/exchange_profile/c_profile_default.pyx +1 -0
  118. algo_engine/exchange_profile/c_profile_dispatcher.c +7798 -0
  119. algo_engine/exchange_profile/c_profile_dispatcher.cp315-win_amd64.pyd +0 -0
  120. algo_engine/exchange_profile/c_profile_dispatcher.pxd +1 -0
  121. algo_engine/exchange_profile/c_profile_dispatcher.pyi +3 -0
  122. algo_engine/exchange_profile/c_profile_dispatcher.pyx +1 -0
  123. algo_engine/includes/algo_engine/base/c_allocator_protocol.c +11608 -0
  124. algo_engine/includes/algo_engine/base/c_intern_string.c +5908 -0
  125. algo_engine/includes/algo_engine/base/c_market_data/c_candlestick.c +18996 -0
  126. algo_engine/includes/algo_engine/base/c_market_data/c_internal.c +14059 -0
  127. algo_engine/includes/algo_engine/base/c_market_data/c_market_data.c +30420 -0
  128. algo_engine/includes/algo_engine/base/c_market_data/c_market_data.h +1464 -0
  129. algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.c +29407 -0
  130. algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
  131. algo_engine/includes/algo_engine/base/c_market_data/c_market_data_config.h +29 -0
  132. algo_engine/includes/algo_engine/base/c_market_data/c_tick.c +44303 -0
  133. algo_engine/includes/algo_engine/base/c_market_data/c_trade_utils.c +28702 -0
  134. algo_engine/includes/algo_engine/base/c_market_data/c_transaction.c +23558 -0
  135. algo_engine/includes/algo_engine/engine/c_event_engine.c +16325 -0
  136. algo_engine/includes/algo_engine/engine/c_market_engine.c +24258 -0
  137. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
  138. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.h +1204 -0
  139. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
  140. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
  141. algo_engine/includes/algo_engine/exchange_profile/c_exchange_profile.c +52375 -0
  142. algo_engine/includes/algo_engine/exchange_profile/c_profile_cn.c +7798 -0
  143. algo_engine/includes/algo_engine/exchange_profile/c_profile_default.c +7798 -0
  144. algo_engine/includes/algo_engine/exchange_profile/c_profile_dispatcher.c +7798 -0
  145. algo_engine/monitor/__init__.py +15 -0
  146. algo_engine/monitor/advanced_data_interface.py +334 -0
  147. algo_engine/strategy/__init__.py +44 -0
  148. algo_engine/strategy/strategy_engine.py +441 -0
  149. algo_engine/utils/__init__.py +3 -0
  150. algo_engine/utils/commit_regularizer.py +49 -0
  151. algo_engine/utils/data_utils.py +296 -0
  152. pyalgoengine-0.12.3.dist-info/METADATA +142 -0
  153. pyalgoengine-0.12.3.dist-info/RECORD +156 -0
  154. pyalgoengine-0.12.3.dist-info/WHEEL +5 -0
  155. pyalgoengine-0.12.3.dist-info/licenses/LICENSE +21 -0
  156. 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()