polyester-bot 0.0.0-stage → 1.1.0
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.
- package/LICENSE +21 -0
- package/README.md +273 -2
- package/bin/polyester-bot.js +7 -0
- package/bin/polyester-farm.js +4 -0
- package/bin/setup.js +75 -0
- package/bin/venv.js +12 -0
- package/bot/__init__.py +1 -0
- package/bot/big_buy.py +75 -0
- package/bot/cancel_all.py +72 -0
- package/bot/check_orders.py +47 -0
- package/bot/hunter_forever.py +18 -0
- package/bot/kill_bot.py +11 -0
- package/bot/monitor.py +847 -0
- package/bot/paths.py +43 -0
- package/bot/polyester_farm.py +434 -0
- package/bot/scan_spread.py +76 -0
- package/bot/setup.py +300 -0
- package/bot/status_all.py +22 -0
- package/install.js +5 -0
- package/package.json +41 -4
- package/polyester-bot.png +0 -0
- package/pyproject.toml +40 -0
package/bot/setup.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Guided setup: ask for the credentials, check them, write the .env.
|
|
2
|
+
|
|
3
|
+
Run it with `polyester-bot setup` (npm) or `python setup.py` (source tree).
|
|
4
|
+
It asks for the API key, the Ed25519 seed and the Telegram bot token plus the
|
|
5
|
+
chat id, derives the Polyester account id from the key, proves both channels
|
|
6
|
+
work and then writes the .env the bot reads.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import getpass
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
import urllib.error
|
|
18
|
+
import urllib.parse
|
|
19
|
+
import urllib.request
|
|
20
|
+
|
|
21
|
+
if __package__:
|
|
22
|
+
from .paths import HOME_ENV, STATE_DIR, env_files
|
|
23
|
+
else:
|
|
24
|
+
from paths import HOME_ENV, STATE_DIR, env_files
|
|
25
|
+
|
|
26
|
+
API = "https://api.testnet.polyester.com"
|
|
27
|
+
SITE = "https://testnet.polyester.com"
|
|
28
|
+
TELEGRAM_API = "https://api.telegram.org"
|
|
29
|
+
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
30
|
+
"(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SetupError(Exception):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def normalize_seed(raw):
|
|
38
|
+
"""The Ed25519 seed as 64 hex characters, from hex or base64."""
|
|
39
|
+
text = (raw or "").strip()
|
|
40
|
+
if text[:2].lower() == "0x":
|
|
41
|
+
text = text[2:]
|
|
42
|
+
if len(text) == 64 and all(c in "0123456789abcdefABCDEF" for c in text):
|
|
43
|
+
return text.lower()
|
|
44
|
+
padded = text + "=" * (-len(text) % 4)
|
|
45
|
+
try:
|
|
46
|
+
raw_bytes = base64.b64decode(padded, validate=True)
|
|
47
|
+
except Exception:
|
|
48
|
+
raw_bytes = b""
|
|
49
|
+
if len(raw_bytes) != 32:
|
|
50
|
+
raise ValueError("the seed must be 64 hex characters or base64 of 32 bytes")
|
|
51
|
+
return raw_bytes.hex()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def merge_env(path, values):
|
|
55
|
+
"""Write values into the .env at path, keeping every other line."""
|
|
56
|
+
try:
|
|
57
|
+
with open(path, encoding="utf-8") as f:
|
|
58
|
+
lines = f.read().splitlines()
|
|
59
|
+
except OSError:
|
|
60
|
+
lines = []
|
|
61
|
+
out = []
|
|
62
|
+
seen = set()
|
|
63
|
+
for line in lines:
|
|
64
|
+
key = line.split("=", 1)[0].strip() if "=" in line else ""
|
|
65
|
+
if key in values:
|
|
66
|
+
if key in seen:
|
|
67
|
+
continue
|
|
68
|
+
out.append(key + "=" + values[key])
|
|
69
|
+
seen.add(key)
|
|
70
|
+
else:
|
|
71
|
+
out.append(line)
|
|
72
|
+
for key, value in values.items():
|
|
73
|
+
if key not in seen:
|
|
74
|
+
out.append(key + "=" + value)
|
|
75
|
+
try:
|
|
76
|
+
folder = os.path.dirname(path)
|
|
77
|
+
if folder and not os.path.isdir(folder):
|
|
78
|
+
os.makedirs(folder)
|
|
79
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
80
|
+
f.write("\n".join(out) + "\n")
|
|
81
|
+
os.chmod(path, 0o600)
|
|
82
|
+
except OSError as e:
|
|
83
|
+
raise SetupError("cannot write " + path + ": " + str(e))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def signed_get(path, key_id, seed):
|
|
87
|
+
"""GET a signed endpoint with the given key id and hex seed."""
|
|
88
|
+
import nacl.signing
|
|
89
|
+
|
|
90
|
+
ts = str(int(time.time() * 1000))
|
|
91
|
+
canon = "\n".join([ts, "GET", path, "", hashlib.sha256(b"").hexdigest()])
|
|
92
|
+
signature = nacl.signing.SigningKey(bytes.fromhex(seed)).sign(
|
|
93
|
+
canon.encode()).signature.hex()
|
|
94
|
+
req = urllib.request.Request(API + path, method="GET")
|
|
95
|
+
req.add_header("User-Agent", UA)
|
|
96
|
+
req.add_header("Accept", "application/json")
|
|
97
|
+
req.add_header("Origin", SITE)
|
|
98
|
+
req.add_header("Referer", SITE + "/")
|
|
99
|
+
req.add_header("X-API-KEY-ID", key_id)
|
|
100
|
+
req.add_header("X-API-TIMESTAMP", ts)
|
|
101
|
+
req.add_header("X-API-SIGNATURE", signature)
|
|
102
|
+
try:
|
|
103
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
104
|
+
return json.loads(r.read() or b"{}")
|
|
105
|
+
except urllib.error.HTTPError as e:
|
|
106
|
+
body = e.read().decode(errors="replace")
|
|
107
|
+
raise SetupError("the exchange refused " + path + ": " + body[:200])
|
|
108
|
+
except OSError as e:
|
|
109
|
+
raise SetupError("cannot reach the exchange: " + str(e))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def telegram(method, token, **params):
|
|
113
|
+
"""Call a Telegram bot method and return its result."""
|
|
114
|
+
url = TELEGRAM_API + "/bot" + token + "/" + method
|
|
115
|
+
if params:
|
|
116
|
+
url += "?" + urllib.parse.urlencode(params)
|
|
117
|
+
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
|
118
|
+
try:
|
|
119
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
120
|
+
data = json.loads(r.read() or b"{}")
|
|
121
|
+
except urllib.error.HTTPError as e:
|
|
122
|
+
body = e.read().decode(errors="replace")
|
|
123
|
+
try:
|
|
124
|
+
detail = json.loads(body).get("description", body[:200])
|
|
125
|
+
except ValueError:
|
|
126
|
+
detail = body[:200]
|
|
127
|
+
raise SetupError("telegram " + method + " failed: " + str(detail))
|
|
128
|
+
except OSError as e:
|
|
129
|
+
raise SetupError("cannot reach telegram: " + str(e))
|
|
130
|
+
if not data.get("ok"):
|
|
131
|
+
raise SetupError("telegram " + method + " failed: "
|
|
132
|
+
+ str(data.get("description", data)))
|
|
133
|
+
return data.get("result", data)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def telegram_chat_ids(token):
|
|
137
|
+
"""Chat ids the bot has seen so far, oldest first."""
|
|
138
|
+
updates = telegram("getUpdates", token, timeout=0) or []
|
|
139
|
+
ids = []
|
|
140
|
+
for update in updates:
|
|
141
|
+
for field in ("message", "channel_post", "edited_message"):
|
|
142
|
+
chat = (update.get(field) or {}).get("chat") or {}
|
|
143
|
+
if chat.get("id") is not None and chat["id"] not in ids:
|
|
144
|
+
ids.append(chat["id"])
|
|
145
|
+
return ids
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def ask(label, default=""):
|
|
149
|
+
"""A required line of text, with the current value offered as default."""
|
|
150
|
+
hint = (" [" + default + "]") if default else ""
|
|
151
|
+
while True:
|
|
152
|
+
value = input(label + hint + ": ").strip()
|
|
153
|
+
if value:
|
|
154
|
+
return value
|
|
155
|
+
if default:
|
|
156
|
+
return default
|
|
157
|
+
print("This one is needed.")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def ask_optional(label, default=""):
|
|
161
|
+
"""A line of text that may be left empty."""
|
|
162
|
+
hint = (" [" + default + "]") if default else ""
|
|
163
|
+
value = input(label + hint + ": ").strip()
|
|
164
|
+
return value or default
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def ask_secret(label, default=""):
|
|
168
|
+
hint = " [kept]" if default else ""
|
|
169
|
+
while True:
|
|
170
|
+
value = getpass.getpass(label + hint + ": ").strip()
|
|
171
|
+
if value:
|
|
172
|
+
return value
|
|
173
|
+
if default:
|
|
174
|
+
return default
|
|
175
|
+
print("This one is needed.")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def ask_optional_secret(label):
|
|
179
|
+
"""A secret that may be left empty."""
|
|
180
|
+
value = getpass.getpass(label + " (optional, Enter to skip): ").strip()
|
|
181
|
+
return value
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def ask_chat_id(token, username):
|
|
185
|
+
"""The Telegram chat to alert: typed in, or found from a message to the bot."""
|
|
186
|
+
while True:
|
|
187
|
+
raw = input("Open @%s, press Start, then press Enter here "
|
|
188
|
+
"(or paste a chat id): " % username).strip()
|
|
189
|
+
if raw and re.fullmatch(r"-?\d+", raw):
|
|
190
|
+
return raw
|
|
191
|
+
try:
|
|
192
|
+
found = telegram_chat_ids(token)
|
|
193
|
+
except SetupError as e:
|
|
194
|
+
print(str(e))
|
|
195
|
+
found = []
|
|
196
|
+
if found:
|
|
197
|
+
return str(found[-1])
|
|
198
|
+
print("No chat id yet. Send /start to @%s and press Enter again, "
|
|
199
|
+
"or paste the chat id." % username)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def defaults():
|
|
203
|
+
"""The values already on this machine, so the wizard can pre-fill them."""
|
|
204
|
+
out = dict(os.environ)
|
|
205
|
+
for path in env_files():
|
|
206
|
+
try:
|
|
207
|
+
with open(path, encoding="utf-8") as f:
|
|
208
|
+
for line in f:
|
|
209
|
+
line = line.strip()
|
|
210
|
+
if line and not line.startswith("#") and "=" in line:
|
|
211
|
+
k, _, v = line.partition("=")
|
|
212
|
+
out.setdefault(k.strip(), v.strip())
|
|
213
|
+
except OSError:
|
|
214
|
+
pass
|
|
215
|
+
return out
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def targets():
|
|
219
|
+
"""Where the credentials go: the home copy, plus a .env already in the folder."""
|
|
220
|
+
out = [HOME_ENV]
|
|
221
|
+
local = os.path.join(STATE_DIR, ".env")
|
|
222
|
+
if os.path.isfile(local):
|
|
223
|
+
out.append(local)
|
|
224
|
+
return out
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def main():
|
|
228
|
+
"""Walk the user through the credentials and leave a working .env behind."""
|
|
229
|
+
print("Polyester setup")
|
|
230
|
+
print("Press Enter to keep the value in brackets. Testnet only.\n")
|
|
231
|
+
|
|
232
|
+
have = defaults()
|
|
233
|
+
key_id = ask("Polyester API key id (ak_...)",
|
|
234
|
+
have.get("POLYESTER_API_KEY_ID", ""))
|
|
235
|
+
seed = normalize_seed(ask_secret(
|
|
236
|
+
"Ed25519 seed (64 hex characters or base64)",
|
|
237
|
+
have.get("POLYESTER_ED25519_SEED", "")))
|
|
238
|
+
|
|
239
|
+
me = signed_get("/v1/auth/me", key_id, seed)
|
|
240
|
+
account_id = str(me.get("accountId") or "")
|
|
241
|
+
if not account_id:
|
|
242
|
+
raise SetupError("the exchange returned no account id: "
|
|
243
|
+
+ json.dumps(me)[:200])
|
|
244
|
+
print("\nExchange ok: account %s (%s)"
|
|
245
|
+
% (account_id, me.get("username", "unknown user")))
|
|
246
|
+
|
|
247
|
+
token = ask_secret("Telegram bot token",
|
|
248
|
+
have.get("TELEGRAM_BOT_TOKEN", ""))
|
|
249
|
+
bot = telegram("getMe", token)
|
|
250
|
+
username = bot.get("username", "your bot")
|
|
251
|
+
print("Telegram ok: @%s" % username)
|
|
252
|
+
|
|
253
|
+
while True:
|
|
254
|
+
chat_id = ask_chat_id(token, username)
|
|
255
|
+
try:
|
|
256
|
+
telegram("sendMessage", token, chat_id=chat_id,
|
|
257
|
+
text="Polyester setup: this chat gets the bot alerts.")
|
|
258
|
+
except SetupError as e:
|
|
259
|
+
print(str(e))
|
|
260
|
+
print("Give another chat id.")
|
|
261
|
+
continue
|
|
262
|
+
break
|
|
263
|
+
print("Telegram chat ok: %s" % chat_id)
|
|
264
|
+
|
|
265
|
+
values = {
|
|
266
|
+
"POLYESTER_API_KEY_ID": key_id,
|
|
267
|
+
"POLYESTER_ED25519_SEED": seed,
|
|
268
|
+
"POLYESTER_ACCOUNT_ID": account_id,
|
|
269
|
+
"TELEGRAM_BOT_TOKEN": token,
|
|
270
|
+
"TELEGRAM_CHAT_ID": chat_id,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private_key = ask_optional_secret(
|
|
274
|
+
"EOA private key (only for the daily paper-asset claim)")
|
|
275
|
+
if private_key:
|
|
276
|
+
values["POLYESTER_PRIVATE_KEY"] = private_key
|
|
277
|
+
values["POLYESTER_SMART_ACCOUNT"] = ask_optional(
|
|
278
|
+
"Smart account address (0x...)",
|
|
279
|
+
str(me.get("rootSmartAccountAddress") or ""))
|
|
280
|
+
|
|
281
|
+
for target in targets():
|
|
282
|
+
merge_env(target, values)
|
|
283
|
+
print("\nWrote %s" % " and ".join(targets()))
|
|
284
|
+
print("Start the bot with: polyester-bot")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def cli():
|
|
288
|
+
"""Run the wizard and turn any failure into a one-line error."""
|
|
289
|
+
try:
|
|
290
|
+
main()
|
|
291
|
+
except SetupError as e:
|
|
292
|
+
print("SETUP FAILED: " + str(e), file=sys.stderr)
|
|
293
|
+
sys.exit(1)
|
|
294
|
+
except (KeyboardInterrupt, EOFError):
|
|
295
|
+
print("\nSetup cancelled.", file=sys.stderr)
|
|
296
|
+
sys.exit(1)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
cli()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""One-shot: show the bot's open quotes and the current hot-pairs signal."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
if __package__:
|
|
7
|
+
from . import check_orders # noqa: F401
|
|
8
|
+
from .paths import state_file
|
|
9
|
+
else:
|
|
10
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
11
|
+
import check_orders # noqa: F401
|
|
12
|
+
from paths import state_file
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
with open(state_file("hot_pairs.json"), encoding="utf-8") as f:
|
|
16
|
+
hot = json.load(f)
|
|
17
|
+
if hot:
|
|
18
|
+
print("hot pairs:", json.dumps(hot))
|
|
19
|
+
else:
|
|
20
|
+
print("hot pairs: none (all quiet)")
|
|
21
|
+
except OSError:
|
|
22
|
+
print("hot pairs: no scan yet")
|
package/install.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polyester-bot",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Polyester testnet gap hunter: buys orderbook gaps, asks them back, alerts on Telegram",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"polyester",
|
|
7
|
+
"testnet",
|
|
8
|
+
"trading",
|
|
9
|
+
"market-making",
|
|
10
|
+
"bot",
|
|
11
|
+
"telegram",
|
|
12
|
+
"crypto"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/natanim-kemal/polyester-bot",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/natanim-kemal/polyester-bot.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/natanim-kemal/polyester-bot/issues"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "natanim-kemal",
|
|
24
|
+
"bin": {
|
|
25
|
+
"polyester-bot": "bin/polyester-bot.js",
|
|
26
|
+
"polyester-farm": "bin/polyester-farm.js"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"postinstall": "node install.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"bin/",
|
|
33
|
+
"install.js",
|
|
34
|
+
"pyproject.toml",
|
|
35
|
+
"bot/*.py",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"README.md",
|
|
38
|
+
"polyester-bot.png"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=14"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
Binary file
|
package/pyproject.toml
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "polyester-bot"
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
description = "Polyester testnet gap hunter: buys orderbook gaps, asks them back, alerts on Telegram"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "natanim-kemal" }]
|
|
14
|
+
keywords = ["polyester", "testnet", "trading", "market-making", "bot", "telegram"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: End Users/Desktop",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"polyester-sdk>=0.6.0,<0.7",
|
|
23
|
+
"pynacl>=1.5",
|
|
24
|
+
"eth-account>=0.13",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/natanim-kemal/polyester-bot"
|
|
29
|
+
Repository = "https://github.com/natanim-kemal/polyester-bot"
|
|
30
|
+
Issues = "https://github.com/natanim-kemal/polyester-bot/issues"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
polyester-bot = "polyester_bot.monitor:cli"
|
|
34
|
+
polyester-farm = "polyester_bot.polyester_farm:main"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools]
|
|
37
|
+
packages = ["polyester_bot"]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.package-dir]
|
|
40
|
+
polyester_bot = "bot"
|