hypertrader 0.1.0__py3-none-any.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.
utils/style.py ADDED
@@ -0,0 +1,173 @@
1
+ import builtins
2
+ import sys
3
+ from typing import Any, Optional
4
+
5
+ try:
6
+ import colored
7
+ except Exception: # pragma: no cover - color output should degrade cleanly.
8
+ colored = None # type: ignore[assignment]
9
+
10
+
11
+ _ORIGINAL_PRINT = builtins.print
12
+
13
+
14
+ def _color(name: str) -> str:
15
+ if colored is None:
16
+ return ""
17
+ return str(getattr(colored.Fore, name, ""))
18
+
19
+
20
+ def _style(name: str) -> str:
21
+ if colored is None:
22
+ return ""
23
+ return str(getattr(colored.Style, name, ""))
24
+
25
+
26
+ RESET = _style("RESET")
27
+ BOLD = _style("BOLD")
28
+ DIM = _style("DIM")
29
+
30
+ PALETTE = {
31
+ "banner": _color("deep_sky_blue_2"),
32
+ "header": _color("cyan_3"),
33
+ "normal": _color("LIGHT_BLUE"),
34
+ "muted": _color("grey_70"),
35
+ "detail": _color("light_cyan_3"),
36
+ "success": _color("spring_green_3b"),
37
+ "good": _color("yellow_1"),
38
+ "warning": _color("VIOLET"),
39
+ "error": _color("RED_1"),
40
+ "accent": _color("orange_1"),
41
+ "auto": _color("gold_1"),
42
+ "watch": _color("light_slate_blue"),
43
+ "entry": _color("deep_sky_blue_1"),
44
+ "mm": _color("plum_2"),
45
+ "hidden": _color("medium_spring_green"),
46
+ }
47
+
48
+ PREFIX_STYLES = {
49
+ "[ERROR]": "error",
50
+ "[FATAL]": "error",
51
+ "[WARN]": "warning",
52
+ "[AUTO-WARN]": "warning",
53
+ "[WATCH-WARN]": "warning",
54
+ "[WS-WARN]": "warning",
55
+ "[HIDDEN-TP-WARN]": "warning",
56
+ "[TP-REMAINDER-WARN]": "warning",
57
+ "[DONE]": "success",
58
+ "[OK]": "success",
59
+ "[RESULT]": "success",
60
+ "[SUCCESS]": "success",
61
+ "[OPEN]": "good",
62
+ "[START]": "header",
63
+ "[LOG]": "detail",
64
+ "[AUTO]": "auto",
65
+ "[AUTO-RISK]": "accent",
66
+ "[WATCH]": "watch",
67
+ "[ENTRY]": "entry",
68
+ "[MM]": "mm",
69
+ "[HIDDEN-TP]": "hidden",
70
+ "[HIDDEN-SL]": "hidden",
71
+ "[HIDE-ORDERS]": "hidden",
72
+ "[TP-REVERSAL]": "accent",
73
+ "[TP-REVERSAL EXIT]": "accent",
74
+ "[TP-REVERSAL LIMIT]": "accent",
75
+ "[MARKET ENTRY]": "accent",
76
+ "[MARKET EXIT]": "accent",
77
+ "[TP]": "good",
78
+ "[SL-SAR]": "accent",
79
+ "[SL-SAR-WARN]": "warning",
80
+ "[TRAILING-TP]": "accent",
81
+ "[TP-REMAINDER]": "accent",
82
+ "[CANCEL]": "warning",
83
+ "[WS]": "detail",
84
+ }
85
+
86
+
87
+ def _emit(text: str, color_key: str = "normal", pretext: Optional[str] = None) -> None:
88
+ if pretext:
89
+ prefix = f"[{pretext}] "
90
+ _ORIGINAL_PRINT(f"{PALETTE.get(color_key, '')}{BOLD}{prefix}{RESET}{text}")
91
+ return
92
+ _ORIGINAL_PRINT(f"{PALETTE.get(color_key, '')}{text}{RESET}")
93
+
94
+
95
+ def _classify_line(line: str) -> str:
96
+ stripped = line.strip()
97
+ if not stripped:
98
+ return line
99
+
100
+ if stripped and len(set(stripped)) == 1 and stripped[0] in {"=", "-"} and len(stripped) >= 12:
101
+ return f"{PALETTE['banner']}{BOLD}{line}{RESET}"
102
+
103
+ if stripped.startswith("[!]"):
104
+ return f"{PALETTE['warning']}{BOLD}{line}{RESET}"
105
+
106
+ if stripped.startswith(" Hyperliquid Async") or stripped.startswith(" Async "):
107
+ return f"{PALETTE['header']}{BOLD}{line}{RESET}"
108
+
109
+ for prefix, color_key in sorted(PREFIX_STYLES.items(), key=lambda item: len(item[0]), reverse=True):
110
+ if stripped.startswith(prefix):
111
+ return f"{PALETTE[color_key]}{BOLD}{line}{RESET}"
112
+
113
+ if line.startswith(" "):
114
+ return f"{PALETTE['detail']}{line}{RESET}"
115
+
116
+ if ":" in line and not stripped.startswith("{"):
117
+ return f"{PALETTE['normal']}{line}{RESET}"
118
+
119
+ return f"{PALETTE['muted']}{line}{RESET}"
120
+
121
+
122
+ def emit_styled_print(*values: Any, sep: str = " ", end: str = "\n", file: Any = None, flush: bool = False) -> None:
123
+ target = sys.stdout if file is None else file
124
+ if target is not sys.stdout:
125
+ _ORIGINAL_PRINT(*values, sep=sep, end=end, file=file, flush=flush)
126
+ return
127
+
128
+ rendered = sep.join(str(value) for value in values)
129
+ trailing_newline = rendered.endswith("\n")
130
+ segments = rendered.split("\n")
131
+ for index, segment in enumerate(segments):
132
+ is_last = index == len(segments) - 1
133
+ segment_end = "" if not is_last else end
134
+ _ORIGINAL_PRINT(_classify_line(segment), end=segment_end, file=target, flush=False)
135
+ if index < len(segments) - 1:
136
+ _ORIGINAL_PRINT("", file=target, flush=False)
137
+ if trailing_newline and end == "\n":
138
+ target.flush()
139
+ if flush:
140
+ target.flush()
141
+
142
+
143
+ def install_pretty_stdout() -> None:
144
+ builtins.print = emit_styled_print
145
+
146
+
147
+ class PrettyText:
148
+ @classmethod
149
+ def normal(cls, data: Any, pretext: str = "+") -> None:
150
+ _emit(str(data), "normal", pretext)
151
+
152
+ @classmethod
153
+ def error(cls, data: Any, pretext: str = "!") -> None:
154
+ _emit(str(data), "error", pretext)
155
+
156
+ @classmethod
157
+ def good(cls, data: Any, pretext: str = "~") -> None:
158
+ _emit(str(data), "good", pretext)
159
+
160
+ @classmethod
161
+ def success(cls, data: Any, pretext: str = "~") -> None:
162
+ _emit(str(data), "success", pretext)
163
+
164
+ @classmethod
165
+ def warning(cls, data: Any, pretext: str = "*") -> None:
166
+ _emit(str(data), "warning", pretext)
167
+
168
+ @classmethod
169
+ def print(cls, data: Any, color: str) -> None:
170
+ if colored is None:
171
+ _ORIGINAL_PRINT(str(data))
172
+ return
173
+ _ORIGINAL_PRINT(_color(color) + str(data) + RESET)
utils/timer.py ADDED
@@ -0,0 +1,14 @@
1
+ import time
2
+
3
+ class Timer:
4
+ def __init__(self, name: str = __name__ ):
5
+ self.start_time = None
6
+ self.name = name
7
+ self.reset()
8
+
9
+ @property
10
+ def elapsed(self):
11
+ return time.time() - self.start_time
12
+
13
+ def reset(self):
14
+ self.start_time = time.time()
utils/worker.py ADDED
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Class-based asyncio task queue with bounded concurrency.
4
+
5
+ Runs up to `concurrency` async jobs at the same time and yields results as soon
6
+ as each job completes, not in submission order.
7
+
8
+ Run:
9
+ python3 asyncio_task_queue_class.py
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import random
16
+ import time
17
+ from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
18
+ from dataclasses import dataclass
19
+ from typing import Generic, TypeVar
20
+
21
+
22
+ T = TypeVar("T")
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class QueueResult(Generic[T]):
27
+ """
28
+ Result emitted by AsyncTaskQueue.
29
+
30
+ index:
31
+ Original zero-based submission index.
32
+
33
+ ok:
34
+ True when the coroutine returned normally.
35
+ False when it raised an exception.
36
+
37
+ value:
38
+ Return value from the coroutine when ok=True.
39
+
40
+ error:
41
+ Exception raised by the coroutine when ok=False.
42
+
43
+ elapsed:
44
+ Runtime for this individual coroutine in seconds.
45
+ """
46
+
47
+ index: int
48
+ ok: bool
49
+ value: T | None
50
+ error: BaseException | None
51
+ elapsed: float
52
+
53
+
54
+ class AsyncTaskQueue(Generic[T]):
55
+ """
56
+ Async task queue with bounded concurrency.
57
+
58
+ The queue accepts an iterable of zero-argument callables. Each callable must
59
+ return an awaitable/coroutine when called.
60
+
61
+ Example:
62
+ jobs = [
63
+ lambda item=item: process_item(item)
64
+ for item in items
65
+ ]
66
+
67
+ queue = AsyncTaskQueue(jobs, concurrency=10)
68
+
69
+ async for result in queue.run():
70
+ print(result)
71
+
72
+ Use callables instead of already-created coroutine objects so the queue
73
+ controls when each coroutine is created and scheduled.
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ jobs: Iterable[Callable[[], Awaitable[T]]],
79
+ concurrency: int = 10,
80
+ ) -> None:
81
+ if concurrency < 1:
82
+ raise ValueError("concurrency must be >= 1")
83
+
84
+ self.jobs = list(jobs)
85
+ self.concurrency = concurrency
86
+ self._work_queue: asyncio.Queue[
87
+ tuple[int, Callable[[], Awaitable[T]]] | None
88
+ ] = asyncio.Queue()
89
+ self._result_queue: asyncio.Queue[QueueResult[T]] = asyncio.Queue()
90
+ self._workers: list[asyncio.Task[None]] = []
91
+ self._started = False
92
+
93
+ def __aiter__(self) -> AsyncIterator[QueueResult[T]]:
94
+ return self.run()
95
+
96
+ @property
97
+ def total_jobs(self) -> int:
98
+ return len(self.jobs)
99
+
100
+ @property
101
+ def worker_count(self) -> int:
102
+ return min(self.concurrency, self.total_jobs) if self.total_jobs else 0
103
+
104
+ async def run(self) -> AsyncIterator[QueueResult[T]]:
105
+ """
106
+ Run queued jobs and yield QueueResult objects as jobs complete.
107
+ """
108
+
109
+ if self._started:
110
+ raise RuntimeError("AsyncTaskQueue instances can only be run once")
111
+
112
+ self._started = True
113
+ self._load_jobs()
114
+ self._start_workers()
115
+
116
+ try:
117
+ for _ in range(self.total_jobs):
118
+ yield await self._result_queue.get()
119
+ finally:
120
+ await self.aclose()
121
+
122
+ def _load_jobs(self) -> None:
123
+ for index, job in enumerate(self.jobs):
124
+ self._work_queue.put_nowait((index, job))
125
+
126
+ for _ in range(self.worker_count):
127
+ self._work_queue.put_nowait(None)
128
+
129
+ def _start_workers(self) -> None:
130
+ self._workers = [
131
+ asyncio.create_task(self._worker())
132
+ for _ in range(self.worker_count)
133
+ ]
134
+
135
+ async def _worker(self) -> None:
136
+ while True:
137
+ item = await self._work_queue.get()
138
+
139
+ try:
140
+ if item is None:
141
+ return
142
+
143
+ index, job = item
144
+ result = await self._run_one(index=index, job=job)
145
+ await self._result_queue.put(result)
146
+ finally:
147
+ self._work_queue.task_done()
148
+
149
+ async def _run_one(
150
+ self,
151
+ index: int,
152
+ job: Callable[[], Awaitable[T]],
153
+ ) -> QueueResult[T]:
154
+ started = time.perf_counter()
155
+
156
+ try:
157
+ value = await job()
158
+ return QueueResult(
159
+ index=index,
160
+ ok=True,
161
+ value=value,
162
+ error=None,
163
+ elapsed=time.perf_counter() - started,
164
+ )
165
+ except BaseException as exc:
166
+ return QueueResult(
167
+ index=index,
168
+ ok=False,
169
+ value=None,
170
+ error=exc,
171
+ elapsed=time.perf_counter() - started,
172
+ )
173
+
174
+ async def aclose(self) -> None:
175
+ """
176
+ Cancel any still-running workers and wait for them to exit.
177
+
178
+ This is called automatically when run() exits, including when the
179
+ consuming async-for loop breaks early.
180
+ """
181
+
182
+ for task in self._workers:
183
+ if not task.done():
184
+ task.cancel()
185
+
186
+ if self._workers:
187
+ await asyncio.gather(*self._workers, return_exceptions=True)
188
+
189
+
190
+ async def example_job(job_id: int) -> str:
191
+ """
192
+ Example async task.
193
+
194
+ This sleeps for a random amount of time to prove results are yielded in
195
+ completion order rather than submission order.
196
+ """
197
+
198
+ delay = random.uniform(0.1, 2.0)
199
+ await asyncio.sleep(delay)
200
+
201
+ if job_id == 17:
202
+ raise RuntimeError("simulated failure for job 17")
203
+
204
+ return f"job {job_id} finished after {delay:.2f}s"
205
+
206
+
207
+ async def main() -> None:
208
+ total_jobs = 30
209
+
210
+ jobs = [
211
+ lambda job_id=job_id: example_job(job_id)
212
+ for job_id in range(total_jobs)
213
+ ]
214
+
215
+ queue = AsyncTaskQueue(jobs=jobs, concurrency=10)
216
+
217
+ async for result in queue:
218
+ if result.ok:
219
+ print(
220
+ f"[ok] index={result.index:02d} "
221
+ f"elapsed={result.elapsed:.2f}s "
222
+ f"value={result.value}"
223
+ )
224
+ else:
225
+ print(
226
+ f"[fail] index={result.index:02d} "
227
+ f"elapsed={result.elapsed:.2f}s "
228
+ f"error={type(result.error).__name__}: {result.error}"
229
+ )
230
+
231
+
232
+ if __name__ == "__main__":
233
+ asyncio.run(main())