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.
Files changed (54) hide show
  1. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/PKG-INFO +1 -1
  2. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/PyAlgoEngine.egg-info/PKG-INFO +1 -1
  3. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/__init__.py +1 -1
  4. pyalgoengine-0.7.7/algo_engine/apps/sim_input/client.py +412 -0
  5. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/backtest/replay.py +17 -4
  6. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/backtest/sim_match.py +50 -12
  7. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/market_utils_posix.py +7 -1
  8. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/engine/market_engine.py +1 -0
  9. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/setup.py +35 -0
  10. pyalgoengine-0.7.6.post7/algo_engine/apps/sim_input/client.py +0 -253
  11. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/LICENSE +0 -0
  12. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/PyAlgoEngine.egg-info/SOURCES.txt +0 -0
  13. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/PyAlgoEngine.egg-info/dependency_links.txt +0 -0
  14. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/PyAlgoEngine.egg-info/requires.txt +0 -0
  15. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/PyAlgoEngine.egg-info/top_level.txt +0 -0
  16. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/README.md +0 -0
  17. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/__init__.py +0 -0
  18. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/backtest/__init__.py +0 -0
  19. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/backtest/doc_server.py +0 -0
  20. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/backtest/tester.py +0 -0
  21. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/backtest/web_app.py +0 -0
  22. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/bokeh_server.py +0 -0
  23. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/demo/__init__.py +0 -0
  24. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/demo/test.py +0 -0
  25. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/sim_input/__init__.py +0 -0
  26. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/sim_input/sim_keyboard.py +0 -0
  27. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/sim_input/sim_mouse.py +0 -0
  28. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/apps/sim_input/window.py +0 -0
  29. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/backtest/__init__.py +0 -0
  30. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/backtest/__main__.py +0 -0
  31. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/backtest/metrics.py +0 -0
  32. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/__init__.py +0 -0
  33. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/console_utils.py +0 -0
  34. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/finance_decimal.py +0 -0
  35. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/market_buffer.py +0 -0
  36. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/market_utils.py +0 -0
  37. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/market_utils_nt.py +0 -0
  38. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/technical_analysis.py +0 -0
  39. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/telemetrics.py +0 -0
  40. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/base/trade_utils.py +0 -0
  41. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/engine/__init__.py +0 -0
  42. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/engine/algo_engine.py +0 -0
  43. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/engine/event_engine.py +0 -0
  44. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/engine/trade_engine.py +0 -0
  45. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/monitor/__init__.py +0 -0
  46. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/monitor/advanced_data_interface.py +0 -0
  47. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/profile/__init__.py +0 -0
  48. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/profile/cn.py +0 -0
  49. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/strategy/__init__.py +0 -0
  50. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/strategy/strategy_engine.py +0 -0
  51. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/utils/__init__.py +0 -0
  52. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/utils/commit_regularizer.py +0 -0
  53. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/algo_engine/utils/data_utils.py +0 -0
  54. {pyalgoengine-0.7.6.post7 → pyalgoengine-0.7.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyAlgoEngine
3
- Version: 0.7.6.post7
3
+ Version: 0.7.7
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyAlgoEngine
3
- Version: 0.7.6.post7
3
+ Version: 0.7.7
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -1,4 +1,4 @@
1
- __version__ = "0.7.6.post7"
1
+ __version__ = "0.7.7"
2
2
 
3
3
  import logging
4
4
  import os
@@ -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.start_date])
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.date_progress and (self.date_progress >= len(self.replay_calendar) or self.replay_calendar[self.date_progress] > self.start_date):
218
- self.eod(market_date=self.replay_calendar[self.date_progress - 1], replay=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, instant_fill: bool = False, event_engine=None, topic_set=None, fee_rate: float = 0.):
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 not self.instant_fill:
74
- self.working[order.order_id] = order
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
- if self.instant_fill:
79
- if limit := order.limit_price:
80
- self._match(order=order, match_price=limit)
81
- else:
82
- LOGGER.warning(f'No limit price provided for {order}, instant_fill mode not available.')
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
- LOGGER.warning(f'Requesting .sign of {self.name} is not recommended, use .order_sign instead')
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