Author SHA1 Message Date
discountry 2a1cf24510 Enhance Binance depth health monitoring and defense mode logic
- Updated translations for Binance depth status messages to include depth window information.
- Modified `MakerPointsEngine` to incorporate health checks for the Binance depth tracker, including handling of unhealthy states.
- Improved defense mode activation logic to respond to Binance depth health status, ensuring appropriate logging and notifications.
- Added integration tests for defense mode behavior based on Binance depth health, validating transitions into and out of defense mode.
- Refactored `BinanceDepthTracker` to support health checks and improved connection management.
2026-02-07 21:54:22 +08:00
DisneyandGitHub fa82d45bfb fix size (#20) 2026-02-04 13:26:06 +08:00
discountry d0154e5721 fix den 2026-02-03 18:48:06 +08:00
discountry 52b6a8a076 Add invitation links for Nado registration in trading tutorial 2026-02-03 12:04:36 +08:00
discountry db6a9cfc68 Add Nado trading tutorial for ritmex-bot 2026-02-03 12:02:55 +08:00
DisneyandGitHub 03b8e53d30 Merge pull request #19 from discountry/feat/arb
Feat/arb
2026-02-01 11:15:42 +08:00
10 changed files with 1271 additions and 149 deletions
+27 -29
View File
@@ -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=="],
+550
View File
@@ -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:推荐使用 WSLWindows 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:设置 → 时间和语言 → 同步时间
### 问题 6Gas 费用不足
**原因**Ink 网络上的 ETH 余额不足
**解决方案**
1. 通过跨链桥向 Ink 网络转入少量 ETH0.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
View File
@@ -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"
}
}
+4 -1
View File
@@ -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
View File
@@ -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}",
+428 -104
View File
@@ -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 帧
// 根据文档:必须尽快回复 pongpayload 为 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);
}
}
+17 -2
View File
@@ -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();
});
});
+4
View File
@@ -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,
+56
View File
@@ -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",
});
});
});