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.
- hypertrader-0.1.0.dist-info/METADATA +416 -0
- hypertrader-0.1.0.dist-info/RECORD +16 -0
- hypertrader-0.1.0.dist-info/WHEEL +5 -0
- hypertrader-0.1.0.dist-info/entry_points.txt +2 -0
- hypertrader-0.1.0.dist-info/top_level.txt +3 -0
- hypertrader.py +741 -0
- modes/auto_trader.py +2141 -0
- modes/market_maker.py +397 -0
- modes/position_management.py +1911 -0
- modes/position_watcher.py +411 -0
- modes/trailing_stop.py +277 -0
- utils/constants.py +31 -0
- utils/helpers.py +988 -0
- utils/style.py +173 -0
- utils/timer.py +14 -0
- utils/worker.py +233 -0
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())
|