polyester-bot 1.2.0 → 1.2.1
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/README.md +50 -258
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/README.md
CHANGED
|
@@ -1,72 +1,27 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="https://raw.githubusercontent.com/natanim-kemal/polyester-bot/main/polyester-bot.png" alt="Polyester" width="
|
|
3
|
+
<img src="https://raw.githubusercontent.com/natanim-kemal/polyester-bot/main/polyester-bot.png" alt="Polyester" width="90" height="90" style="border-radius:50%;" />
|
|
4
4
|
|
|
5
5
|
# Polyester Gap Hunter
|
|
6
6
|
|
|
7
|
-
**Testnet market-making bot
|
|
8
|
-
|
|
9
|
-

|
|
10
|
-

|
|
11
|
-

|
|
12
|
-

|
|
7
|
+
**Testnet market-making bot. One position at a time. Telegram alerts.**
|
|
13
8
|
|
|
14
9
|
</div>
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
11
|
+
Scans every USDT pair for spreads ≥ `GAP_MIN_PCT`, buys the bid, asks one tick under the
|
|
12
|
+
best offer — or falls back to market-making churn when spreads are below the fee bar, to
|
|
13
|
+
keep season volume up. Fees are live — read from the exchange (your VIP tier) every 30
|
|
14
|
+
minutes — so the gap floor and every cost line use the real rates, not a baked-in 0.20%.
|
|
52
15
|
|
|
53
16
|
## Install
|
|
54
17
|
|
|
55
|
-
**npm** — the wrapper builds its own Python environment on first run:
|
|
56
|
-
|
|
57
18
|
```bash
|
|
58
|
-
npm install -g polyester-bot
|
|
59
|
-
polyester-bot setup
|
|
19
|
+
npm install -g polyester-bot # builds its own Python env on first run
|
|
20
|
+
polyester-bot setup # wizard: key, Telegram, writes ~/.polyester-bot/.env
|
|
21
|
+
polyester-bot # trades testnet
|
|
60
22
|
```
|
|
61
23
|
|
|
62
|
-
|
|
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**:
|
|
24
|
+
Or from source:
|
|
70
25
|
|
|
71
26
|
```bash
|
|
72
27
|
git clone https://github.com/natanim-kemal/polyester-bot.git
|
|
@@ -75,228 +30,65 @@ pip install -e .
|
|
|
75
30
|
polyester-bot setup
|
|
76
31
|
```
|
|
77
32
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
## Setup
|
|
81
|
-
|
|
82
|
-
One command walks you through the credentials and leaves a working `.env` behind:
|
|
33
|
+
or straight from the release tag:
|
|
83
34
|
|
|
84
35
|
```bash
|
|
85
|
-
polyester-bot
|
|
36
|
+
pip install "polyester-bot @ git+https://github.com/natanim-kemal/polyester-bot.git@v1.2.0"
|
|
86
37
|
```
|
|
87
38
|
|
|
88
|
-
|
|
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.
|
|
39
|
+
The wizard asks for your API key id + Ed25519 seed, derives the account id itself, and
|
|
40
|
+
verifies the exchange and Telegram before writing anything.
|
|
102
41
|
|
|
103
|
-
|
|
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
|
|
42
|
+
## How it works
|
|
117
43
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
```
|
|
44
|
+
- **Fees are live** — read from the exchange every 30 min; the gap floor follows them
|
|
45
|
+
(`2 x maker + 0.01` margin, `0.11%` at VIP3). Set `GAP_MIN_PCT` to pin it.
|
|
46
|
+
- **Gap mode** — buys the bid, asks under the offer, bot chasing every 2s:
|
|
47
|
+
two contiguous lines per action, Telegram alerts on fills, skips, milestones.
|
|
48
|
+
- **Chase (both sides)** — orders are **amended** one tick past a competing ask/bid
|
|
49
|
+
(atomic, order id kept), cancel only when the amend cannot.
|
|
50
|
+
- **Volume fallback** — deepest market-maker churn if no profitable gap; escalation
|
|
51
|
+
ladder pennies the bid then crosses the spread as timeouts repeat.
|
|
52
|
+
- **Dead-man switch** — `cancel_all_after` re-arms every 60s; if the VM freezes/dies,
|
|
53
|
+
the exchange cancels everything itself.
|
|
54
|
+
- **Freeze-resilient** — sandbox pause stops the guest clock; wall-vs-monotonic skew
|
|
55
|
+
>`FREEZE_SECONDS` triggers a full re-sync (cancel own orders, adopt balances).
|
|
160
56
|
|
|
161
57
|
## Configuration
|
|
162
58
|
|
|
163
|
-
`bot/.env`
|
|
164
|
-
The
|
|
59
|
+
Everything tunable lives in `bot/.env` (created by `setup`) and documented there.
|
|
60
|
+
The ones that matter most:
|
|
165
61
|
|
|
166
62
|
| Key | Default | Meaning |
|
|
167
|
-
|
|
168
|
-
| `GAP_MIN_PCT` | fee-driven (`0.11`) |
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
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 |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `GAP_MIN_PCT` | fee-driven (`0.11`) | spread to take in gap mode |
|
|
65
|
+
| `DEPTH_MIN_USD` | `50` | min depth each side to enter |
|
|
66
|
+
| `BETTER_ASKS_LIMIT` / `BIDS_LIMIT` | `1` | better prices beating ours to trigger a chase |
|
|
67
|
+
| `PERSIST_SECONDS` | `60` | how long those must hold |
|
|
68
|
+
| `SAT_PENNY_AFTER` / `SAT_TAKER_AFTER` | `2` / `4` | fallback-ladder triggers |
|
|
69
|
+
| `FALLBACK_BUY_TIMEOUT` | `60` | fallback buy timeout on the ladder |
|
|
70
|
+
| `DEADMAN_TIMEOUT` | `120` | silent deadline before the exchange cancels everything |
|
|
71
|
+
| `FREEZE_SECONDS` | `300` | wall-vs-monotonic skew that counts as a paused sandbox |
|
|
72
|
+
| `TP_TRIGGER_PCT` | `0` | optional full-size server-side take-profit (off by default) |
|
|
187
73
|
|
|
188
|
-
Alerts
|
|
189
|
-
is set. Everything is also printed to the journal.
|
|
74
|
+
## Alerts
|
|
190
75
|
|
|
191
|
-
Every
|
|
192
|
-
|
|
76
|
+
Every alert is 1 header + 1 fact line + 1 money line (`🟢 BUY` / `🔵 ASK` / `✅ CYCLE DONE` /
|
|
77
|
+
`🏆 MILESTONE`). Telegram gets everything; journal keeps a copy too.
|
|
193
78
|
|
|
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.
|
|
79
|
+
## Verify
|
|
251
80
|
|
|
252
81
|
```bash
|
|
253
82
|
for t in tests/test_*.py; do python "$t" || exit 1; done
|
|
254
83
|
```
|
|
255
84
|
|
|
256
|
-
|
|
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
|
|
85
|
+
## Release history
|
|
279
86
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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 |
|
|
87
|
+
- **v1.2.0** — live VIP fee rates, buy-side chase with atomic amend, websocket orderbook,
|
|
88
|
+
liquidity-aware fallback, dead-man switch, daily equity line, opt-in TP triggers,
|
|
89
|
+
orphan/cycle fixes (found on the live book: 500M SHIB stuck).
|
|
90
|
+
- v1.1.0 — guided setup wizard; test suites; scanner + re-sync.
|
|
289
91
|
|
|
290
|
-
##
|
|
92
|
+
## License
|
|
291
93
|
|
|
292
|
-
|
|
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>
|
|
94
|
+
MIT
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "polyester-bot"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.1"
|
|
8
8
|
description = "Polyester testnet gap hunter: buys orderbook gaps, asks them back, alerts on Telegram"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|