polyester-bot 0.0.0-stage → 1.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 natanim-kemal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,302 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <img src="https://raw.githubusercontent.com/natanim-kemal/polyester-bot/main/polyester-bot.png" alt="Polyester" width="120" height="120" style="border-radius:50%; object-fit:cover;" />
4
+
5
+ # Polyester Gap Hunter
6
+
7
+ **Testnet market-making bot · one position at a time · Telegram alerts · GitHub Actions deploy**
8
+
9
+ ![Python](https://img.shields.io/badge/Python-3.13-blue?logo=python)
10
+ ![asyncio](https://img.shields.io/badge/asyncio-trading-2b5f8a?logo=asyncio)
11
+ ![systemd](https://img.shields.io/badge/systemd-service-81dafb?logo=systemd)
12
+ ![GitHub Actions](https://img.shields.io/badge/deploy-GitHub_Actions-2088ff?logo=github-actions)
13
+
14
+ </div>
15
+
16
+ ## How it works
17
+
18
+ ```
19
+ scan pairs → find gap → BUY at the bid → ASK under the offer → CYCLE DONE
20
+ ```
21
+
22
+ - **Gap mode** — buys the bid, asks one tick under the offer (maker on both legs).
23
+ Fees are read from the exchange (VIP tier) every 30 min; the gap floor follows
24
+ them (`2 x maker + GAP_MARGIN_PCT`, `0.11%` at VIP3). An explicit `GAP_MIN_PCT`
25
+ pins it instead.
26
+ - **Volume fallback** — when nothing clears the fee bar, the deepest tightest pair is
27
+ cycled anyway to keep season volume up. The candidate with real trade flow wins
28
+ over the widest dead spread. A saturation ladder cuts the waiting:
29
+ after `SAT_PENNY_AFTER` consecutive unfilled fallback buys it pennies the bid,
30
+ after `SAT_TAKER_AFTER` it buys at the ask (taker fee), and the buy timeout
31
+ shrinks to `FALLBACK_BUY_TIMEOUT`.
32
+ - **Chase rule (both sides)** — if `BETTER_ASKS_LIMIT` better asks (sell) or
33
+ `BETTER_BIDS_LIMIT` better bids (buy) beat our price for `PERSIST_SECONDS`,
34
+ the resting order is **amended** one tick more aggressive than them — one
35
+ atomic call, no unfilled window. A cancel happens only when the amend cannot.
36
+ - **Dead-man switch** — `orders.cancel_all_after` is re-armed every 60 s; if the
37
+ bot dies or freezes without re-arming, the exchange cancels all our orders itself.
38
+ - **Pre-flight check** — before a buy is placed, `orders.preview_order` asks the
39
+ exchange whether it would be admissible; only a clear rejection blocks
40
+ (throttled, never an outage blocking trades).
41
+ - **Websocket books** — every pair's orderbook is streamed live (`orderbook.subscribe`);
42
+ readers prefer the fresh cache and fall back to REST when it goes quiet.
43
+ - **One position** — never stacks trades; the exception is a `>20%` 24h mover.
44
+ - **Volume tracker** — every cycle adds to `volume_state.json`, milestone alert
45
+ every 10,000 USDT.
46
+ - **Daily equity line** — once a day one `EQUITY` alert: total in USDT (balances ×
47
+ fresh mids), per-position breakdown, 24 h delta vs yesterday's snapshot
48
+ (`equity_state.json`).
49
+ - **TP triggers** — `TP_TRIGGER_PCT` arms a full-size server-side take-profit on a held
50
+ position; the trigger reserves the whole inventory while armed, so the bot cannot
51
+ sell alongside it. Off by default.
52
+
53
+ ## Install
54
+
55
+ **npm** — the wrapper builds its own Python environment on first run:
56
+
57
+ ```bash
58
+ npm install -g polyester-bot
59
+ polyester-bot setup
60
+ ```
61
+
62
+ **pip** — straight from the release tag:
63
+
64
+ ```bash
65
+ pip install "polyester-bot @ git+https://github.com/natanim-kemal/polyester-bot.git@v1.1.0"
66
+ polyester-bot setup
67
+ ```
68
+
69
+ **source**:
70
+
71
+ ```bash
72
+ git clone https://github.com/natanim-kemal/polyester-bot.git
73
+ cd polyester-bot
74
+ pip install -e .
75
+ polyester-bot setup
76
+ ```
77
+
78
+ Python 3.11 or newer.
79
+
80
+ ## Setup
81
+
82
+ One command walks you through the credentials and leaves a working `.env` behind:
83
+
84
+ ```bash
85
+ polyester-bot setup
86
+ ```
87
+
88
+ It asks for:
89
+
90
+ | Question | Where to get it |
91
+ | --- | --- |
92
+ | Polyester API key id (`ak_...`) | Polyester dashboard → API keys |
93
+ | Ed25519 seed (64 hex chars or base64) | the same API key, shown once when you create it |
94
+ | Telegram bot token | [@BotFather](https://t.me/BotFather) → `/newbot` |
95
+ | Telegram chat id | send `/start` to your bot; the wizard finds it, or paste the id |
96
+ | EOA private key | optional, only for the daily paper-asset claim |
97
+
98
+ The account id is **not** asked for: the wizard signs a `GET /v1/auth/me` with
99
+ your key and derives it. The exchange and Telegram are both checked before
100
+ anything is written, and the wizard sends one test message so you know the
101
+ alerts will arrive.
102
+
103
+ Credentials land in `~/.polyester-bot/.env` (and in a `.env` next to the bot if
104
+ one is already there), never in the git repository. The bot reads the working
105
+ directory first, then the home copy, so it works from anywhere.
106
+
107
+ Run the bot the same way:
108
+
109
+ ```bash
110
+ polyester-bot
111
+ ```
112
+
113
+ `POLYESTER_STATE_DIR` moves the working directory somewhere else. The bot trades
114
+ **testnet** funds only.
115
+
116
+ ## Project Structure
117
+
118
+ ```
119
+ ├── bot/
120
+ │ ├── monitor.py # the bot: scanner, executor, chase rule
121
+ │ ├── setup.py # guided setup: ask, check, write the .env
122
+ │ ├── polyester_farm.py # season volume, daily paper-asset claim
123
+ │ ├── paths.py # where .env and the state files live
124
+ │ ├── check_orders.py # print open orders
125
+ │ ├── cancel_all.py # cancel every open order
126
+ │ ├── status_all.py # open orders + hot pairs
127
+ │ ├── scan_spread.py # spread of every pair
128
+ │ ├── big_buy.py # one-off market buy
129
+ │ ├── hunter_forever.py # run monitor.py in a loop
130
+ │ ├── kill_bot.py # stop the bot
131
+ │ └── .env.example # every setting, no secrets
132
+ ├── bin/ # npm wrapper: polyester-bot, polyester-farm
133
+ ├── deploy/
134
+ │ ├── polyester-bot.service # systemd unit for the bot
135
+ │ ├── polyester-claim.service # daily paper-asset claim
136
+ │ ├── polyester-claim.timer # 00:10 UTC, persistent
137
+ │ └── deploy.sh # install code + units, restart
138
+ ├── tests/ # 14 suites, all run in CI before any deploy:
139
+ │ ├── test_sell_loop.py # the re-ask/cancel machine, end to end
140
+ │ ├── test_freeze_detect.py # detects a paused sandbox from clock skew
141
+ │ ├── test_paths.py # .env and state file resolution
142
+ │ ├── test_stuck.py # a filled buy survives a lagging balance
143
+ │ ├── test_setup.py # the setup wizard and account derivation
144
+ │ ├── test_fees.py # fee math, gap floor and entry ladder
145
+ │ ├── test_chase.py # both sides chase; sell trigger on one better ask
146
+ │ ├── test_amend.py # chase amends in place, cancel only as fallback
147
+ │ ├── test_tp_trigger.py # server-side take-profit triggers (opt-in)
148
+ │ ├── test_ws.py # websocket orderbook feed with REST fallback
149
+ │ ├── test_liquidity.py # fallback prefers a live tape over a wide dead book
150
+ │ ├── test_equity.py # daily equity report from balances x mids
151
+ │ ├── test_preflight.py # admissibility check before a buy is placed
152
+ │ └── test_orphan.py # orphan-clear runs hourly, not every poll
153
+ ├── .github/workflows/
154
+ │ ├── deploy.yml # push to main → deploy to the sandbox
155
+ │ └── keepalive.yml # every 10 min: keep the sandbox awake
156
+ ├── pyproject.toml # the Python package (pip install .)
157
+ ├── package.json # the npm package (npm install -g polyester-bot)
158
+ └── polyester-bot.png
159
+ ```
160
+
161
+ ## Configuration
162
+
163
+ `bot/.env` holds the credentials and tuning; `bot/.env.example` lists every key.
164
+ The real file stays on the machine and is gitignored — a deploy never overwrites it.
165
+
166
+ | Key | Default | Meaning |
167
+ | --- | --- | --- |
168
+ | `GAP_MIN_PCT` | fee-driven (`0.11`) | minimum spread to take in gap mode |
169
+ | `GAP_MARGIN_PCT` | `0.01` | profit margin added on top of the round-trip fee |
170
+ | `FEES_REFRESH_SECONDS` | `1800` | how often per-pair fees are re-read from the exchange |
171
+ | `DEPTH_MIN_USD` | `50` | minimum depth on both sides |
172
+ | `SCAN_SECONDS` | `10` | how often to scan for a gap |
173
+ | `POLL_SECONDS` | `2` | how often to manage the open position |
174
+ | `PERSIST_SECONDS` | `60` | how long better prices must persist before a chase fires |
175
+ | `BETTER_ASKS_LIMIT` | `1` | better asks under our ask that trigger the sell chase |
176
+ | `BETTER_BIDS_LIMIT` | `1` | better bids over our bid that trigger the buy chase |
177
+ | `BUY_TIMEOUT_SECONDS` | `180` | cancel an unfilled buy after this long |
178
+ | `SAT_PENNY_AFTER` | `2` | consecutive fallback buy timeouts before penny bids |
179
+ | `SAT_TAKER_AFTER` | `4` | consecutive fallback buy timeouts before buying at the ask |
180
+ | `FALLBACK_BUY_TIMEOUT` | `60` | fallback buy timeout once the ladder is engaged |
181
+ | `DEADMAN_TIMEOUT` | `300` | auto-cancel window re-armed every 60s; `0` disables |
182
+ | `TP_TRIGGER_PCT` | `0` | arm server-side take-profit on sell entry; `0` = never arm |
183
+ | `FREEZE_SECONDS` | `300` | wall-vs-monotonic clock skew that counts as a paused sandbox |
184
+ | `STALL_SECONDS` | `900` | how long the loop may hang before it counts as a pause |
185
+ | `FALLBACK_*` | see example | volume fallback mode |
186
+ | `VOLUME_BASELINE` | `57000` | season volume to start from with no state file |
187
+
188
+ Alerts go to **Telegram** (every alert) and **Discord** if `DISCORD_WEBHOOK_URL`
189
+ is set. Everything is also printed to the journal.
190
+
191
+ Every Telegram alert is three lines — the action, the market with its spread,
192
+ then the money:
193
+
194
+ ```
195
+ 🟢 BUY
196
+ BTC-USDT | gap 0.85% (net +0.65%) | 0.0123 @ 41,250.00 | depth 12,450.00/12,556.00
197
+ expected +8.20 USDT | USDT 3,094.10 | BTC 0.0123
198
+ ```
199
+
200
+ ```
201
+ 🔵 ASK
202
+ ARB-USDT | gap 0.118% | 8,946.0 @ 0.167 | bought @ 0.167
203
+ expected +1.21 USDT | USDT 2,983.94 | ARB 8,946.35
204
+ ```
205
+
206
+ `🟡 VOLUME BUY`, `✅ BUY FILLED`, `✅ CYCLE DONE`, `🏆 MILESTONE`, `❄️ FREEZE
207
+ DETECTED` and `⚠️` warnings use the same shape.
208
+
209
+ ## Deploy
210
+
211
+ Push to `main` → GitHub Actions → SSH → `deploy/deploy.sh` → systemd restart → verify.
212
+
213
+ | Name | Kind | Value |
214
+ | --- | --- | --- |
215
+ | `INSTABOX_SSH_KEY` | secret | private key with access to the sandbox |
216
+ | `INSTABOX_HOST` | variable | `ssh.instabox.sh` |
217
+ | `INSTABOX_PORT` | variable | `2222` |
218
+ | `INSTABOX_USER` | variable | sandbox user |
219
+
220
+ Secrets, `.session` and `volume_state.json` are never touched by a deploy.
221
+
222
+ `keepalive.yml` runs every 10 minutes and opens an SSH session: the sandbox pauses
223
+ itself after 30 idle minutes, and SSH traffic counts as activity, so it stays up.
224
+ It also restarts the bot when its heartbeat file (`/var/tmp/polyester.heartbeat`)
225
+ goes stale, which catches a bot that is alive but wedged.
226
+
227
+ The bot defends itself as well. A paused sandbox stops the guest clock, so the
228
+ wall clock jumps while the monotonic clock does not; when the bot sees that
229
+ skew (or a loop that hung for minutes), it cancels its **own** stale orders,
230
+ re-reads the balances and re-adopts the real position. If the exchange is
231
+ unreachable it keeps the position it has and retries on the next poll instead
232
+ of trading on stale state.
233
+
234
+ ## Daily claim
235
+
236
+ `polyester-claim.timer` claims the daily paper asset at **00:10 UTC** with
237
+ `Persistent=true`, so a run missed while the machine was down happens on next boot.
238
+ Whatever it pays out arrives as extra balances, which the bot adopts and sells back
239
+ into USDT before looking for the next gap.
240
+
241
+ ```bash
242
+ systemctl list-timers polyester-claim.timer
243
+ journalctl -u polyester-claim.service
244
+ ```
245
+
246
+ ## Tests
247
+
248
+ All 14 suites run in CI on every push, before anything is deployed, together with a
249
+ build of the Python package and a smoke test of its entry point.
250
+ `python tests/test_<name>.py` runs any one locally.
251
+
252
+ ```bash
253
+ for t in tests/test_*.py; do python "$t" || exit 1; done
254
+ ```
255
+
256
+ Exits non-zero on the first failing suite. Coverage map:
257
+
258
+ `test_sell_loop.py` replays the sell state machine against a fake exchange — the
259
+ bug where the bot cancelled the ask it had just placed.
260
+ `test_freeze_detect.py` drives the freeze guard — the real 1h47m pause triggers a
261
+ re-sync, an unreachable exchange keeps the position.
262
+ `test_stuck.py` replays the bug where a filled buy was invisible because the
263
+ balance lagged behind the order.
264
+ `test_setup.py` checks the setup wizard and that the account id is derived, never
265
+ baked in.
266
+ `test_paths.py` checks `.env` and state file resolution.
267
+ `test_fees.py` pins the fee math: the gap floor, the entry ladder, cycle fees.
268
+ `test_chase.py` checks both sides chase one tick past the competitor, and the
269
+ flicking-competitor guard.
270
+ `test_amend.py` checks orders amend atomically and cancel only when unavoidable.
271
+ `test_tp_trigger.py` arms a server-side take-profit when enabled (off by default).
272
+ `test_ws.py` checks the websocket book feed populates the cache and REST falls back.
273
+ `test_liquidity.py` checks the fallback prefers a live tape.
274
+ `test_equity.py` checks the daily equity report and its snapshot file.
275
+ `test_preflight.py` checks the admissibility check blocks a clear rejection only.
276
+ `test_orphan.py` checks the orphan-clear runs hourly, not every poll.
277
+
278
+ ## Ops
279
+
280
+ | Script | What it does |
281
+ | --- | --- |
282
+ | `check_orders.py` | print all open orders with client ids |
283
+ | `cancel_all.py` | cancel every open order |
284
+ | `status_all.py` | open orders plus hot pairs |
285
+ | `scan_spread.py` | print the spread of every pair |
286
+ | `big_buy.py` | buy a fixed notional at market |
287
+ | `hunter_forever.py` | keep `monitor.py` running |
288
+ | `kill_bot.py` | stop the bot processes |
289
+
290
+ ## Remote / Links
291
+
292
+ - GitHub: [@natanim-kemal](https://github.com/natanim-kemal)
293
+ - Telegram: [@phnatanim](https://t.me/phnatanim)
294
+ - Email: reachnatanim@gmail.com
295
+
296
+ ---
297
+
298
+ <div align="center">
299
+
300
+ Built for the <a href="https://polyester.com">Polyester</a> testnet by <a href="https://github.com/natanim-kemal">@natanim-kemal</a>
301
+
302
+ </div>
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const args = process.argv.slice(2);
5
+ const target = args[0] === "setup" ? "polyester_bot.setup" : "polyester_bot.monitor";
6
+
7
+ require("./venv").run(target, args[0] === "setup" ? args.slice(1) : args);
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ require("./venv").run("polyester_bot.polyester_farm", process.argv.slice(2));
package/bin/setup.js ADDED
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+
3
+ const { spawnSync } = require("child_process");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const root = path.join(__dirname, "..");
8
+ const venv = path.join(root, ".venv");
9
+
10
+ function python() {
11
+ return process.platform === "win32"
12
+ ? path.join(venv, "Scripts", "python.exe")
13
+ : path.join(venv, "bin", "python");
14
+ }
15
+
16
+ function run(cmd, args, label) {
17
+ const r = spawnSync(cmd, args, {
18
+ stdio: ["ignore", "pipe", "pipe"],
19
+ encoding: "utf8"
20
+ });
21
+ if (r.status !== 0) {
22
+ console.error("polyester-bot: " + label + " failed");
23
+ if (r.stderr) console.error(r.stderr.trim());
24
+ console.error("Install it by hand:");
25
+ console.error(" python3 -m venv .venv && .venv/bin/pip install .");
26
+ process.exit(1);
27
+ }
28
+ return r.stdout || "";
29
+ }
30
+
31
+ function findPython() {
32
+ const candidates = process.platform === "win32"
33
+ ? [["py", ["-3"]], ["python", []], ["python3", []]]
34
+ : [["python3", []], ["python", []]];
35
+ for (const [cmd, args] of candidates) {
36
+ const probe = spawnSync(
37
+ cmd,
38
+ args.concat(["-c", "import sys; print(sys.version_info >= (3, 11))"]),
39
+ { encoding: "utf8" }
40
+ );
41
+ if (probe.status === 0 && probe.stdout.trim() === "True") {
42
+ return [cmd, args];
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ function ready() {
49
+ if (!fs.existsSync(python())) {
50
+ return false;
51
+ }
52
+ return spawnSync(python(), ["-c", "import polyester_bot"], { encoding: "utf8" }).status === 0;
53
+ }
54
+
55
+ function ensurePython() {
56
+ if (ready()) {
57
+ return python();
58
+ }
59
+ const found = findPython();
60
+ if (!found) {
61
+ console.error("polyester-bot: Python 3.11 or newer is required and was not found.");
62
+ console.error("Install Python 3.11+, then run: npm rebuild polyester-bot");
63
+ process.exit(1);
64
+ }
65
+ console.log("polyester-bot: setting up the Python environment (takes a minute)...");
66
+ run(found[0], found[1].concat(["-m", "venv", venv]),
67
+ "creating the Python environment");
68
+ run(python(), ["-m", "pip", "install", "--upgrade", "--quiet", "pip"],
69
+ "upgrading pip");
70
+ run(python(), ["-m", "pip", "install", "--quiet", root],
71
+ "installing the bot");
72
+ return python();
73
+ }
74
+
75
+ module.exports = { root, venv, python, ensurePython };
package/bin/venv.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+
3
+ const { spawnSync } = require("child_process");
4
+ const { ensurePython } = require("./setup");
5
+
6
+ function run(moduleName, args) {
7
+ const py = ensurePython();
8
+ const r = spawnSync(py, ["-m", moduleName].concat(args), { stdio: "inherit" });
9
+ process.exit(r.status === null ? 1 : r.status);
10
+ }
11
+
12
+ module.exports = { run };
@@ -0,0 +1 @@
1
+ """Polyester testnet gap-hunter bot."""
package/bot/big_buy.py ADDED
@@ -0,0 +1,75 @@
1
+ """One-shot: place a full-balance BUY one tick above best bid on BTC-USDT."""
2
+
3
+ import asyncio
4
+ import os
5
+ import uuid
6
+ from decimal import Decimal, ROUND_FLOOR
7
+
8
+ from polyester import POLYESTER_TESTNET_ENVIRONMENT, AsyncPolyester
9
+
10
+ if __package__:
11
+ from .paths import env_files
12
+ else:
13
+ from paths import env_files
14
+
15
+
16
+ def load_env():
17
+ for path in env_files():
18
+ try:
19
+ with open(path, encoding="utf-8") as f:
20
+ for line in f:
21
+ line = line.strip()
22
+ if line and not line.startswith("#") and "=" in line:
23
+ k, _, v = line.partition("=")
24
+ os.environ.setdefault(k.strip(), v.strip())
25
+ except OSError:
26
+ pass
27
+
28
+
29
+ load_env()
30
+
31
+ SYMBOL = "BTC-USDT"
32
+ STEP = Decimal("0.00000001")
33
+ FEE_BUFFER = Decimal("1.0015")
34
+
35
+ CID = "man1-" + uuid.uuid4().hex[:16]
36
+
37
+
38
+ async def main():
39
+ c = AsyncPolyester(environment=POLYESTER_TESTNET_ENVIRONMENT,
40
+ api_key_id=os.environ["POLYESTER_API_KEY_ID"],
41
+ api_private_key=os.environ["POLYESTER_ED25519_SEED"],
42
+ default_account_id=os.environ["POLYESTER_ACCOUNT_ID"])
43
+ try:
44
+ await c.wait_for_catalogs()
45
+ b = await c.balances.list()
46
+ usdt = next(a for a in b.balances if a.asset_id == 1)
47
+ avail = Decimal(usdt.available) / Decimal(10 ** 18)
48
+
49
+ book = await c.orderbook.get(symbol=SYMBOL, depth=1)
50
+ bid = book.bids[0].price.as_decimal()
51
+ price = bid + Decimal("0.01")
52
+
53
+ qty = (avail / (price * FEE_BUFFER)).quantize(STEP, rounding=ROUND_FLOOR)
54
+ notional = qty * price
55
+
56
+ print(f"USDT available : {avail}")
57
+ print(f"best bid : {bid}")
58
+ print(f"order price : {price} (bid + 1 tick)")
59
+ print(f"order qty : {qty} BTC")
60
+ print(f"notional : ~{notional.quantize(Decimal('0.01'))} USDT")
61
+ print(f"maker fee 0.1% : ~{(notional * Decimal('0.001')).quantize(Decimal('0.01'))} USDT on fill")
62
+
63
+ res = await c.orders.batch_create(symbol=SYMBOL, items=[{
64
+ "symbol": SYMBOL, "side": "buy", "order_type": "limit", "tif": "gtc",
65
+ "qty": format(qty, "f"), "price": format(price, "f"),
66
+ "client_order_id": CID,
67
+ }], request_id="man1-" + uuid.uuid4().hex[:16])
68
+ for item in res.results:
69
+ print("RESULT:", item.status, getattr(item, "code", "") or "",
70
+ getattr(item, "order_id", ""))
71
+ finally:
72
+ await c.aclose()
73
+
74
+
75
+ asyncio.run(main())
@@ -0,0 +1,72 @@
1
+ """List ALL open orders on the account, cancel everything, verify empty."""
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import time
7
+ import urllib.request
8
+ import urllib.error
9
+
10
+ import nacl.signing
11
+
12
+ if __package__:
13
+ from .paths import env_files
14
+ else:
15
+ from paths import env_files
16
+
17
+ for path in env_files():
18
+ with open(path, encoding="utf-8") as f:
19
+ for line in f:
20
+ line = line.strip()
21
+ if line and not line.startswith("#") and "=" in line:
22
+ k, _, v = line.partition("=")
23
+ os.environ.setdefault(k.strip(), v.strip())
24
+
25
+ KEY = os.environ["POLYESTER_API_KEY_ID"]
26
+ sk = nacl.signing.SigningKey(bytes.fromhex(os.environ["POLYESTER_ED25519_SEED"]))
27
+ UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
28
+ "(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36")
29
+ API = "https://api.testnet.polyester.com"
30
+
31
+
32
+ def signed(method, path, query="", body=b""):
33
+ ts = str(int(time.time() * 1000))
34
+ canon = "\n".join([ts, method.upper(), path, query,
35
+ hashlib.sha256(body).hexdigest()])
36
+ req = urllib.request.Request(API + path + (("?") + query if query else ""),
37
+ data=body or None, method=method)
38
+ req.add_header("User-Agent", UA)
39
+ req.add_header("Accept", "application/json")
40
+ req.add_header("Origin", "https://testnet.polyester.com")
41
+ if body:
42
+ req.add_header("Content-Type", "application/json")
43
+ req.add_header("X-API-KEY-ID", KEY)
44
+ req.add_header("X-API-TIMESTAMP", ts)
45
+ req.add_header("X-API-SIGNATURE", sk.sign(canon.encode()).signature.hex())
46
+ try:
47
+ with urllib.request.urlopen(req, timeout=30) as r:
48
+ return r.status, json.loads(r.read() or b"{}")
49
+ except urllib.error.HTTPError as e:
50
+ return e.code, {"raw": e.read().decode(errors="replace")[:200]}
51
+
52
+
53
+ s, before = signed("GET", "/v1/orders/open")
54
+ orders = before.get("orders", []) if isinstance(before, dict) else []
55
+ print(f"open orders BEFORE: {len(orders)}")
56
+ for o in orders:
57
+ print(" ", o.get("orderId"), o.get("symbol"), o.get("side"), o.get("origQty"),
58
+ "@", o.get("price"), o.get("status"), o.get("clientOrderId", "")[:14])
59
+
60
+ if orders:
61
+ import uuid
62
+ body = json.dumps({"requestId": "manual-" + uuid.uuid4().hex[:12]}).encode()
63
+ s, res = signed("POST", "/v1/orders/cancel-all", body=body)
64
+ print("cancel-all:", s, str(res)[:200])
65
+ time.sleep(2)
66
+ s, after = signed("GET", "/v1/orders/open")
67
+ left = after.get("orders", []) if isinstance(after, dict) else []
68
+ print(f"open orders AFTER: {len(left)}")
69
+ for o in left:
70
+ print(" ", o.get("orderId"), o.get("symbol"), o.get("side"), o.get("status"))
71
+ else:
72
+ print("nothing open — nothing to cancel")
@@ -0,0 +1,47 @@
1
+ """One-shot check: list the bot's open BTC-USDT quotes."""
2
+ import hashlib
3
+ import json
4
+ import os
5
+ import time
6
+ import urllib.request
7
+ import urllib.error
8
+
9
+ import nacl.signing
10
+
11
+ if __package__:
12
+ from .paths import env_files
13
+ else:
14
+ from paths import env_files
15
+
16
+ for path in env_files():
17
+ with open(path, encoding="utf-8") as f:
18
+ for line in f:
19
+ line = line.strip()
20
+ if line and not line.startswith("#") and "=" in line:
21
+ k, _, v = line.partition("=")
22
+ os.environ.setdefault(k.strip(), v.strip())
23
+
24
+ KEY = os.environ["POLYESTER_API_KEY_ID"]
25
+ sk = nacl.signing.SigningKey(bytes.fromhex(os.environ["POLYESTER_ED25519_SEED"]))
26
+ UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
27
+ "(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36")
28
+ API = "https://api.testnet.polyester.com"
29
+
30
+ path, query = "/v1/orders/open", "symbol=BTC-USDT"
31
+ ts = str(int(time.time() * 1000))
32
+ canon = "\n".join([ts, "GET", path, query, hashlib.sha256(b"").hexdigest()])
33
+ req = urllib.request.Request(f"{API}{path}?{query}", method="GET")
34
+ req.add_header("User-Agent", UA)
35
+ req.add_header("Accept", "application/json")
36
+ req.add_header("Origin", "https://testnet.polyester.com")
37
+ req.add_header("X-API-KEY-ID", KEY)
38
+ req.add_header("X-API-TIMESTAMP", ts)
39
+ req.add_header("X-API-SIGNATURE", sk.sign(canon.encode()).signature.hex())
40
+ try:
41
+ with urllib.request.urlopen(req, timeout=30) as r:
42
+ data = json.loads(r.read())
43
+ for o in data.get("orders", []):
44
+ print(o["side"], o["origQty"], "@", o["price"], o["status"], o["clientOrderId"][:12])
45
+ print(len(data.get("orders", [])), "open orders")
46
+ except urllib.error.HTTPError as e:
47
+ print("ERR", e.code, e.read().decode(errors="replace")[:300])
@@ -0,0 +1,18 @@
1
+ """Supervisor: restart the gap hunter whenever it exits (crash, WiFi drop)."""
2
+
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ import time
7
+
8
+ if __package__:
9
+ from .paths import HERE
10
+ else:
11
+ from paths import HERE
12
+
13
+ print("[supervisor] gap hunter supervisor started", flush=True)
14
+
15
+ while True:
16
+ p = subprocess.run([sys.executable, os.path.join(HERE, "monitor.py")], cwd=HERE)
17
+ print(f"[supervisor] hunter exited ({p.returncode}); restarting in 10s", flush=True)
18
+ time.sleep(10)
@@ -0,0 +1,11 @@
1
+ """Kill the gap hunter, its supervisor, and any leftover bot processes."""
2
+ import subprocess
3
+ import sys
4
+
5
+ ps = r"""
6
+ Get-CimInstance Win32_Process |
7
+ Where-Object { $_.CommandLine -match 'monitor|hunter_forever' -and $_.ProcessId -ne $PID } |
8
+ ForEach-Object { Write-Output $_.ProcessId; Stop-Process -Id $_.ProcessId -Force }
9
+ """
10
+ subprocess.run(["powershell", "-NoProfile", "-Command", ps])
11
+ print("done")