mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 16:28:06 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a1cf24510 | ||
|
|
fa82d45bfb | ||
|
|
d0154e5721 | ||
|
|
52b6a8a076 | ||
|
|
db6a9cfc68 | ||
|
|
03b8e53d30 |
@@ -31,7 +31,7 @@
|
||||
"packages": {
|
||||
"@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="],
|
||||
|
||||
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-qI/5TaaaCZE4yeSZ83lu0+xi1r88JSxUjnH4OP/iZF7+KKZ75u3ee5isd0LxX+6N8U0npL61YrpbthILHB6BnA=="],
|
||||
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.4", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-HTgrrTgZ9Jgeo6Z3oqbQ7lifOVvRR14vaDuBGPPUxk9Thm+vObaO4QfYYYWw4Zo5CWQDBEfsinFA6Gre+AqwNQ=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="],
|
||||
|
||||
@@ -85,19 +85,19 @@
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="],
|
||||
|
||||
"@grvt/client": ["@grvt/client@1.6.4", "", { "dependencies": { "axios": "^1.12.2" } }, "sha512-yZfEvsC/BtkcdMjoB7Eh6gshBraz/9810qCllvNXDx6OmdxR+NfxPO+ucNlPbyjEt1HfSVzHBWB1yGnVl1usNA=="],
|
||||
"@grvt/client": ["@grvt/client@1.6.25", "", { "dependencies": { "axios": "^1.13.2" } }, "sha512-LG3oZSJDq1Qz6mLiHDmg+v/Hp+Nd+yOp8GIvkBVyIdOcPVzuzC8UQD7IxvS+g5EfQQKLiKzXSjKLc39a11Uj1A=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@nadohq/client": ["@nadohq/client@0.1.0-alpha.41", "", { "dependencies": { "@nadohq/engine-client": "^0.1.0-alpha.41", "@nadohq/indexer-client": "^0.1.0-alpha.41", "@nadohq/shared": "^0.1.0-alpha.41", "@nadohq/trigger-client": "^0.1.0-alpha.41", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-BbJjG32AsaBJtFA9VPH+3/sfR/pUCXVJ0spgairReb+FaZY2rYsasWGPi07y7OROXL7ZS9/LewntC6iu2NVzQg=="],
|
||||
"@nadohq/client": ["@nadohq/client@0.1.0-alpha.45", "", { "dependencies": { "@nadohq/engine-client": "^0.1.0-alpha.45", "@nadohq/indexer-client": "^0.1.0-alpha.45", "@nadohq/shared": "^0.1.0-alpha.45", "@nadohq/trigger-client": "^0.1.0-alpha.45", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-63S7eT5xSk3hxEXRLGDI94mPeu/zd1Nu4oUOZrY8wzhaT7Gl7PVP2snb8JH/PduVQzgs7+qblr4aTBI/j6QvLQ=="],
|
||||
|
||||
"@nadohq/engine-client": ["@nadohq/engine-client@0.1.0-alpha.41", "", { "dependencies": { "@nadohq/shared": "^0.1.0-alpha.41", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-PR2/MaZ2rprd42572UL26B6zBzUFLUVbvAwPFGf6N733RysUxMRixEJmUfSrPVKgZkA4gxqqEhLB0iZA08DBjw=="],
|
||||
"@nadohq/engine-client": ["@nadohq/engine-client@0.1.0-alpha.45", "", { "dependencies": { "@nadohq/shared": "^0.1.0-alpha.45", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-GqaRrsB0Z2EgNY6wu4nG5OTc5hF+buNRMc0+jPLlOQonlPJAC4OiMCgCigm4nJecYwsdiQD05W3Z4YUqggxbyw=="],
|
||||
|
||||
"@nadohq/indexer-client": ["@nadohq/indexer-client@0.1.0-alpha.41", "", { "dependencies": { "@nadohq/engine-client": "^0.1.0-alpha.41", "@nadohq/shared": "^0.1.0-alpha.41", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-5oXM+r3xQmbAHx00MlZmZj64VfcVNNWCtZgJu1Hx+A6F7zZtt0zBmDR6WnY1qWQBM/yZc/xa/Rr3ypXN5VSyQg=="],
|
||||
"@nadohq/indexer-client": ["@nadohq/indexer-client@0.1.0-alpha.45", "", { "dependencies": { "@nadohq/engine-client": "^0.1.0-alpha.45", "@nadohq/shared": "^0.1.0-alpha.45", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-8PzG9taYh380lsKT8i4UvGfhZiPpt+zPisx+ErVqMifyhcJAhgMeYNcfijwMQFNJdgTaBvtI0R5JK98l4Y0YQw=="],
|
||||
|
||||
"@nadohq/shared": ["@nadohq/shared@0.1.0-alpha.41", "", { "peerDependencies": { "bignumber.js": "^9.3.0", "viem": "*" } }, "sha512-c6rRU/9RjIRV0RSHwmliNOE3i3xi65zKGqW9O0M4Oc8a0SQXB5SpskQeSFOaZ1b9BHGi0TYg1xaLBjtYzbysJQ=="],
|
||||
"@nadohq/shared": ["@nadohq/shared@0.1.0-alpha.45", "", { "peerDependencies": { "bignumber.js": "^9.3.0", "viem": "*" } }, "sha512-4Ml8dO9mkwnIidflpunwVMLX2KJA79b53T9xTBkH1Yzy0iOnbEE27r25wvgJgyszU0aznbPBv2++FZ0d0B5Zhg=="],
|
||||
|
||||
"@nadohq/trigger-client": ["@nadohq/trigger-client@0.1.0-alpha.41", "", { "dependencies": { "@nadohq/engine-client": "^0.1.0-alpha.41", "@nadohq/shared": "^0.1.0-alpha.41", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-PxHeqi+aGUHs6S2jHRz2GC9OQD3Mb25sqqwgvDz3EoXWAfv+tYAaSDsLL8FmYEPsbTswjfXm0PoThPDyImyU9A=="],
|
||||
"@nadohq/trigger-client": ["@nadohq/trigger-client@0.1.0-alpha.45", "", { "dependencies": { "@nadohq/engine-client": "^0.1.0-alpha.45", "@nadohq/shared": "^0.1.0-alpha.45", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-Xt8W9zxOZhMvoXSRnx9kUR26wWuHc3EgTVTBbUXt7SlI9v3IP9I3idS6SU3j2f08flcvNqRmC2EbXyYDC2XeyQ=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
|
||||
"@scure/bip39": ["@scure/bip39@1.3.0", "", { "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
|
||||
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
|
||||
|
||||
@@ -171,8 +171,6 @@
|
||||
|
||||
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
|
||||
@@ -189,7 +187,7 @@
|
||||
|
||||
"abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@7.1.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-YdhtCd19sKRKfAAUsrcC1wzm4JuzJoiX4pOJqIoW2qmKj5WzG/dL8uUJ0361zaXtHqK7gEhOwtAtz7t3Yq3X5g=="],
|
||||
"ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
@@ -201,17 +199,17 @@
|
||||
|
||||
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
||||
|
||||
"axios": ["axios@1.12.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw=="],
|
||||
"axios": ["axios@1.13.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
|
||||
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
||||
|
||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"ccxt": ["ccxt@4.5.12", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-2lfL2TKjq4vBkQUQWJfDqFywhvYCZmk9r0SWC8GqA4AHZ6qozKVUJowxQTvdRsLX9jBwYSE0nc7JVurBrQ6SHg=="],
|
||||
"ccxt": ["ccxt@4.5.35", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-aFn1jq/vR9wD3Vxu/2UFoS8snqlvfYn/iYrNhyZZfzB3N+kAHhP5+sAgO6ZwNHkHiUT9IPPahC1ZSFdPtjpIYA=="],
|
||||
|
||||
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
|
||||
|
||||
@@ -223,7 +221,7 @@
|
||||
|
||||
"cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
|
||||
|
||||
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
|
||||
"cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
|
||||
|
||||
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
|
||||
|
||||
@@ -231,15 +229,13 @@
|
||||
|
||||
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.2", "", {}, "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q=="],
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
@@ -297,7 +293,7 @@
|
||||
|
||||
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
|
||||
|
||||
"ink": ["ink@6.3.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-3wGwITGrzL6rkWsi2gEKzgwdafGn4ZYd3u4oRp+sOPvfoxEHlnoB5Vnk9Uy5dMRUhDOqF3hqr4rLQ4lEzBc2sQ=="],
|
||||
"ink": ["ink@6.6.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.2.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^8.1.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-QDt6FgJxgmSxAelcOvOHUvFxbIUjVpCH5bx+Slvc5m7IEcpGt3dYwbz/L+oRnqEGeRvwy1tineKK4ect3nW1vQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
|
||||
|
||||
@@ -325,7 +321,7 @@
|
||||
|
||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"ox": ["ox@0.10.5", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-mXJRiZswmX46abrzNkJpTN9sPJ/Rhevsp5Dfg0z80D55aoLNmEV4oN+/+feSNW593c2CnHavMqSVBanpJ0lUkQ=="],
|
||||
"ox": ["ox@0.11.3", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw=="],
|
||||
|
||||
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
|
||||
|
||||
@@ -341,15 +337,15 @@
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
|
||||
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||
|
||||
"react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="],
|
||||
"react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
||||
|
||||
"rollup": ["rollup@4.52.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.0", "@rollup/rollup-android-arm64": "4.52.0", "@rollup/rollup-darwin-arm64": "4.52.0", "@rollup/rollup-darwin-x64": "4.52.0", "@rollup/rollup-freebsd-arm64": "4.52.0", "@rollup/rollup-freebsd-x64": "4.52.0", "@rollup/rollup-linux-arm-gnueabihf": "4.52.0", "@rollup/rollup-linux-arm-musleabihf": "4.52.0", "@rollup/rollup-linux-arm64-gnu": "4.52.0", "@rollup/rollup-linux-arm64-musl": "4.52.0", "@rollup/rollup-linux-loong64-gnu": "4.52.0", "@rollup/rollup-linux-ppc64-gnu": "4.52.0", "@rollup/rollup-linux-riscv64-gnu": "4.52.0", "@rollup/rollup-linux-riscv64-musl": "4.52.0", "@rollup/rollup-linux-s390x-gnu": "4.52.0", "@rollup/rollup-linux-x64-gnu": "4.52.0", "@rollup/rollup-linux-x64-musl": "4.52.0", "@rollup/rollup-openharmony-arm64": "4.52.0", "@rollup/rollup-win32-arm64-msvc": "4.52.0", "@rollup/rollup-win32-ia32-msvc": "4.52.0", "@rollup/rollup-win32-x64-gnu": "4.52.0", "@rollup/rollup-win32-x64-msvc": "4.52.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g=="],
|
||||
|
||||
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||
|
||||
@@ -365,7 +361,7 @@
|
||||
|
||||
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
"string-width": ["string-width@8.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
@@ -393,7 +389,7 @@
|
||||
|
||||
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
|
||||
|
||||
"viem": ["viem@2.43.1", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.10.5", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-S33pBNlRvOlVv4+L94Z8ydCMDB1j0cuHFUvaC28i6OTxw3uY1P4M3h1YDFK8YC1H9/lIbeBTTvCRhi0FqU/2iw=="],
|
||||
"viem": ["viem@2.45.1", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.11.3", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-LN6Pp7vSfv50LgwhkfSbIXftAM5J89lP9x8TeDa8QM7o41IxlHrDh0F9X+FfnCWtsz11pEVV5sn+yBUoOHNqYA=="],
|
||||
|
||||
"vite": ["vite@7.1.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA=="],
|
||||
|
||||
@@ -407,12 +403,10 @@
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
|
||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||
|
||||
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
|
||||
|
||||
"cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
|
||||
|
||||
"ox/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="],
|
||||
|
||||
"ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
@@ -429,7 +423,11 @@
|
||||
|
||||
"viem/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="],
|
||||
|
||||
"cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
|
||||
"viem/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"ox/@scure/bip32/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
|
||||
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
# Nado 交易机器人完全新手教程
|
||||
|
||||
> 本教程面向零基础用户,手把手教你如何在 Nado 交易所使用 ritmex-bot 进行自动化交易。
|
||||
|
||||
## 目录
|
||||
|
||||
1. [什么是 Nado 和 ritmex-bot](#什么是-nado-和-ritmex-bot)
|
||||
2. [准备工作](#准备工作)
|
||||
3. [安装 Bun 运行环境](#安装-bun-运行环境)
|
||||
4. [下载并安装 ritmex-bot](#下载并安装-ritmex-bot)
|
||||
5. [在 Nado 创建账户并入金](#在-nado-创建账户并入金)
|
||||
6. [获取 Nado 鉴权信息(重要)](#获取-nado-鉴权信息重要)
|
||||
7. [配置 .env 文件](#配置-env-文件)
|
||||
8. [启动交易机器人](#启动交易机器人)
|
||||
9. [策略说明与选择](#策略说明与选择)
|
||||
10. [常见问题与排查](#常见问题与排查)
|
||||
|
||||
---
|
||||
|
||||
## 什么是 Nado 和 ritmex-bot
|
||||
|
||||
### Nado 交易所
|
||||
|
||||
Nado 是一个运行在 **Ink L2 网络**(基于 Optimism Superchain)的去中心化永续合约交易所。它支持 BTC、ETH 等主流加密货币的永续合约交易,具有以下特点:
|
||||
|
||||
- **去中心化**:资产由用户钱包控制,无需信任中心化机构
|
||||
- **低延迟**:使用 L2 网络,交易确认快速
|
||||
- **统一保证金**:支持多种资产作为抵押品
|
||||
|
||||
注册使用 Nado 目前需要邀请码:
|
||||
|
||||
* [https://app.nado.xyz?join=eGp9qPD](https://app.nado.xyz?join=eGp9qPD)
|
||||
* [https://app.nado.xyz?join=LKbIUs5](https://app.nado.xyz?join=LKbIUs5)
|
||||
|
||||
### ritmex-bot 交易机器人
|
||||
|
||||
ritmex-bot 是一个多交易所量化交易终端,支持多种自动化交易策略:
|
||||
|
||||
- **趋势跟随策略**:基于 SMA30 均线的趋势交易
|
||||
- **做市策略**:双边挂单赚取买卖价差
|
||||
- **Guardian 防守策略**:自动监控仓位,补挂止损止盈
|
||||
- **网格策略**:区间震荡行情的网格交易
|
||||
|
||||
---
|
||||
|
||||
## 准备工作
|
||||
|
||||
在开始之前,请确保你具备以下条件:
|
||||
|
||||
### 硬件要求
|
||||
|
||||
- 一台可以联网的电脑(macOS、Linux 或 Windows)
|
||||
- 稳定的网络连接
|
||||
|
||||
### 软件要求
|
||||
|
||||
- **终端/命令行工具**
|
||||
- macOS:使用自带的"终端"应用
|
||||
- Windows:推荐使用 WSL(Windows Subsystem for Linux)或 PowerShell
|
||||
- Linux:使用自带的终端
|
||||
|
||||
### 资金要求
|
||||
|
||||
- 至少 **50-100 USDT** 等值的资金用于交易
|
||||
- 少量 **ETH**(约 0.01-0.05 ETH)用于支付 Gas 费用
|
||||
|
||||
### 钱包要求
|
||||
|
||||
- 一个支持 EVM 的钱包,推荐:
|
||||
- **MetaMask**(浏览器插件)
|
||||
- **Rabby Wallet**(更专业的交易钱包)
|
||||
|
||||
---
|
||||
|
||||
## 安装 Bun 运行环境
|
||||
|
||||
ritmex-bot 使用 **Bun** 作为运行环境。Bun 是一个快速的 JavaScript 运行时。
|
||||
|
||||
### macOS / Linux 安装
|
||||
|
||||
打开终端,输入以下命令:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
```
|
||||
|
||||
安装完成后,**关闭并重新打开终端**,然后验证安装:
|
||||
|
||||
```bash
|
||||
bun -v
|
||||
```
|
||||
|
||||
如果显示版本号(如 `1.2.x`),说明安装成功。
|
||||
|
||||
### Windows 安装
|
||||
|
||||
**方法一:使用 PowerShell(推荐)**
|
||||
|
||||
以管理员身份打开 PowerShell,输入:
|
||||
|
||||
```powershell
|
||||
powershell -c "irm bun.sh/install.ps1 | iex"
|
||||
```
|
||||
|
||||
**方法二:使用 WSL(更稳定)**
|
||||
|
||||
1. 先安装 WSL:在 PowerShell 中运行 `wsl --install`
|
||||
2. 重启电脑
|
||||
3. 打开 WSL 终端,按照 Linux 方式安装 Bun
|
||||
|
||||
---
|
||||
|
||||
## 下载并安装 ritmex-bot
|
||||
|
||||
### 方法一:使用 Git(推荐)
|
||||
|
||||
```bash
|
||||
# 克隆代码仓库
|
||||
git clone https://github.com/discountry/ritmex-bot.git
|
||||
|
||||
# 进入项目目录
|
||||
cd ritmex-bot
|
||||
|
||||
# 安装依赖
|
||||
bun install
|
||||
```
|
||||
|
||||
### 方法二:直接下载 ZIP
|
||||
|
||||
1. 访问 https://github.com/discountry/ritmex-bot
|
||||
2. 点击绿色的 "Code" 按钮
|
||||
3. 选择 "Download ZIP"
|
||||
4. 解压下载的文件
|
||||
5. 在终端中进入解压后的目录
|
||||
6. 运行 `bun install`
|
||||
|
||||
---
|
||||
|
||||
## 在 Nado 创建账户并入金
|
||||
|
||||
### 步骤 1:连接钱包
|
||||
|
||||
1. 访问 Nado 官网:https://app.nado.xyz
|
||||
2. 点击右上角的 **"Connect Wallet"**
|
||||
3. 选择你的钱包(如 MetaMask)
|
||||
4. 在钱包中确认连接
|
||||
|
||||
### 步骤 2:添加 Ink 网络到钱包
|
||||
|
||||
Nado 运行在 Ink L2 网络上,你需要先添加这个网络:
|
||||
|
||||
**自动添加方式:**
|
||||
1. 访问 https://chainlist.org/
|
||||
2. 搜索 "Ink"
|
||||
3. 点击 "Add to MetaMask"
|
||||
|
||||
**手动添加方式:**
|
||||
|
||||
在钱包设置中添加自定义网络,填写以下信息:
|
||||
|
||||
| 参数 | 值 |
|
||||
|------|-----|
|
||||
| 网络名称 | INK |
|
||||
| RPC URL | https://rpc-gel.inkonchain.com |
|
||||
| Chain ID | 57073 |
|
||||
| 货币符号 | ETH |
|
||||
| 区块浏览器 | https://explorer.inkonchain.com |
|
||||
|
||||
### 步骤 3:获取 Gas 费用(ETH)
|
||||
|
||||
在 Ink 网络上进行任何操作都需要少量 ETH 作为 Gas 费用。
|
||||
|
||||
**获取方式:**
|
||||
|
||||
- **从 CEX 直接提现**:Kraken 支持零手续费提现到 Ink 网络
|
||||
- **跨链桥接**:使用 [Superbridge](https://superbridge.app/)、[Bungee](https://bungee.exchange/) 或 [Relay](https://relay.link/) 从其他链桥接 ETH 到 Ink
|
||||
|
||||
> 建议至少准备 0.01-0.05 ETH 用于支付 Gas 费用
|
||||
|
||||
### 步骤 4:存入交易资金
|
||||
|
||||
1. 在 Nado 网站导航到 **Portfolio** 页面
|
||||
2. 点击 **Deposit**
|
||||
3. 选择要存入的资产(支持 USDT0、wETH、USDC、kBTC、wBTC)
|
||||
4. 输入金额并确认交易
|
||||
|
||||
---
|
||||
|
||||
## 获取 Nado 鉴权信息(重要)
|
||||
|
||||
这是配置机器人最关键的一步。Nado 使用基于 EVM 的签名认证方式,你需要获取以下两个关键信息:
|
||||
|
||||
- **NADO_SIGNER_PRIVATE_KEY**:签名私钥
|
||||
- **NADO_SUBACCOUNT_OWNER**:子账户所有者地址
|
||||
|
||||
### 获取方式:从浏览器开发者工具提取
|
||||
|
||||
#### 步骤 1:登录 Nado 交易界面
|
||||
|
||||
1. 访问 https://app.nado.xyz
|
||||
2. 连接你的钱包并完成登录
|
||||
3. 确保你已经在 Nado 启用了一键交易功能并能正常下单
|
||||
|
||||
#### 步骤 2:打开浏览器开发者工具
|
||||
|
||||
- **Chrome / Edge**:按 `F12` 或 `Ctrl+Shift+I`(Mac 上是 `Cmd+Option+I`)
|
||||
- **Firefox**:按 `F12` 或 `Ctrl+Shift+I`
|
||||
- **Safari**:先在偏好设置中启用开发者菜单,然后按 `Cmd+Option+I`
|
||||
|
||||
#### 步骤 3:找到 Local Storage 中的私钥
|
||||
|
||||
1. 在开发者工具中,切换到 **Application** 标签(Chrome/Edge)或 **Storage** 标签(Firefox)
|
||||
2. 在左侧菜单找到 **Local Storage**
|
||||
3. 点击展开,找到 `https://app.nado.xyz`
|
||||
4. 在右侧列表中找到 **`nado.userSettings`** 这一项
|
||||
5. 点击这一项,查看其 Value(值)
|
||||
|
||||
#### 步骤 4:提取私钥
|
||||
|
||||
`nado.userSettings` 的值是一个 JSON 对象,大致结构如下:
|
||||
|
||||
```json
|
||||
"signingPreferenceBySubaccountKey": {
|
||||
"inkMainnet_default": {
|
||||
{
|
||||
"privateKey": "0x1234567890abcdef...",
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**你需要的是 `privateKey` 字段的值**,它是一个以 `0x` 开头的 64 位十六进制字符串。
|
||||
|
||||
将这个值复制下来,这就是你的 `NADO_SIGNER_PRIVATE_KEY`。
|
||||
|
||||
#### 步骤 5:获取子账户所有者地址
|
||||
|
||||
`NADO_SUBACCOUNT_OWNER` 就是你连接到 Nado 的钱包地址。
|
||||
|
||||
获取方式:
|
||||
1. 打开你的钱包(如 MetaMask)
|
||||
2. 复制你的钱包地址(以 `0x` 开头的 42 位地址)
|
||||
|
||||
这就是你的 `NADO_SUBACCOUNT_OWNER`。
|
||||
|
||||
### 关于 Linked Signer(进阶)
|
||||
|
||||
Nado 支持 "Linked Signer" 功能,允许你使用一个专门的签名密钥来代表你的主账户进行交易。这提供了额外的安全层:
|
||||
|
||||
- 主钱包私钥保持离线安全
|
||||
- Linked Signer 只有交易权限,无法提取资金
|
||||
- 可以随时撤销 Linked Signer
|
||||
|
||||
当你在 Nado 网站首次连接钱包时,系统会自动为你创建一个 Linked Signer,这个签名密钥就存储在浏览器的 Local Storage 中。
|
||||
|
||||
> **重要安全提示**:
|
||||
> - `NADO_SIGNER_PRIVATE_KEY` 是 Linked Signer 的私钥,不是你主钱包的私钥
|
||||
> - 这个私钥只能用于在 Nado 上签署交易,无法直接转移你的链上资产
|
||||
> - 但仍需妥善保管,不要分享给他人
|
||||
|
||||
---
|
||||
|
||||
## 配置 .env 文件
|
||||
|
||||
### 步骤 1:创建 .env 文件
|
||||
|
||||
在项目根目录下,复制示例配置文件:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### 步骤 2:编辑 .env 文件
|
||||
|
||||
使用任意文本编辑器打开 `.env` 文件:
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
open -e .env
|
||||
|
||||
# Linux
|
||||
nano .env
|
||||
|
||||
# Windows
|
||||
notepad .env
|
||||
```
|
||||
|
||||
### 步骤 3:填写 Nado 配置
|
||||
|
||||
在 `.env` 文件中找到并修改以下配置项:
|
||||
|
||||
```bash
|
||||
# ============================================
|
||||
# 交易所选择
|
||||
# ============================================
|
||||
EXCHANGE=nado
|
||||
|
||||
# ============================================
|
||||
# Nado 交易所配置(必填)
|
||||
# ============================================
|
||||
|
||||
# 签名私钥 - 从浏览器 Local Storage 获取
|
||||
# 格式:以 0x 开头的 64 位十六进制字符串
|
||||
NADO_SIGNER_PRIVATE_KEY=0x你的私钥
|
||||
|
||||
# 子账户所有者地址 - 你的钱包地址
|
||||
# 格式:以 0x 开头的 42 位地址
|
||||
NADO_SUBACCOUNT_OWNER=0x你的钱包地址
|
||||
|
||||
# 子账户名称(可选,默认为 default)
|
||||
NADO_SUBACCOUNT_NAME=default
|
||||
|
||||
# 网络环境(可选)
|
||||
# inkMainnet = 主网(真实交易)
|
||||
# inkTestnet = 测试网(模拟交易)
|
||||
NADO_ENV=inkMainnet
|
||||
|
||||
# 交易品种(可选,默认 BTC-PERP)
|
||||
NADO_SYMBOL=BTC-PERP
|
||||
|
||||
# ============================================
|
||||
# 交易参数配置
|
||||
# ============================================
|
||||
|
||||
# 单笔交易数量(以 BTC 计)
|
||||
TRADE_AMOUNT=0.001
|
||||
|
||||
# 止损金额(USD)- 亏损超过此金额触发平仓
|
||||
LOSS_LIMIT=10
|
||||
|
||||
# 移动止盈触发金额(USD)
|
||||
TRAILING_PROFIT=5
|
||||
|
||||
# 移动止盈回撤比例(0.2 = 20%)
|
||||
TRAILING_CALLBACK_RATE=0.2
|
||||
```
|
||||
|
||||
### 完整的 Nado 配置示例
|
||||
|
||||
```bash
|
||||
# 交易所选择
|
||||
EXCHANGE=nado
|
||||
|
||||
# Nado 凭证
|
||||
NADO_SIGNER_PRIVATE_KEY=0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
|
||||
NADO_SUBACCOUNT_OWNER=0xAbCdEf1234567890AbCdEf1234567890AbCdEf12
|
||||
NADO_SUBACCOUNT_NAME=default
|
||||
NADO_ENV=inkMainnet
|
||||
NADO_SYMBOL=BTC-PERP
|
||||
|
||||
# 交易参数
|
||||
TRADE_AMOUNT=0.001
|
||||
|
||||
# 趋势策略
|
||||
LOSS_LIMIT=10
|
||||
TRAILING_PROFIT=5
|
||||
TRAILING_CALLBACK_RATE=0.2
|
||||
|
||||
# 波段策略
|
||||
SWING_DIRECTION=short
|
||||
SWING_STOP_LOSS_PCT=0.05 # 价格反向运动 5% 止损
|
||||
|
||||
# 做市策略参数(如果使用做市策略)
|
||||
LOSS_LIMIT=0.08
|
||||
MAKER_LOSS_LIMIT=0.08
|
||||
LIQUIDITY_MAKER_LOSS_LIMIT=0.08
|
||||
LIQUIDITY_MAKER_CLOSE_TICK_OFFSET=5
|
||||
MAKER_ENTRY_DEPTH_LEVEL=1
|
||||
```
|
||||
|
||||
## 启动交易机器人
|
||||
|
||||
### 方法一:交互式启动(推荐新手)
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
启动后会显示一个交互式菜单,使用方向键选择策略,按回车确认。
|
||||
|
||||
### 方法二:命令行直接启动
|
||||
|
||||
```bash
|
||||
# 启动趋势策略
|
||||
bun run index.ts --strategy trend --silent
|
||||
|
||||
# 启动做市策略
|
||||
bun run index.ts --strategy maker --silent
|
||||
```
|
||||
|
||||
### 方法三:使用 PM2 后台运行(推荐生产环境)
|
||||
|
||||
```bash
|
||||
# 安装 PM2
|
||||
bun add -d pm2
|
||||
|
||||
# 后台启动趋势策略
|
||||
bunx pm2 start bun --name ritmex-trend --cwd . --restart-delay 5000 -- run index.ts --strategy trend --silent
|
||||
|
||||
# 查看运行状态
|
||||
bunx pm2 list
|
||||
|
||||
# 查看日志
|
||||
bunx pm2 logs ritmex-trend
|
||||
|
||||
# 停止运行
|
||||
bunx pm2 stop ritmex-trend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 策略说明与选择
|
||||
|
||||
### 趋势策略(Trend)
|
||||
|
||||
**适用场景**:单边行情,趋势明显的市场
|
||||
|
||||
**工作原理**:
|
||||
- 使用 SMA30(30 周期简单移动平均线)判断趋势
|
||||
- 价格突破均线时开仓
|
||||
- 内置止损和移动止盈
|
||||
|
||||
**配置参数**:
|
||||
```bash
|
||||
TRADE_AMOUNT=0.001 # 单笔交易量
|
||||
LOSS_LIMIT=10 # 止损金额(USD)
|
||||
TRAILING_PROFIT=5 # 移动止盈触发金额
|
||||
TRAILING_CALLBACK_RATE=0.2 # 回撤比例
|
||||
```
|
||||
|
||||
### 做市策略(Maker)
|
||||
|
||||
**适用场景**:震荡行情,低波动市场
|
||||
|
||||
**工作原理**:
|
||||
- 在买卖盘口双边挂单
|
||||
- 赚取买卖价差
|
||||
- 根据持仓自动调整挂单方向
|
||||
|
||||
**配置参数**:
|
||||
```bash
|
||||
MAKER_BID_OFFSET=0 # 买单价格偏移
|
||||
MAKER_ASK_OFFSET=0 # 卖单价格偏移
|
||||
MAKER_REFRESH_INTERVAL_MS=500 # 刷新间隔
|
||||
```
|
||||
|
||||
## 常见问题与排查
|
||||
|
||||
### 问题 1:启动时提示 "Missing NADO_SIGNER_PRIVATE_KEY"
|
||||
|
||||
**原因**:未正确配置签名私钥
|
||||
|
||||
**解决方案**:
|
||||
1. 确认 `.env` 文件存在于项目根目录
|
||||
2. 检查 `NADO_SIGNER_PRIVATE_KEY` 是否正确填写
|
||||
3. 确保私钥以 `0x` 开头
|
||||
|
||||
### 问题 2:启动时提示 "Missing NADO_SUBACCOUNT_OWNER"
|
||||
|
||||
**原因**:未配置子账户所有者地址
|
||||
|
||||
**解决方案**:
|
||||
1. 在 `.env` 中填写 `NADO_SUBACCOUNT_OWNER` 为你的钱包地址
|
||||
2. 也可以使用 `NADO_EVM_ADDRESS` 作为替代
|
||||
|
||||
### 问题 3:连接失败,无法获取账户信息
|
||||
|
||||
**可能原因及解决方案**:
|
||||
|
||||
1. **网络问题**:检查网络连接,确保能访问 Nado API
|
||||
2. **私钥错误**:重新从浏览器 Local Storage 获取正确的私钥
|
||||
3. **账户未激活**:确保在 Nado 网站上至少存入过资金
|
||||
4. **Linked Signer 未启用**:账户需要至少有 5 USDT 价值的资产才能使用 Linked Signer
|
||||
|
||||
### 问题 4:下单失败,提示精度错误
|
||||
|
||||
**原因**:价格或数量精度不符合交易所要求
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# BTC-PERP 的标准精度
|
||||
PRICE_TICK=0.1 # 价格精度 0.1 USD
|
||||
QTY_STEP=0.001 # 数量精度 0.001 BTC
|
||||
```
|
||||
|
||||
### 问题 5:时间同步错误
|
||||
|
||||
**原因**:本地时间与服务器时间相差太大
|
||||
|
||||
**解决方案**:
|
||||
- macOS/Linux:`sudo ntpdate -u time.apple.com`
|
||||
- Windows:设置 → 时间和语言 → 同步时间
|
||||
|
||||
### 问题 6:Gas 费用不足
|
||||
|
||||
**原因**:Ink 网络上的 ETH 余额不足
|
||||
|
||||
**解决方案**:
|
||||
1. 通过跨链桥向 Ink 网络转入少量 ETH(0.01-0.05 ETH)
|
||||
2. 推荐使用 Superbridge 或 Relay 进行跨链
|
||||
|
||||
### 问题 7:找不到 Local Storage 中的私钥
|
||||
|
||||
**可能原因**:
|
||||
1. 尚未在 Nado 完成首次连接/操作
|
||||
2. 浏览器清除了缓存
|
||||
|
||||
**解决方案**:
|
||||
1. 重新访问 https://app.nado.xyz
|
||||
2. 连接钱包并进行一次操作(如查看账户)
|
||||
3. 再次检查 Local Storage
|
||||
|
||||
---
|
||||
|
||||
## 安全提示
|
||||
|
||||
1. **永远不要分享你的私钥**:`NADO_SIGNER_PRIVATE_KEY` 虽然不是主钱包私钥,但仍可用于在 Nado 上进行交易
|
||||
|
||||
2. **使用专用交易账户**:建议创建一个专门用于交易的新钱包,不要放置大量资产
|
||||
|
||||
3. **定期检查授权**:在钱包中定期检查并撤销不需要的 DApp 授权
|
||||
|
||||
4. **小额测试**:首次运行时使用小额资金测试,确认一切正常后再增加资金
|
||||
|
||||
5. **保护 .env 文件**:
|
||||
- 不要将 `.env` 文件提交到 Git
|
||||
- 不要分享给他人
|
||||
- 定期更换凭证
|
||||
|
||||
---
|
||||
|
||||
## 社区与支持
|
||||
|
||||
- **Telegram 交流群**:https://t.me/+4fdo0quY87o4Mjhh
|
||||
- **GitHub Issues**:https://github.com/discountry/ritmex-bot/issues
|
||||
- **Nado 官方文档**:https://docs.nado.xyz
|
||||
|
||||
---
|
||||
|
||||
## 风险提示
|
||||
|
||||
量化交易具备风险。请注意:
|
||||
|
||||
- 市场波动可能导致亏损
|
||||
- 技术故障可能导致订单执行异常
|
||||
- 请勿投入无法承受损失的资金
|
||||
- 建议先在测试网或小额账户中验证策略
|
||||
|
||||
祝交易顺利!
|
||||
+11
-11
@@ -21,21 +21,21 @@
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grvt/client": "^1.6.4",
|
||||
"@nadohq/client": "^0.1.0-alpha.41",
|
||||
"@grvt/client": "^1.6.25",
|
||||
"@nadohq/client": "^0.1.0-alpha.45",
|
||||
"@noble/ed25519": "^3.0.0",
|
||||
"axios": "^1.12.2",
|
||||
"axios": "^1.13.4",
|
||||
"bignumber.js": "^9.3.1",
|
||||
"ccxt": "^4.5.12",
|
||||
"dotenv": "^17.2.2",
|
||||
"ethereum-cryptography": "^2.1.3",
|
||||
"ink": "^6.3.1",
|
||||
"react": "^19.1.1",
|
||||
"ccxt": "^4.5.35",
|
||||
"dotenv": "^17.2.3",
|
||||
"ethereum-cryptography": "^2.2.1",
|
||||
"ink": "^6.6.0",
|
||||
"react": "^19.2.4",
|
||||
"trading-signals": "^7.4.3",
|
||||
"viem": "^2.43.1",
|
||||
"ws": "^8.18.3"
|
||||
"viem": "^2.45.1",
|
||||
"ws": "^8.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,7 +602,10 @@ export class ParadexGateway {
|
||||
// Only omit amount for MARKET close-position orders; STOP requires explicit size
|
||||
const shouldOmitAmount = isClosePosition && type === "market";
|
||||
const amountArg: any = shouldOmitAmount ? undefined : amount;
|
||||
if (!shouldOmitAmount && amountArg != null && extraParams.size === undefined) {
|
||||
// Paradex/ccxt may require `size` even when amount is omitted for closePosition MARKET orders.
|
||||
if (shouldOmitAmount && amount != null && extraParams.size === undefined) {
|
||||
extraParams.size = amount.toString();
|
||||
} else if (!shouldOmitAmount && amountArg != null && extraParams.size === undefined) {
|
||||
extraParams.size = amountArg.toString();
|
||||
}
|
||||
const order = (await this.exchange.createOrder(
|
||||
|
||||
+2
-2
@@ -248,8 +248,8 @@ const translations: Record<string, TranslationEntry> = {
|
||||
en: "Quote mode: {mode} | BUY {buy} | SELL {sell}",
|
||||
},
|
||||
"makerPoints.binanceLine": {
|
||||
zh: "Binance 深度: 买10 {buy} | 卖10 {sell} | 状态: {status}",
|
||||
en: "Binance depth: bid10 {buy} | ask10 {sell} | Status: {status}",
|
||||
zh: "Binance 深度(±9bps): 买 {buy} | 卖 {sell} | 状态: {status}",
|
||||
en: "Binance depth (±9bps): bid {buy} | ask {sell} | Status: {status}",
|
||||
},
|
||||
"makerPoints.bandDepthLine": {
|
||||
zh: "StandX 档位 {band}bps 深度: 买 {buy} | 卖 {sell}",
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import NodeWebSocket from "ws";
|
||||
import { computeDepthStats, type DepthImbalance } from "../../utils/depth";
|
||||
import type { AsterDepthLevel } from "../../exchanges/types";
|
||||
import type { DepthImbalance } from "../../utils/depth";
|
||||
|
||||
const WebSocketCtor: typeof globalThis.WebSocket =
|
||||
typeof globalThis.WebSocket !== "undefined"
|
||||
? globalThis.WebSocket
|
||||
: ((NodeWebSocket as unknown) as typeof globalThis.WebSocket);
|
||||
|
||||
const DEFAULT_BASE_URL = "wss://stream.binance.com:9443/ws";
|
||||
const DEFAULT_WS_BASE_URL = "wss://stream.binance.com:9443/ws";
|
||||
const DEFAULT_REST_BASE_URL = "https://api.binance.com";
|
||||
|
||||
// ========== Binance WebSocket 连接管理常量 ==========
|
||||
// Binance 会发送 ping,若长时间无消息则认为连接异常
|
||||
// 我们设置 5 分钟作为心跳超时阈值(保守值)
|
||||
const HEARTBEAT_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
// 心跳检查间隔(每 30 秒检查一次)
|
||||
const HEARTBEAT_CHECK_INTERVAL_MS = 30_000;
|
||||
// Binance 连接最长有效期 24 小时,我们设置 23 小时主动重连
|
||||
const MAX_CONNECTION_DURATION_MS = 23 * 60 * 60 * 1000;
|
||||
// 数据过时阈值(毫秒)- 超过此时间未收到数据,标记为不可用
|
||||
const DATA_STALE_THRESHOLD_MS = 5_000;
|
||||
// 基础重连延迟
|
||||
const RECONNECT_DELAY_BASE_MS = 3000;
|
||||
// 最大重连延迟
|
||||
const RECONNECT_DELAY_MAX_MS = 60_000;
|
||||
|
||||
const DEFAULT_REFRESH_SYNC_INTERVAL_MS = 30_000;
|
||||
const DEFAULT_DEPTH_WINDOW_BPS = 9;
|
||||
const DEFAULT_IMBALANCE_RATIO = 9;
|
||||
const MAX_BUFFER_SIZE = 5000;
|
||||
const SYNC_SNAPSHOT_MAX_RETRIES = 5;
|
||||
const REST_FAILURE_DEFENSE_THRESHOLD = 1;
|
||||
|
||||
export type BinanceConnectionState = "connected" | "disconnected" | "stale";
|
||||
|
||||
export interface BinanceDepthSnapshot {
|
||||
@@ -33,49 +34,91 @@ export interface BinanceDepthSnapshot {
|
||||
skipSellSide: boolean;
|
||||
imbalance: DepthImbalance;
|
||||
updatedAt: number;
|
||||
windowBps: number;
|
||||
localLastUpdateId: number;
|
||||
}
|
||||
|
||||
export interface BinanceDepthHealth {
|
||||
started: boolean;
|
||||
connected: boolean;
|
||||
orderBookReady: boolean;
|
||||
restHealthy: boolean;
|
||||
healthy: boolean;
|
||||
reason: string | null;
|
||||
lastEventAt: number;
|
||||
lastSnapshotAt: number;
|
||||
lastRestSyncAt: number;
|
||||
localLastUpdateId: number;
|
||||
}
|
||||
|
||||
export type BinanceConnectionListener = (state: BinanceConnectionState) => void;
|
||||
|
||||
interface DepthUpdateEvent {
|
||||
U: number;
|
||||
u: number;
|
||||
bids: AsterDepthLevel[];
|
||||
asks: AsterDepthLevel[];
|
||||
}
|
||||
|
||||
interface DepthSnapshotResponse {
|
||||
lastUpdateId: number;
|
||||
bids: AsterDepthLevel[];
|
||||
asks: AsterDepthLevel[];
|
||||
}
|
||||
|
||||
export class BinanceDepthTracker {
|
||||
private ws: WebSocket | null = null;
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private reconnectDelayMs = RECONNECT_DELAY_BASE_MS;
|
||||
private stopped = false;
|
||||
private started = false;
|
||||
|
||||
private snapshot: BinanceDepthSnapshot | null = null;
|
||||
private listeners = new Set<(snapshot: BinanceDepthSnapshot) => void>();
|
||||
private connectionListeners = new Set<BinanceConnectionListener>();
|
||||
|
||||
// ========== 心跳与连接管理 ==========
|
||||
// 上次收到消息的时间戳
|
||||
private lastMessageTime = 0;
|
||||
// 心跳检查定时器
|
||||
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
||||
// 连接建立时间(用于日志记录)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private connectionStartTime = 0;
|
||||
// 24 小时重连定时器
|
||||
private maxDurationTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
// 当前连接状态
|
||||
private refreshSyncTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private connectionState: BinanceConnectionState = "disconnected";
|
||||
|
||||
private bidBook = new Map<string, number>();
|
||||
private askBook = new Map<string, number>();
|
||||
private localLastUpdateId = 0;
|
||||
private orderBookReady = false;
|
||||
private eventBuffer: DepthUpdateEvent[] = [];
|
||||
private syncInFlight: Promise<void> | null = null;
|
||||
|
||||
private lastEventAt = 0;
|
||||
private lastSnapshotAt = 0;
|
||||
private lastRestSyncAt = 0;
|
||||
private restConsecutiveFailures = 0;
|
||||
private restLastError: string | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly symbol: string,
|
||||
private readonly options?: {
|
||||
baseUrl?: string;
|
||||
restBaseUrl?: string;
|
||||
levels?: number;
|
||||
ratio?: number;
|
||||
speedMs?: number;
|
||||
depthWindowBps?: number;
|
||||
refreshSyncMs?: number;
|
||||
logger?: (context: string, error: unknown) => void;
|
||||
}
|
||||
) {}
|
||||
|
||||
start(): void {
|
||||
this.started = true;
|
||||
this.stopped = false;
|
||||
this.connect();
|
||||
this.startRefreshSyncTimer();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.started = false;
|
||||
this.stopped = true;
|
||||
this.cleanup();
|
||||
}
|
||||
@@ -88,9 +131,6 @@ export class BinanceDepthTracker {
|
||||
this.listeners.delete(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听连接状态变化
|
||||
*/
|
||||
onConnectionChange(handler: BinanceConnectionListener): void {
|
||||
this.connectionListeners.add(handler);
|
||||
}
|
||||
@@ -103,69 +143,111 @@ export class BinanceDepthTracker {
|
||||
return this.snapshot ? { ...this.snapshot } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前连接状态
|
||||
*/
|
||||
getConnectionState(): BinanceConnectionState {
|
||||
return this.connectionState;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据是否过时
|
||||
*/
|
||||
isDataStale(): boolean {
|
||||
if (!this.snapshot) return true;
|
||||
return Date.now() - this.snapshot.updatedAt > DATA_STALE_THRESHOLD_MS;
|
||||
}
|
||||
|
||||
isHealthy(): boolean {
|
||||
return this.getHealth().healthy;
|
||||
}
|
||||
|
||||
getHealth(): BinanceDepthHealth {
|
||||
if (!this.started) {
|
||||
return {
|
||||
started: false,
|
||||
connected: false,
|
||||
orderBookReady: false,
|
||||
restHealthy: true,
|
||||
healthy: true,
|
||||
reason: null,
|
||||
lastEventAt: this.lastEventAt,
|
||||
lastSnapshotAt: this.lastSnapshotAt,
|
||||
lastRestSyncAt: this.lastRestSyncAt,
|
||||
localLastUpdateId: this.localLastUpdateId,
|
||||
};
|
||||
}
|
||||
|
||||
const restHealthy = this.restConsecutiveFailures < REST_FAILURE_DEFENSE_THRESHOLD;
|
||||
let reason: string | null = null;
|
||||
|
||||
if (this.connectionState !== "connected") {
|
||||
reason = `ws_${this.connectionState}`;
|
||||
} else if (!this.orderBookReady) {
|
||||
reason = "orderbook_not_ready";
|
||||
} else if (this.isDataStale()) {
|
||||
reason = "orderbook_stale";
|
||||
} else if (!restHealthy) {
|
||||
reason = this.restLastError ? `rest_sync_failed:${this.restLastError}` : "rest_sync_failed";
|
||||
}
|
||||
|
||||
return {
|
||||
started: true,
|
||||
connected: this.connectionState === "connected",
|
||||
orderBookReady: this.orderBookReady,
|
||||
restHealthy,
|
||||
healthy: reason == null,
|
||||
reason,
|
||||
lastEventAt: this.lastEventAt,
|
||||
lastSnapshotAt: this.lastSnapshotAt,
|
||||
lastRestSyncAt: this.lastRestSyncAt,
|
||||
localLastUpdateId: this.localLastUpdateId,
|
||||
};
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
// 停止心跳监控
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
this.heartbeatTimer = null;
|
||||
}
|
||||
// 停止 24 小时重连定时器
|
||||
if (this.maxDurationTimer) {
|
||||
clearTimeout(this.maxDurationTimer);
|
||||
this.maxDurationTimer = null;
|
||||
}
|
||||
// 停止重连定时器
|
||||
if (this.refreshSyncTimer) {
|
||||
clearInterval(this.refreshSyncTimer);
|
||||
this.refreshSyncTimer = null;
|
||||
}
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
// 关闭 WebSocket
|
||||
if (this.ws) {
|
||||
try {
|
||||
this.ws.close();
|
||||
} catch {
|
||||
// Ignore close errors
|
||||
// ignore close errors
|
||||
}
|
||||
this.ws = null;
|
||||
}
|
||||
this.updateConnectionState("disconnected");
|
||||
}
|
||||
|
||||
private connect(): void {
|
||||
if (this.ws || this.stopped) return;
|
||||
const url = this.buildUrl();
|
||||
const url = this.buildWsUrl();
|
||||
this.ws = new WebSocketCtor(url);
|
||||
|
||||
const handleOpen = () => {
|
||||
this.reconnectDelayMs = RECONNECT_DELAY_BASE_MS;
|
||||
this.connectionStartTime = Date.now();
|
||||
this.lastMessageTime = Date.now();
|
||||
this.lastEventAt = Date.now();
|
||||
this.orderBookReady = false;
|
||||
this.eventBuffer = [];
|
||||
this.updateConnectionState("connected");
|
||||
|
||||
// 启动心跳监控
|
||||
this.startHeartbeatMonitor();
|
||||
// 启动 24 小时自动重连定时器
|
||||
this.startMaxDurationTimer();
|
||||
|
||||
this.options?.logger?.("binanceDepth", "WebSocket connected");
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
this.ws = null;
|
||||
this.orderBookReady = false;
|
||||
this.eventBuffer = [];
|
||||
this.stopHeartbeatMonitor();
|
||||
this.stopMaxDurationTimer();
|
||||
this.updateConnectionState("disconnected");
|
||||
@@ -178,7 +260,6 @@ export class BinanceDepthTracker {
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
this.options?.logger?.("binanceDepth", error);
|
||||
// 如果连接从未成功建立,需要清理并重连
|
||||
if (this.ws && this.connectionState === "disconnected") {
|
||||
this.ws = null;
|
||||
this.scheduleReconnect();
|
||||
@@ -187,20 +268,18 @@ export class BinanceDepthTracker {
|
||||
|
||||
const handleMessage = (event: { data: unknown }) => {
|
||||
this.lastMessageTime = Date.now();
|
||||
// 如果之前是 stale 状态,恢复为 connected
|
||||
this.lastEventAt = Date.now();
|
||||
if (this.connectionState === "stale") {
|
||||
this.updateConnectionState("connected");
|
||||
}
|
||||
this.handlePayload(event.data);
|
||||
};
|
||||
|
||||
// 处理 Binance 服务器的 ping 帧
|
||||
// 根据文档:必须尽快回复 pong,payload 为 ping 的 payload 副本
|
||||
const handlePing = (data: unknown) => {
|
||||
this.lastMessageTime = Date.now();
|
||||
if (this.ws && "pong" in this.ws && typeof this.ws.pong === "function") {
|
||||
try {
|
||||
this.ws.pong(data as any);
|
||||
this.ws.pong(data as never);
|
||||
} catch (error) {
|
||||
this.options?.logger?.("binanceDepth pong", error);
|
||||
}
|
||||
@@ -209,33 +288,40 @@ export class BinanceDepthTracker {
|
||||
|
||||
if ("addEventListener" in this.ws && typeof this.ws.addEventListener === "function") {
|
||||
this.ws.addEventListener("open", handleOpen);
|
||||
this.ws.addEventListener("message", handleMessage as any);
|
||||
this.ws.addEventListener("message", handleMessage as never);
|
||||
this.ws.addEventListener("close", handleClose);
|
||||
this.ws.addEventListener("error", handleError as any);
|
||||
this.ws.addEventListener("ping", handlePing as any);
|
||||
} else if ("on" in this.ws && typeof (this.ws as any).on === "function") {
|
||||
const nodeSocket = this.ws as any;
|
||||
this.ws.addEventListener("error", handleError as never);
|
||||
this.ws.addEventListener("ping", handlePing as never);
|
||||
} else if ("on" in this.ws && typeof (this.ws as { on?: unknown }).on === "function") {
|
||||
const nodeSocket = this.ws as { on: (event: string, listener: (...args: unknown[]) => void) => void };
|
||||
nodeSocket.on("open", handleOpen);
|
||||
nodeSocket.on("message", (data: unknown) => handleMessage({ data }));
|
||||
nodeSocket.on("close", handleClose);
|
||||
nodeSocket.on("error", handleError);
|
||||
nodeSocket.on("ping", handlePing);
|
||||
} else {
|
||||
(this.ws as any).onopen = handleOpen;
|
||||
(this.ws as any).onmessage = handleMessage;
|
||||
(this.ws as any).onclose = handleClose;
|
||||
(this.ws as any).onerror = handleError;
|
||||
const genericSocket = this.ws as any;
|
||||
genericSocket.onopen = handleOpen;
|
||||
genericSocket.onmessage = handleMessage;
|
||||
genericSocket.onclose = handleClose;
|
||||
genericSocket.onerror = handleError;
|
||||
}
|
||||
}
|
||||
|
||||
private buildUrl(): string {
|
||||
const base = this.options?.baseUrl ?? DEFAULT_BASE_URL;
|
||||
const levels = this.options?.levels ?? 10;
|
||||
private buildWsUrl(): string {
|
||||
const baseRaw = (this.options?.baseUrl ?? DEFAULT_WS_BASE_URL).replace(/\/+$/, "");
|
||||
const base = baseRaw.endsWith("/ws") || baseRaw.includes("/stream") ? baseRaw : `${baseRaw}/ws`;
|
||||
const speed = this.options?.speedMs ?? 100;
|
||||
const stream = `${this.symbol.toLowerCase()}@depth${levels}@${speed}ms`;
|
||||
const stream = `${this.symbol.toLowerCase()}@depth@${speed}ms`;
|
||||
return `${base}/${stream}`;
|
||||
}
|
||||
|
||||
private buildRestDepthUrl(): string {
|
||||
const base = (this.options?.restBaseUrl ?? process.env.BINANCE_REST_URL ?? DEFAULT_REST_BASE_URL).replace(/\/+$/, "");
|
||||
const symbol = this.symbol.toUpperCase();
|
||||
return `${base}/api/v3/depth?symbol=${encodeURIComponent(symbol)}&limit=5000`;
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectTimer || this.stopped) return;
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
@@ -245,24 +331,17 @@ export class BinanceDepthTracker {
|
||||
}, this.reconnectDelayMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动心跳监控
|
||||
* 根据 Binance 文档:长时间无 pong 会断连
|
||||
* 我们设置 5 分钟作为心跳超时阈值
|
||||
*/
|
||||
private startHeartbeatMonitor(): void {
|
||||
this.stopHeartbeatMonitor();
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
const elapsed = now - this.lastMessageTime;
|
||||
|
||||
// 检查数据是否过时(5 秒无数据)
|
||||
if (elapsed > DATA_STALE_THRESHOLD_MS && this.connectionState === "connected") {
|
||||
this.updateConnectionState("stale");
|
||||
this.options?.logger?.("binanceDepth", `Data stale: ${elapsed}ms since last message`);
|
||||
}
|
||||
|
||||
// 检查心跳超时(5 分钟无消息)
|
||||
if (elapsed > HEARTBEAT_TIMEOUT_MS) {
|
||||
this.options?.logger?.("binanceDepth", `Heartbeat timeout: ${elapsed}ms, forcing reconnect`);
|
||||
this.forceReconnect("heartbeat_timeout");
|
||||
@@ -277,11 +356,6 @@ export class BinanceDepthTracker {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 24 小时自动重连定时器
|
||||
* 根据 Binance 文档:连接最长有效期 24 小时
|
||||
* 我们设置 23 小时主动重连,避免被服务器断开
|
||||
*/
|
||||
private startMaxDurationTimer(): void {
|
||||
this.stopMaxDurationTimer();
|
||||
this.maxDurationTimer = setTimeout(() => {
|
||||
@@ -297,9 +371,15 @@ export class BinanceDepthTracker {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制重连
|
||||
*/
|
||||
private startRefreshSyncTimer(): void {
|
||||
if (this.refreshSyncTimer) return;
|
||||
const refreshSyncMs = Math.max(5000, this.options?.refreshSyncMs ?? DEFAULT_REFRESH_SYNC_INTERVAL_MS);
|
||||
this.refreshSyncTimer = setInterval(() => {
|
||||
if (!this.started || this.stopped || !this.orderBookReady) return;
|
||||
this.ensureSynced("periodic_refresh");
|
||||
}, refreshSyncMs);
|
||||
}
|
||||
|
||||
private forceReconnect(reason: string): void {
|
||||
this.options?.logger?.("binanceDepth", `Force reconnect: ${reason}`);
|
||||
this.stopHeartbeatMonitor();
|
||||
@@ -314,15 +394,13 @@ export class BinanceDepthTracker {
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
this.orderBookReady = false;
|
||||
this.eventBuffer = [];
|
||||
this.updateConnectionState("disconnected");
|
||||
// 立即重连(不使用指数退避)
|
||||
this.reconnectDelayMs = RECONNECT_DELAY_BASE_MS;
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新连接状态并通知监听器
|
||||
*/
|
||||
private updateConnectionState(state: BinanceConnectionState): void {
|
||||
if (this.connectionState === state) return;
|
||||
this.connectionState = state;
|
||||
@@ -336,27 +414,197 @@ export class BinanceDepthTracker {
|
||||
}
|
||||
|
||||
private handlePayload(data: unknown): void {
|
||||
const payload = this.parsePayload(data);
|
||||
if (!payload) return;
|
||||
const bids = Array.isArray(payload.b) ? payload.b : Array.isArray(payload.bids) ? payload.bids : [];
|
||||
const asks = Array.isArray(payload.a) ? payload.a : Array.isArray(payload.asks) ? payload.asks : [];
|
||||
const depth = {
|
||||
lastUpdateId: Number(payload.lastUpdateId ?? payload.u ?? Date.now()),
|
||||
bids,
|
||||
asks,
|
||||
};
|
||||
const levels = this.options?.levels ?? 10;
|
||||
const ratio = this.options?.ratio ?? 3;
|
||||
const stats = computeDepthStats(depth, levels, ratio);
|
||||
const event = this.parseDepthEvent(data);
|
||||
if (!event) return;
|
||||
|
||||
if (!this.orderBookReady) {
|
||||
this.eventBuffer.push(event);
|
||||
if (this.eventBuffer.length > MAX_BUFFER_SIZE) {
|
||||
this.eventBuffer.splice(0, this.eventBuffer.length - MAX_BUFFER_SIZE);
|
||||
}
|
||||
this.ensureSynced("bootstrap");
|
||||
return;
|
||||
}
|
||||
|
||||
const applied = this.applyDepthEvent(event);
|
||||
if (!applied) {
|
||||
this.options?.logger?.(
|
||||
"binanceDepth",
|
||||
`Detected update gap: local=${this.localLastUpdateId}, event=[${event.U},${event.u}], resyncing`
|
||||
);
|
||||
this.orderBookReady = false;
|
||||
this.eventBuffer = [event];
|
||||
this.ensureSynced("sequence_gap");
|
||||
return;
|
||||
}
|
||||
|
||||
this.emitDepthSnapshot();
|
||||
}
|
||||
|
||||
private ensureSynced(reason: string): void {
|
||||
if (this.syncInFlight || this.stopped || !this.started) return;
|
||||
this.syncInFlight = (async () => {
|
||||
try {
|
||||
if (!this.orderBookReady) {
|
||||
await this.bootstrapOrderBookFromSnapshot(reason);
|
||||
return;
|
||||
}
|
||||
await this.refreshOrderBookFromSnapshot(reason);
|
||||
} finally {
|
||||
this.syncInFlight = null;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
private async bootstrapOrderBookFromSnapshot(reason: string): Promise<void> {
|
||||
if (this.eventBuffer.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < SYNC_SNAPSHOT_MAX_RETRIES; attempt += 1) {
|
||||
const firstBuffered = this.eventBuffer[0];
|
||||
if (!firstBuffered) return;
|
||||
|
||||
const snapshot = await this.fetchDepthSnapshot(reason);
|
||||
if (!snapshot) return;
|
||||
|
||||
if (snapshot.lastUpdateId < firstBuffered.U) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.resetOrderBook(snapshot);
|
||||
|
||||
const buffered = this.eventBuffer.filter((event) => event.u > snapshot.lastUpdateId);
|
||||
if (buffered.length > 0) {
|
||||
const nextEvent = buffered[0];
|
||||
if (!nextEvent) return;
|
||||
const nextUpdateId = snapshot.lastUpdateId + 1;
|
||||
if (nextEvent.U > nextUpdateId || nextEvent.u < nextUpdateId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let failed = false;
|
||||
for (const event of buffered) {
|
||||
if (!this.applyDepthEvent(event)) {
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (failed) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
this.orderBookReady = true;
|
||||
this.eventBuffer = [];
|
||||
this.emitDepthSnapshot();
|
||||
return;
|
||||
}
|
||||
|
||||
this.options?.logger?.("binanceDepth", "Bootstrap orderbook failed after retries");
|
||||
}
|
||||
|
||||
private async refreshOrderBookFromSnapshot(reason: string): Promise<void> {
|
||||
const snapshot = await this.fetchDepthSnapshot(reason);
|
||||
if (!snapshot) return;
|
||||
if (snapshot.lastUpdateId < this.localLastUpdateId) {
|
||||
return;
|
||||
}
|
||||
this.resetOrderBook(snapshot);
|
||||
this.orderBookReady = true;
|
||||
this.emitDepthSnapshot();
|
||||
}
|
||||
|
||||
private resetOrderBook(snapshot: DepthSnapshotResponse): void {
|
||||
this.bidBook.clear();
|
||||
this.askBook.clear();
|
||||
this.applyLevels(this.bidBook, snapshot.bids);
|
||||
this.applyLevels(this.askBook, snapshot.asks);
|
||||
this.localLastUpdateId = snapshot.lastUpdateId;
|
||||
this.lastSnapshotAt = Date.now();
|
||||
}
|
||||
|
||||
private applyDepthEvent(event: DepthUpdateEvent): boolean {
|
||||
if (!this.localLastUpdateId) return false;
|
||||
|
||||
if (event.u < this.localLastUpdateId) {
|
||||
return true;
|
||||
}
|
||||
if (event.U > this.localLastUpdateId + 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.applyLevels(this.bidBook, event.bids);
|
||||
this.applyLevels(this.askBook, event.asks);
|
||||
this.localLastUpdateId = event.u;
|
||||
return true;
|
||||
}
|
||||
|
||||
private applyLevels(book: Map<string, number>, levels: AsterDepthLevel[]): void {
|
||||
for (const level of levels) {
|
||||
const priceRaw = level?.[0];
|
||||
const qtyRaw = level?.[1];
|
||||
const price = Number(priceRaw);
|
||||
const qty = Number(qtyRaw);
|
||||
if (!priceRaw || !Number.isFinite(price) || price <= 0) continue;
|
||||
if (!Number.isFinite(qty) || qty < 0) continue;
|
||||
|
||||
if (qty === 0) {
|
||||
book.delete(priceRaw);
|
||||
} else {
|
||||
book.set(priceRaw, qty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private emitDepthSnapshot(): void {
|
||||
const bestBid = this.findBestPrice(this.bidBook, "bid");
|
||||
const bestAsk = this.findBestPrice(this.askBook, "ask");
|
||||
if (bestBid == null || bestAsk == null || bestBid <= 0 || bestAsk <= 0 || bestAsk < bestBid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const windowBps = Math.max(1, this.options?.depthWindowBps ?? DEFAULT_DEPTH_WINDOW_BPS);
|
||||
const ratio = Math.max(1.01, this.options?.ratio ?? DEFAULT_IMBALANCE_RATIO);
|
||||
const bidWindowMin = bestBid * (1 - windowBps / 10_000);
|
||||
const askWindowMax = bestAsk * (1 + windowBps / 10_000);
|
||||
|
||||
let buySum = 0;
|
||||
let sellSum = 0;
|
||||
|
||||
for (const [priceRaw, qty] of this.bidBook.entries()) {
|
||||
const price = Number(priceRaw);
|
||||
if (!Number.isFinite(price) || price < bidWindowMin) continue;
|
||||
buySum += qty;
|
||||
}
|
||||
for (const [priceRaw, qty] of this.askBook.entries()) {
|
||||
const price = Number(priceRaw);
|
||||
if (!Number.isFinite(price) || price > askWindowMax) continue;
|
||||
sellSum += qty;
|
||||
}
|
||||
|
||||
const skipSellSide = sellSum === 0 || buySum > sellSum * ratio;
|
||||
const skipBuySide = buySum === 0 || sellSum > buySum * ratio;
|
||||
|
||||
let imbalance: DepthImbalance = "balanced";
|
||||
if (buySum > sellSum * ratio) {
|
||||
imbalance = "buy_dominant";
|
||||
} else if (sellSum > buySum * ratio) {
|
||||
imbalance = "sell_dominant";
|
||||
}
|
||||
|
||||
this.snapshot = {
|
||||
symbol: this.symbol,
|
||||
buySum: stats.buySum,
|
||||
sellSum: stats.sellSum,
|
||||
skipBuySide: stats.skipBuySide,
|
||||
skipSellSide: stats.skipSellSide,
|
||||
imbalance: stats.imbalance,
|
||||
buySum,
|
||||
sellSum,
|
||||
skipBuySide,
|
||||
skipSellSide,
|
||||
imbalance,
|
||||
updatedAt: Date.now(),
|
||||
windowBps,
|
||||
localLastUpdateId: this.localLastUpdateId,
|
||||
};
|
||||
|
||||
for (const listener of this.listeners) {
|
||||
try {
|
||||
listener({ ...this.snapshot });
|
||||
@@ -366,24 +614,100 @@ export class BinanceDepthTracker {
|
||||
}
|
||||
}
|
||||
|
||||
private parsePayload(
|
||||
data: unknown
|
||||
): { b?: [string, string][]; a?: [string, string][]; bids?: [string, string][]; asks?: [string, string][]; u?: number; lastUpdateId?: number } | null {
|
||||
private findBestPrice(book: Map<string, number>, side: "bid" | "ask"): number | null {
|
||||
let best: number | null = null;
|
||||
|
||||
for (const [priceRaw, qty] of book.entries()) {
|
||||
if (!Number.isFinite(qty) || qty <= 0) continue;
|
||||
const price = Number(priceRaw);
|
||||
if (!Number.isFinite(price) || price <= 0) continue;
|
||||
|
||||
if (best == null) {
|
||||
best = price;
|
||||
continue;
|
||||
}
|
||||
if (side === "bid") {
|
||||
if (price > best) best = price;
|
||||
} else if (price < best) {
|
||||
best = price;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private async fetchDepthSnapshot(reason: string): Promise<DepthSnapshotResponse | null> {
|
||||
try {
|
||||
const response = await fetch(this.buildRestDepthUrl(), {
|
||||
method: "GET",
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const json = (await response.json()) as {
|
||||
lastUpdateId?: number;
|
||||
bids?: Array<[string, string]>;
|
||||
asks?: Array<[string, string]>;
|
||||
};
|
||||
|
||||
const lastUpdateId = Number(json.lastUpdateId);
|
||||
if (!Number.isFinite(lastUpdateId) || lastUpdateId <= 0) {
|
||||
throw new Error("invalid lastUpdateId");
|
||||
}
|
||||
|
||||
const bids = Array.isArray(json.bids) ? (json.bids as AsterDepthLevel[]) : [];
|
||||
const asks = Array.isArray(json.asks) ? (json.asks as AsterDepthLevel[]) : [];
|
||||
this.lastRestSyncAt = Date.now();
|
||||
this.restConsecutiveFailures = 0;
|
||||
this.restLastError = null;
|
||||
return { lastUpdateId, bids, asks };
|
||||
} catch (error) {
|
||||
this.restConsecutiveFailures += 1;
|
||||
this.restLastError = this.extractMessage(error);
|
||||
this.options?.logger?.("binanceDepth", `REST sync failed (${reason}): ${this.restLastError}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private parseDepthEvent(data: unknown): DepthUpdateEvent | null {
|
||||
try {
|
||||
const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString("utf-8") : null;
|
||||
if (!text) return null;
|
||||
const parsed = JSON.parse(text);
|
||||
const parsed = JSON.parse(text) as unknown;
|
||||
if (!parsed || typeof parsed !== "object") return null;
|
||||
return parsed as {
|
||||
b?: [string, string][];
|
||||
a?: [string, string][];
|
||||
bids?: [string, string][];
|
||||
asks?: [string, string][];
|
||||
u?: number;
|
||||
lastUpdateId?: number;
|
||||
};
|
||||
|
||||
const maybeCombined = parsed as { data?: unknown };
|
||||
const payload =
|
||||
maybeCombined.data && typeof maybeCombined.data === "object"
|
||||
? (maybeCombined.data as Record<string, unknown>)
|
||||
: (parsed as Record<string, unknown>);
|
||||
|
||||
const eventType = typeof payload.e === "string" ? payload.e : "";
|
||||
if (eventType && eventType !== "depthUpdate") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const U = Number(payload.U);
|
||||
const u = Number(payload.u);
|
||||
if (!Number.isFinite(U) || !Number.isFinite(u)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bidsRaw = Array.isArray(payload.b) ? payload.b : [];
|
||||
const asksRaw = Array.isArray(payload.a) ? payload.a : [];
|
||||
const bids = bidsRaw.filter((level): level is AsterDepthLevel => Array.isArray(level)) as AsterDepthLevel[];
|
||||
const asks = asksRaw.filter((level): level is AsterDepthLevel => Array.isArray(level)) as AsterDepthLevel[];
|
||||
|
||||
return { U, u, bids, asks };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private extractMessage(error: unknown): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,8 +203,10 @@ export class MakerPointsEngine {
|
||||
this.qtyStep = Math.max(1e-9, this.config.qtyStep);
|
||||
this.binanceDepth = new BinanceDepthTracker(resolveBinanceSymbol(this.config.symbol), {
|
||||
baseUrl: process.env.BINANCE_SPOT_WS_URL ?? process.env.BINANCE_WS_URL,
|
||||
restBaseUrl: process.env.BINANCE_REST_URL,
|
||||
levels: 20,
|
||||
ratio: 9,
|
||||
depthWindowBps: 9,
|
||||
speedMs: 100,
|
||||
logger: (context, error) => {
|
||||
this.tradeLog.push("warn", `Binance ${context} 异常: ${extractMessage(error)}`);
|
||||
@@ -221,6 +223,7 @@ export class MakerPointsEngine {
|
||||
this.feedStatus.binance = false;
|
||||
this.tradeLog.push("warn", "Binance 深度连接断开");
|
||||
} else if (state === "stale") {
|
||||
this.feedStatus.binance = false;
|
||||
this.tradeLog.push("warn", "Binance 深度数据过时");
|
||||
} else if (state === "connected") {
|
||||
this.feedStatus.binance = true;
|
||||
@@ -1654,6 +1657,8 @@ export class MakerPointsEngine {
|
||||
const now = Date.now();
|
||||
const standxDepthStale = this.lastStandxDepthTime > 0 && (now - this.lastStandxDepthTime) > DATA_STALE_THRESHOLD_MS;
|
||||
const binanceStale = this.lastBinanceDepthTime > 0 && (now - this.lastBinanceDepthTime) > DATA_STALE_THRESHOLD_MS;
|
||||
const binanceHealth = this.binanceDepth.getHealth();
|
||||
const binanceUnhealthy = !binanceHealth.healthy;
|
||||
|
||||
const standxAccountAge = this.lastStandxAccountTime > 0 ? now - this.lastStandxAccountTime : 0;
|
||||
const standxAccountStaleByAge = this.lastStandxAccountTime > 0 && standxAccountAge > ACCOUNT_DATA_STALE_THRESHOLD_MS;
|
||||
@@ -1675,6 +1680,7 @@ export class MakerPointsEngine {
|
||||
const shouldDefend =
|
||||
standxDepthStale ||
|
||||
binanceStale ||
|
||||
binanceUnhealthy ||
|
||||
standxAccountStale ||
|
||||
accountInvalid ||
|
||||
standxRestUnhealthy ||
|
||||
@@ -1692,6 +1698,8 @@ export class MakerPointsEngine {
|
||||
standxRestLastError: this.standxRestLastError,
|
||||
marginModeNotIsolated,
|
||||
marginMode,
|
||||
binanceUnhealthy,
|
||||
binanceHealthReason: binanceHealth.reason,
|
||||
standxDepthAge: this.lastStandxDepthTime > 0 ? now - this.lastStandxDepthTime : 0,
|
||||
binanceAge: this.lastBinanceDepthTime > 0 ? now - this.lastBinanceDepthTime : 0,
|
||||
standxAccountAge,
|
||||
@@ -1710,6 +1718,8 @@ export class MakerPointsEngine {
|
||||
private enterDefenseMode(staleInfo: {
|
||||
standxDepthStale: boolean;
|
||||
binanceStale: boolean;
|
||||
binanceUnhealthy?: boolean;
|
||||
binanceHealthReason?: string | null;
|
||||
standxAccountStale: boolean;
|
||||
accountInvalid: boolean;
|
||||
standxRestUnhealthy: boolean;
|
||||
@@ -1744,8 +1754,13 @@ export class MakerPointsEngine {
|
||||
if (staleInfo.binanceStale) {
|
||||
staleItems.push(`Binance深度(${Math.round(staleInfo.binanceAge / 1000)}s)`);
|
||||
}
|
||||
if (staleInfo.binanceUnhealthy && staleInfo.binanceHealthReason) {
|
||||
staleItems.push(`Binance簿记异常(${staleInfo.binanceHealthReason})`);
|
||||
}
|
||||
|
||||
this.tradeLog.push("warn", `数据过时检测: ${staleItems.join(", ")},进入防御模式`);
|
||||
const staleSummary = staleItems.length > 0 ? staleItems.join(", ") : "unknown";
|
||||
|
||||
this.tradeLog.push("warn", `数据过时检测: ${staleSummary},进入防御模式`);
|
||||
|
||||
// 发送通知
|
||||
if (!this.defenseModeNotified) {
|
||||
@@ -1754,7 +1769,7 @@ export class MakerPointsEngine {
|
||||
level: "warn",
|
||||
symbol: this.config.symbol,
|
||||
title: "防御模式",
|
||||
message: `数据推送中断: ${staleItems.join(", ")},已取消所有挂单`,
|
||||
message: `数据推送中断: ${staleSummary},已取消所有挂单`,
|
||||
details: staleInfo,
|
||||
});
|
||||
this.defenseModeNotified = true;
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||
|
||||
const ORIGINAL_FETCH = globalThis.fetch;
|
||||
|
||||
class StubAdapter implements ExchangeAdapter {
|
||||
id = "standx";
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
||||
|
||||
async createOrder(): Promise<AsterOrder> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
async cancelOrder(): Promise<void> {}
|
||||
async cancelOrders(): Promise<void> {}
|
||||
async cancelAllOrders(): Promise<void> {}
|
||||
|
||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
||||
return {
|
||||
canTrade: true,
|
||||
canDeposit: true,
|
||||
canWithdraw: true,
|
||||
updateTime: Date.now(),
|
||||
totalWalletBalance: "0",
|
||||
totalUnrealizedProfit: "0",
|
||||
positions: [],
|
||||
assets: [],
|
||||
marketType: "perp",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = ORIGINAL_FETCH;
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("MakerPointsEngine Binance depth health defense", () => {
|
||||
it("enters defense mode when Binance depth tracker is unhealthy", () => {
|
||||
vi.useFakeTimers();
|
||||
globalThis.fetch = vi.fn(async () => {
|
||||
throw new Error("network blocked in test");
|
||||
}) as any;
|
||||
|
||||
const engine = new MakerPointsEngine(
|
||||
{
|
||||
symbol: "BTC-USD",
|
||||
perOrderAmount: 0.01,
|
||||
closeThreshold: 0,
|
||||
stopLossUsd: 1,
|
||||
refreshIntervalMs: 500,
|
||||
maxLogEntries: 20,
|
||||
maxCloseSlippagePct: 0.05,
|
||||
priceTick: 0.1,
|
||||
qtyStep: 0.001,
|
||||
enableBand0To10: true,
|
||||
enableBand10To30: false,
|
||||
enableBand30To100: false,
|
||||
band0To10Amount: 0.01,
|
||||
band10To30Amount: 0.01,
|
||||
band30To100Amount: 0.01,
|
||||
minRepriceBps: 3,
|
||||
enableBinanceDepthCancel: true,
|
||||
filterMinDepth: 0,
|
||||
},
|
||||
new StubAdapter()
|
||||
);
|
||||
|
||||
const now = Date.now();
|
||||
(engine as any).lastStandxDepthTime = now;
|
||||
(engine as any).lastStandxAccountTime = now;
|
||||
(engine as any).lastBinanceDepthTime = now;
|
||||
|
||||
(engine as any).binanceDepth = {
|
||||
getHealth: () => ({
|
||||
started: true,
|
||||
connected: true,
|
||||
orderBookReady: false,
|
||||
restHealthy: false,
|
||||
healthy: false,
|
||||
reason: "orderbook_not_ready",
|
||||
lastEventAt: now,
|
||||
lastSnapshotAt: 0,
|
||||
lastRestSyncAt: 0,
|
||||
localLastUpdateId: 0,
|
||||
}),
|
||||
stop: () => {},
|
||||
};
|
||||
|
||||
(engine as any).checkDataStaleAndDefense();
|
||||
expect((engine as any).defenseMode).toBe(true);
|
||||
|
||||
const logs = ((engine as any).tradeLog.all() as Array<{ detail: string }>).map((entry) => entry.detail);
|
||||
expect(logs.some((detail) => detail.includes("Binance簿记异常(orderbook_not_ready)"))).toBe(true);
|
||||
|
||||
engine.stop();
|
||||
});
|
||||
|
||||
it("exits defense mode after Binance depth health recovers", () => {
|
||||
vi.useFakeTimers();
|
||||
globalThis.fetch = vi.fn(async () => {
|
||||
throw new Error("network blocked in test");
|
||||
}) as any;
|
||||
|
||||
const engine = new MakerPointsEngine(
|
||||
{
|
||||
symbol: "BTC-USD",
|
||||
perOrderAmount: 0.01,
|
||||
closeThreshold: 0,
|
||||
stopLossUsd: 1,
|
||||
refreshIntervalMs: 500,
|
||||
maxLogEntries: 20,
|
||||
maxCloseSlippagePct: 0.05,
|
||||
priceTick: 0.1,
|
||||
qtyStep: 0.001,
|
||||
enableBand0To10: true,
|
||||
enableBand10To30: false,
|
||||
enableBand30To100: false,
|
||||
band0To10Amount: 0.01,
|
||||
band10To30Amount: 0.01,
|
||||
band30To100Amount: 0.01,
|
||||
minRepriceBps: 3,
|
||||
enableBinanceDepthCancel: true,
|
||||
filterMinDepth: 0,
|
||||
},
|
||||
new StubAdapter()
|
||||
);
|
||||
|
||||
const now = Date.now();
|
||||
(engine as any).lastStandxDepthTime = now;
|
||||
(engine as any).lastStandxAccountTime = now;
|
||||
(engine as any).lastBinanceDepthTime = now;
|
||||
|
||||
let unhealthy = true;
|
||||
(engine as any).binanceDepth = {
|
||||
getHealth: () => ({
|
||||
started: true,
|
||||
connected: true,
|
||||
orderBookReady: !unhealthy,
|
||||
restHealthy: !unhealthy,
|
||||
healthy: !unhealthy,
|
||||
reason: unhealthy ? "orderbook_not_ready" : null,
|
||||
lastEventAt: now,
|
||||
lastSnapshotAt: now,
|
||||
lastRestSyncAt: now,
|
||||
localLastUpdateId: unhealthy ? 0 : 100,
|
||||
}),
|
||||
stop: () => {},
|
||||
};
|
||||
|
||||
(engine as any).checkDataStaleAndDefense();
|
||||
expect((engine as any).defenseMode).toBe(true);
|
||||
|
||||
unhealthy = false;
|
||||
(engine as any).checkDataStaleAndDefense();
|
||||
expect((engine as any).defenseMode).toBe(false);
|
||||
|
||||
engine.stop();
|
||||
});
|
||||
});
|
||||
@@ -94,6 +94,8 @@ describe("MakerPointsEngine defense-mode REST polling", () => {
|
||||
(engine as any).enterDefenseMode({
|
||||
standxDepthStale: true,
|
||||
binanceStale: false,
|
||||
binanceUnhealthy: false,
|
||||
binanceHealthReason: null,
|
||||
standxAccountStale: false,
|
||||
accountInvalid: false,
|
||||
standxRestUnhealthy: false,
|
||||
@@ -146,6 +148,8 @@ describe("MakerPointsEngine defense-mode REST polling", () => {
|
||||
(engine as any).enterDefenseMode({
|
||||
standxDepthStale: true,
|
||||
binanceStale: false,
|
||||
binanceUnhealthy: false,
|
||||
binanceHealthReason: null,
|
||||
standxAccountStale: false,
|
||||
accountInvalid: false,
|
||||
standxRestUnhealthy: false,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { ParadexGateway } from "../src/exchanges/paradex/gateway";
|
||||
|
||||
describe("ParadexGateway createOrder", () => {
|
||||
it("sets `size` for closePosition MARKET orders even when amount is omitted", async () => {
|
||||
const createOrder = vi.fn(async (symbol, type, side, amount, price, params) => ({
|
||||
id: "1",
|
||||
symbol,
|
||||
type,
|
||||
side,
|
||||
status: "open",
|
||||
price: price ?? 0,
|
||||
amount: amount ?? Number(params?.size ?? 0),
|
||||
filled: 0,
|
||||
stopPrice: undefined,
|
||||
timestamp: Date.now(),
|
||||
lastUpdateTimestamp: Date.now(),
|
||||
info: {
|
||||
reduceOnly: params?.reduceOnly,
|
||||
closePosition: params?.closePosition,
|
||||
},
|
||||
}));
|
||||
|
||||
const gateway = new ParadexGateway({
|
||||
symbol: "BTC/USDC",
|
||||
displaySymbol: "BTC/USDC",
|
||||
privateKey: "test",
|
||||
walletAddress: "test",
|
||||
logger: () => {},
|
||||
usePro: false,
|
||||
}) as any;
|
||||
|
||||
gateway.exchange = { createOrder, markets: {}, symbols: [] };
|
||||
gateway.initialized = true;
|
||||
gateway.marketSymbol = "BTC/USDC";
|
||||
|
||||
await gateway.createOrder({
|
||||
symbol: "BTC/USDC",
|
||||
side: "SELL",
|
||||
type: "MARKET",
|
||||
quantity: 1.23,
|
||||
reduceOnly: "true",
|
||||
closePosition: "true",
|
||||
});
|
||||
|
||||
expect(createOrder).toHaveBeenCalledTimes(1);
|
||||
const [_symbol, _type, _side, amountArg, _price, extraParams] = createOrder.mock.calls[0]!;
|
||||
expect(amountArg).toBeUndefined();
|
||||
expect(extraParams).toMatchObject({
|
||||
closePosition: true,
|
||||
reduceOnly: true,
|
||||
size: "1.23",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user