23 changed files with 519 additions and 8458 deletions
-13
View File
@@ -114,16 +114,3 @@ PARADEX_WALLET_ADDRESS=
# Enable verbose adapter logging: set to "1" or "true"
# PARADEX_DEBUG=false
# Extended exchange configuration (Starknet)
# Set EXCHANGE=extended to activate. Requires on-chain Stark key + vault id.
EXTENDED_API_KEY=
EXTENDED_STARK_PRIVATE_KEY= # 0x-prefixed Stark private key for signing orders
EXTENDED_VAULT_ID= # Vault/sub-account id from Extended UI
EXTENDED_MARKET=BTC-USD # Defaults to TRADE_SYMBOL when omitted
# Optional overrides (defaults target mainnet host)
# EXTENDED_API_HOST=api.starknet.extended.exchange
# EXTENDED_STREAM_HOST=api.starknet.extended.exchange
# EXTENDED_PRIVATE_STREAM_HOST=api.starknet.extended.exchange
# EXTENDED_USER_AGENT=ritmex-bot
+1 -101
View File
@@ -6,16 +6,12 @@
"dependencies": {
"@grvt/client": "^1.6.4",
"@noble/ed25519": "^3.0.0",
"@x10xchange/stark-crypto-wrapper-wasm": "^0.2.0",
"axios": "^1.12.2",
"bignumber.js": "^9.3.1",
"ccxt": "^4.5.12",
"date-fns": "^4.1.0",
"dotenv": "^17.2.2",
"ethereum-cryptography": "^2.1.3",
"ink": "^6.3.1",
"react": "^19.1.1",
"starknet": "^8.5.4",
"ws": "^8.18.3",
},
"devDependencies": {
@@ -136,18 +132,12 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ=="],
"@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
"@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="],
"@scure/bip32": ["@scure/bip32@1.4.0", "", { "dependencies": { "@noble/curves": "~1.4.0", "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg=="],
"@scure/bip39": ["@scure/bip39@1.3.0", "", { "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ=="],
"@scure/starknet": ["@scure/starknet@1.1.0", "", { "dependencies": { "@noble/curves": "~1.7.0", "@noble/hashes": "~1.6.0" } }, "sha512-83g3M6Ix2qRsPN4wqLDqiRZ2GBNbjVWfboJE/9UjfG+MHr6oDSu/CWgy8hsBSJejr09DkkL+l0Ze4KVrlCIdtQ=="],
"@starknet-io/starknet-types-08": ["@starknet-io/types-js@0.8.4", "", {}, "sha512-0RZ3TZHcLsUTQaq1JhDSCM8chnzO4/XNsSCozwDET64JK5bjFDIf2ZUkta+tl5Nlbf4usoU7uZiDI/Q57kt2SQ=="],
"@starknet-io/starknet-types-09": ["@starknet-io/types-js@0.9.2", "", {}, "sha512-vWOc0FVSn+RmabozIEWcEny1I73nDGTvOrLYJsR1x7LGA3AZmqt4i/aW69o/3i2NN5CVP8Ok6G1ayRQJKye3Wg=="],
"@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
@@ -178,18 +168,12 @@
"@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
"@x10xchange/stark-crypto-wrapper-wasm": ["@x10xchange/stark-crypto-wrapper-wasm@0.2.0", "", {}, "sha512-pi7cHvDgPf9Ri9q0cvrUnCnucafB+F3ch7ia0oEJhKMY+bWvxWRYA2vve9s6UMMNV6PnmTyFZPOYptVGGdLTWA=="],
"abi-wan-kanabi": ["abi-wan-kanabi@2.2.4", "", { "dependencies": { "ansicolors": "^0.3.2", "cardinal": "^2.1.1", "fs-extra": "^10.0.0", "yargs": "^17.7.2" }, "bin": { "generate": "dist/generate.js" } }, "sha512-0aA81FScmJCPX+8UvkXLki3X1+yPQuWxEkqXBVKltgPAK79J+NB+Lp5DouMXa7L6f+zcRlIA/6XO7BN/q9fnvg=="],
"ansi-escapes": ["ansi-escapes@7.1.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-YdhtCd19sKRKfAAUsrcC1wzm4JuzJoiX4pOJqIoW2qmKj5WzG/dL8uUJ0361zaXtHqK7gEhOwtAtz7t3Yq3X5g=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"ansicolors": ["ansicolors@0.3.2", "", {}, "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
@@ -198,16 +182,12 @@
"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=="],
"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=="],
"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=="],
"cardinal": ["cardinal@2.1.1", "", { "dependencies": { "ansicolors": "~0.3.2", "redeyed": "~2.1.0" }, "bin": { "cdl": "./bin/cdl.js" } }, "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw=="],
"ccxt": ["ccxt@4.5.12", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-2lfL2TKjq4vBkQUQWJfDqFywhvYCZmk9r0SWC8GqA4AHZ6qozKVUJowxQTvdRsLX9jBwYSE0nc7JVurBrQ6SHg=="],
"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=="],
@@ -222,22 +202,14 @@
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"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=="],
@@ -266,12 +238,8 @@
"esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="],
@@ -284,14 +252,10 @@
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
@@ -300,8 +264,6 @@
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
@@ -318,10 +280,6 @@
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
"lossless-json": ["lossless-json@4.3.0", "", {}, "sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g=="],
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
@@ -340,8 +298,6 @@
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="],
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
@@ -360,10 +316,6 @@
"react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="],
"redeyed": ["redeyed@2.1.1", "", { "dependencies": { "esprima": "~4.0.0" } }, "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"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=="],
@@ -382,8 +334,6 @@
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"starknet": ["starknet@8.9.0", "", { "dependencies": { "@noble/curves": "~1.7.0", "@noble/hashes": "~1.6.0", "@scure/base": "~1.2.1", "@scure/starknet": "1.1.0", "@starknet-io/starknet-types-08": "npm:@starknet-io/types-js@~0.8.4", "@starknet-io/starknet-types-09": "npm:@starknet-io/types-js@~0.9.1", "abi-wan-kanabi": "2.2.4", "lossless-json": "^4.2.0", "pako": "^2.0.4", "ts-mixer": "^6.0.3" } }, "sha512-uhXSCKW0SMBGckd4yVlscVz4DMkQqZ+/RTpfxZ5J46e6AuCUHOqGKv61tqgUP9GhdvgHV/XyN+kdmlyZn3rXjA=="],
"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=="],
@@ -404,16 +354,12 @@
"tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"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=="],
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
@@ -428,56 +374,10 @@
"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=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
"@scure/bip32/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="],
"@scure/bip39/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="],
"@scure/starknet/@noble/curves": ["@noble/curves@1.7.0", "", { "dependencies": { "@noble/hashes": "1.6.0" } }, "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw=="],
"@scure/starknet/@noble/hashes": ["@noble/hashes@1.6.1", "", {}, "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w=="],
"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=="],
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"starknet/@noble/curves": ["@noble/curves@1.7.0", "", { "dependencies": { "@noble/hashes": "1.6.0" } }, "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw=="],
"starknet/@noble/hashes": ["@noble/hashes@1.6.1", "", {}, "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w=="],
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"@scure/starknet/@noble/curves/@noble/hashes": ["@noble/hashes@1.6.0", "", {}, "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="],
"cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"starknet/@noble/curves/@noble/hashes": ["@noble/hashes@1.6.0", "", {}, "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="],
"yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
}
}
-5526
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -25,16 +25,12 @@
"dependencies": {
"@grvt/client": "^1.6.4",
"@noble/ed25519": "^3.0.0",
"@x10xchange/stark-crypto-wrapper-wasm": "^0.2.0",
"axios": "^1.12.2",
"bignumber.js": "^9.3.1",
"ccxt": "^4.5.12",
"date-fns": "^4.1.0",
"dotenv": "^17.2.2",
"ethereum-cryptography": "^2.1.3",
"ink": "^6.3.1",
"react": "^19.1.1",
"starknet": "^8.5.4",
"ws": "^8.18.3"
}
}
+16 -46
View File
@@ -30,7 +30,6 @@ const SYMBOL_PRIORITY_BY_EXCHANGE: Record<SupportedExchangeId, { envKeys: string
lighter: { envKeys: ["LIGHTER_SYMBOL", "TRADE_SYMBOL"], fallback: "BTCUSDT" },
backpack: { envKeys: ["BACKPACK_SYMBOL", "TRADE_SYMBOL"], fallback: "BTCUSDC" },
paradex: { envKeys: ["PARADEX_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC/USDC" },
extended: { envKeys: ["EXTENDED_MARKET", "EXTENDED_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-USD" },
};
export function resolveSymbolFromEnv(explicitExchangeId?: SupportedExchangeId | string | null): string {
@@ -117,25 +116,17 @@ export interface BasisArbConfig {
arbAmount: number; // base asset amount to arb (e.g., ASTER amount when ASTERUSDT)
}
export type GridDirection = "both" | "long" | "short";
export interface GridConfig {
symbol: string;
lowerPrice: number;
upperPrice: number;
gridLevels: number;
orderSize: number;
maxPositionSize: number;
tradeAmount: number;
refreshIntervalMs: number;
maxLogEntries: number;
priceTick: number;
qtyStep: number;
direction: GridDirection;
stopLossPct: number;
restartTriggerPct: number;
autoRestart: boolean;
gridMode: "geometric";
maxCloseSlippagePct: number;
levelsPerSide: number;
spacingPct: number;
stopLossBufferPct: number;
maxLogEntries: number;
maxPositionSize: number;
}
const resolveBasisSymbol = (envKeys: string[], fallback: string): string => {
@@ -163,17 +154,8 @@ export const basisConfig: BasisArbConfig = {
arbAmount: parseNumber(process.env.ARB_AMOUNT, parseNumber(process.env.TRADE_AMOUNT, 0)),
};
const resolveGridDirection = (raw: string | undefined, fallback: GridDirection): GridDirection => {
if (!raw) return fallback;
const normalized = raw.trim().toLowerCase();
if (normalized === "long" || normalized === "long-only") return "long";
if (normalized === "short" || normalized === "short-only") return "short";
if (normalized === "both" || normalized === "dual" || normalized === "bi" || normalized === "two-way") return "both";
return fallback;
};
const resolveGridMaxPosition = (orderSize: number, levels: number): number => {
const fallback = Math.max(orderSize * Math.max(levels - 1, 1), orderSize);
const resolveGridMaxPosition = (tradeAmount: number): number => {
const fallback = Math.max(tradeAmount * 10, tradeAmount);
const raw = process.env.GRID_MAX_POSITION_SIZE ?? process.env.GRID_MAX_POSITION ?? process.env.GRID_POSITION_CAP;
const parsed = parseNumber(raw, fallback);
return parsed > 0 ? parsed : fallback;
@@ -181,30 +163,18 @@ const resolveGridMaxPosition = (orderSize: number, levels: number): number => {
export const gridConfig: GridConfig = {
symbol: resolveSymbolFromEnv(),
lowerPrice: parseNumber(process.env.GRID_LOWER_PRICE ?? process.env.GRID_LOWER_BOUND, 0),
upperPrice: parseNumber(process.env.GRID_UPPER_PRICE ?? process.env.GRID_UPPER_BOUND, 0),
gridLevels: Math.max(2, Math.floor(parseNumber(process.env.GRID_LEVELS, 10))),
orderSize: parseNumber(process.env.GRID_ORDER_SIZE, parseNumber(process.env.TRADE_AMOUNT, 0.001)),
maxPositionSize: 0, // placeholder, replaced below
refreshIntervalMs: parseNumber(process.env.GRID_REFRESH_INTERVAL_MS, 1_000),
maxLogEntries: parseNumber(process.env.GRID_MAX_LOG_ENTRIES, 200),
tradeAmount: parseNumber(process.env.TRADE_AMOUNT, 0.001),
refreshIntervalMs: parseNumber(process.env.GRID_REFRESH_INTERVAL_MS, 800),
priceTick: parseNumber(process.env.GRID_PRICE_TICK ?? process.env.PRICE_TICK, 0.1),
qtyStep: parseNumber(process.env.GRID_QTY_STEP ?? process.env.QTY_STEP, 0.001),
direction: resolveGridDirection(process.env.GRID_DIRECTION, "both"),
stopLossPct: Math.max(0, parseNumber(process.env.GRID_STOP_LOSS_PCT, 0.01)),
restartTriggerPct: Math.max(0, parseNumber(process.env.GRID_RESTART_TRIGGER_PCT, 0.01)),
autoRestart: parseBoolean(process.env.GRID_AUTO_RESTART_ENABLED ?? process.env.GRID_ENABLE_AUTO_RESTART, true),
gridMode: "geometric",
maxCloseSlippagePct: Math.max(
0,
parseNumber(
process.env.GRID_MAX_CLOSE_SLIPPAGE_PCT ?? process.env.MAX_CLOSE_SLIPPAGE_PCT,
0.05
)
),
levelsPerSide: Math.max(5, Math.floor(parseNumber(process.env.GRID_LEVELS_PER_SIDE, 15))),
spacingPct: Math.max(0.0001, parseNumber(process.env.GRID_SPACING_PCT, 0.00025)),
stopLossBufferPct: Math.max(0.001, parseNumber(process.env.GRID_STOP_BUFFER_PCT, 0.003)),
maxLogEntries: parseNumber(process.env.GRID_MAX_LOG_ENTRIES, 200),
maxPositionSize: 0, // placeholder updated below
};
gridConfig.maxPositionSize = resolveGridMaxPosition(gridConfig.orderSize, gridConfig.gridLevels);
gridConfig.maxPositionSize = resolveGridMaxPosition(gridConfig.tradeAmount);
export function isBasisStrategyEnabled(): boolean {
const raw = process.env.ENABLE_BASIS_STRATEGY;
+1 -8
View File
@@ -4,7 +4,6 @@ import { GrvtExchangeAdapter, type GrvtCredentials } from "./grvt/adapter";
import { LighterExchangeAdapter, type LighterCredentials } from "./lighter/adapter";
import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/adapter";
import { ParadexExchangeAdapter, type ParadexCredentials } from "./paradex/adapter";
import { ExtendedExchangeAdapter, type ExtendedCredentials } from "./extended/adapter";
export interface ExchangeFactoryOptions {
symbol: string;
@@ -14,10 +13,9 @@ export interface ExchangeFactoryOptions {
lighter?: LighterCredentials;
backpack?: BackpackCredentials;
paradex?: ParadexCredentials;
extended?: ExtendedCredentials;
}
export type SupportedExchangeId = "aster" | "grvt" | "lighter" | "backpack" | "paradex" | "extended";
export type SupportedExchangeId = "aster" | "grvt" | "lighter" | "backpack" | "paradex";
export function resolveExchangeId(value?: string | null): SupportedExchangeId {
const fallback = (value ?? process.env.EXCHANGE ?? process.env.TRADE_EXCHANGE ?? "aster")
@@ -28,7 +26,6 @@ export function resolveExchangeId(value?: string | null): SupportedExchangeId {
if (fallback === "lighter") return "lighter";
if (fallback === "backpack") return "backpack";
if (fallback === "paradex") return "paradex";
if (fallback === "extended") return "extended";
return "aster";
}
@@ -37,7 +34,6 @@ export function getExchangeDisplayName(id: SupportedExchangeId): string {
if (id === "lighter") return "Lighter";
if (id === "backpack") return "Backpack";
if (id === "paradex") return "Paradex";
if (id === "extended") return "Extended";
return "AsterDex";
}
@@ -55,8 +51,5 @@ export function createExchangeAdapter(options: ExchangeFactoryOptions): Exchange
if (id === "paradex") {
return new ParadexExchangeAdapter({ ...options.paradex, symbol: options.symbol });
}
if (id === "extended") {
return new ExtendedExchangeAdapter({ ...options.extended, market: options.symbol });
}
return new AsterExchangeAdapter({ ...options.aster, symbol: options.symbol });
}
-172
View File
@@ -1,172 +0,0 @@
import { setTimeout, clearTimeout } from "timers";
import type {
AccountListener,
DepthListener,
ExchangeAdapter,
ExchangePrecision,
KlineListener,
OrderListener,
TickerListener,
} from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types";
import { ExtendedGateway, type ExtendedGatewayOptions } from "./gateway";
export interface ExtendedCredentials {
apiKey?: string;
starkPrivateKey?: string;
vaultId?: string;
market?: string;
apiHost?: string;
streamHost?: string;
privateStreamHost?: string;
userAgent?: string;
}
export class ExtendedExchangeAdapter implements ExchangeAdapter {
readonly id = "extended";
private readonly gateway: ExtendedGateway;
private initPromise: Promise<void> | null = null;
private readonly initContexts = new Set<string>();
private retryTimer: ReturnType<typeof setTimeout> | null = null;
private retryDelayMs = 3000;
private lastInitErrorAt = 0;
constructor(credentials: ExtendedCredentials = {}) {
const apiKey = credentials.apiKey ?? process.env.EXTENDED_API_KEY;
const starkPrivateKey = credentials.starkPrivateKey ?? process.env.EXTENDED_STARK_PRIVATE_KEY;
const vaultId = credentials.vaultId ?? process.env.EXTENDED_VAULT_ID;
const market = credentials.market ?? process.env.EXTENDED_MARKET ?? process.env.TRADE_SYMBOL ?? "BTC-USD";
if (!apiKey) throw new Error("Missing EXTENDED_API_KEY");
if (!starkPrivateKey) throw new Error("Missing EXTENDED_STARK_PRIVATE_KEY");
if (!vaultId) throw new Error("Missing EXTENDED_VAULT_ID");
const options: ExtendedGatewayOptions = {
apiKey,
starkPrivateKey,
vaultId,
market,
apiHost: credentials.apiHost ?? process.env.EXTENDED_API_HOST,
streamHost: credentials.streamHost ?? process.env.EXTENDED_STREAM_HOST,
privateStreamHost: credentials.privateStreamHost ?? process.env.EXTENDED_PRIVATE_STREAM_HOST,
userAgent: credentials.userAgent ?? process.env.EXTENDED_USER_AGENT,
logger: (context, error) => this.log(context, error),
};
this.gateway = new ExtendedGateway(options);
}
supportsTrailingStops(): boolean {
return false;
}
watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
}
watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
}
watchDepth(_symbol: string, cb: DepthListener): void {
void this.ensureInitialized("watchDepth");
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
}
watchTicker(_symbol: string, cb: TickerListener): void {
void this.ensureInitialized("watchTicker");
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
}
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized(`watchKlines:${interval}`);
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized("createOrder");
return this.gateway.createOrder(params);
}
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder({ orderId: params.orderId });
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders({ orderIdList: params.orderIdList });
}
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders();
}
async getPrecision(): Promise<ExchangePrecision | null> {
try {
const precision = await this.gateway.getPrecision();
if (!precision) return null;
return {
priceTick: precision.priceTick,
qtyStep: precision.qtyStep,
};
} catch (error) {
this.log("getPrecision", error);
return null;
}
}
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
this.log(`${context} handler`, error);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string): Promise<void> {
if (!this.initPromise) {
this.initContexts.clear();
this.initPromise = this.gateway.ensureInitialized().catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
this.log(context, error);
}
private log(context: string, error: unknown): void {
if (process.env.EXTENDED_DEBUG === "1" || process.env.EXTENDED_DEBUG === "true") {
console.error(`[ExtendedExchangeAdapter] ${context}`, error);
}
}
}
-713
View File
@@ -1,713 +0,0 @@
import axios, { AxiosInstance } from "axios";
import NodeWebSocket from "ws";
import { setTimeout, clearTimeout, setInterval, clearInterval } from "timers";
import type {
AccountListener,
DepthListener,
KlineListener,
OrderListener,
TickerListener,
} from "../adapter";
import type {
AsterAccountAsset,
AsterAccountPosition,
AsterAccountSnapshot,
AsterDepth,
AsterKline,
AsterOrder,
AsterTicker,
CreateOrderParams,
} from "../types";
import { toDecimal } from "./math";
import { ExtendedOrderBuilder, type ExtendedOrderContext } from "./order-builder";
import { tryInitExtendedWasm } from "./signing";
import type {
ExtendedBalance,
ExtendedCandle,
ExtendedDepthMessage,
ExtendedFees,
ExtendedMarket,
ExtendedOrder,
ExtendedPosition,
ExtendedStarknetDomain,
ExtendedTrade,
} from "./types";
const WebSocketCtor: typeof globalThis.WebSocket =
typeof globalThis.WebSocket !== "undefined" ? globalThis.WebSocket : ((NodeWebSocket as unknown) as typeof globalThis.WebSocket);
type WebSocket = typeof WebSocketCtor extends { new (...args: any[]): infer R } ? R : NodeWebSocket;
const RECONNECT_DELAY_MS = 3000;
export interface ExtendedGatewayOptions {
apiKey: string;
starkPrivateKey: string;
vaultId: string;
market: string;
apiHost?: string;
streamHost?: string;
privateStreamHost?: string;
userAgent?: string;
logger?: (context: string, error: unknown) => void;
}
interface AccountState {
balance: ExtendedBalance | null;
positions: ExtendedPosition[];
orders: ExtendedOrder[];
}
interface DepthState {
bids: [string, string][];
asks: [string, string][];
seq: number;
ts: number;
}
export class ExtendedGateway {
private readonly options: ExtendedGatewayOptions;
private readonly logger: (context: string, error: unknown) => void;
private readonly axios: AxiosInstance;
private marketInfo: ExtendedMarket | null = null;
private fees: ExtendedFees | null = null;
private domain: ExtendedStarknetDomain | null = null;
private orderBuilder: ExtendedOrderBuilder | null = null;
private initialized = false;
private accountState: AccountState = { balance: null, positions: [], orders: [] };
private readonly accountListeners = new Set<AccountListener>();
private readonly orderListeners = new Set<OrderListener>();
private readonly depthListeners = new Set<DepthListener>();
private readonly tickerListeners = new Set<TickerListener>();
private readonly klineListeners = new Map<string, Set<KlineListener>>();
private accountWs: WebSocket | null = null;
private depthWs: WebSocket | null = null;
private tradesWs: WebSocket | null = null;
private candleWs = new Map<string, WebSocket>();
private reconnectTimers: Array<ReturnType<typeof setTimeout>> = [];
private lastDepth: DepthState | null = null;
private lastTrade: ExtendedTrade | null = null;
constructor(options: ExtendedGatewayOptions) {
this.options = options;
this.logger =
options.logger ??
((context, error) => {
console.error(`[ExtendedGateway] ${context}`, error);
});
const baseURL = `https://${options.apiHost ?? "api.starknet.extended.exchange"}`;
this.axios = axios.create({
baseURL,
headers: {
"X-Api-Key": options.apiKey,
"User-Agent": options.userAgent ?? "ritmex-bot",
},
});
}
async ensureInitialized(): Promise<void> {
if (this.initialized) return;
this.logInfo("init", `Initializing Extended for ${this.options.market}`);
await tryInitExtendedWasm();
await Promise.all([this.loadMarket(), this.loadFees(), this.loadDomain()]);
this.orderBuilder = new ExtendedOrderBuilder(this.buildOrderContext());
await this.loadInitialAccount();
this.startAccountStream();
this.startDepthStream();
this.startTradesStream();
this.initialized = true;
this.logInfo("init", "Extended gateway ready");
}
destroy(): void {
this.closeSocket(this.accountWs);
this.closeSocket(this.depthWs);
this.closeSocket(this.tradesWs);
for (const ws of this.candleWs.values()) {
this.closeSocket(ws);
}
this.candleWs.clear();
this.reconnectTimers.forEach((timer) => clearTimeout(timer));
this.reconnectTimers = [];
}
onAccount(listener: AccountListener): void {
this.accountListeners.add(listener);
const snapshot = this.buildAccountSnapshot();
if (snapshot) {
try {
listener(snapshot);
} catch (error) {
this.logger("accountReplay", error);
}
}
}
onOrders(listener: OrderListener): void {
this.orderListeners.add(listener);
if (this.accountState.orders.length) {
try {
listener(this.accountState.orders.map((order) => this.mapOrder(order)));
} catch (error) {
this.logger("ordersReplay", error);
}
}
}
onDepth(listener: DepthListener): void {
this.depthListeners.add(listener);
if (this.lastDepth) {
listener(this.mapDepth(this.lastDepth));
}
}
onTicker(listener: TickerListener): void {
this.tickerListeners.add(listener);
const ticker = this.buildTicker();
if (ticker) {
listener(ticker);
}
}
watchKlines(interval: string, listener: KlineListener): void {
const set = this.klineListeners.get(interval) ?? new Set<KlineListener>();
set.add(listener);
this.klineListeners.set(interval, set);
this.ensureCandleStream(interval);
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
if (!this.orderBuilder) {
await this.ensureInitialized();
}
const builder = this.orderBuilder!;
const marketPrice = this.estimateMarketPrice(params.side);
const built = builder.build({ ...params, marketPrice });
const { data } = await this.axios.post<{ data?: ExtendedOrder; status?: string }>("/api/v1/user/order", built.payload);
// Some responses only return status; fallback to local mapping using request params.
const order = data?.data ?? null;
if (order) {
return this.mapOrder(order);
}
// Fallback: return lightweight order reflecting submission.
return this.mapOrder({
id: built.orderId,
market: this.options.market,
type: this.normalizeOrderType(params.type),
side: params.side,
status: "NEW",
price: built.payload.price as string,
qty: built.payload.qty as string,
filledQty: "0",
reduceOnly: built.payload.reduceOnly as boolean,
postOnly: built.payload.postOnly as boolean,
createdTime: Date.now(),
updatedTime: Date.now(),
trigger: built.payload.trigger as any,
});
}
async cancelOrder(params: { orderId: number | string }): Promise<void> {
await this.axios.delete(`/api/v1/user/order/${params.orderId}`);
}
async cancelOrders(params: { orderIdList: Array<number | string> }): Promise<void> {
await this.axios.post("/api/v1/user/order/massCancel", { orderIds: params.orderIdList });
}
async cancelAllOrders(): Promise<void> {
await this.axios.post("/api/v1/user/order/massCancel", { markets: [this.options.market] });
}
async getPrecision(): Promise<{ priceTick: number; qtyStep: number } | null> {
if (!this.marketInfo) return null;
const { tradingConfig } = this.marketInfo;
return {
priceTick: Number(tradingConfig.minPriceChange),
qtyStep: Number(tradingConfig.minOrderSizeChange),
};
}
// ---- Internal fetchers --------------------------------------------------
private async loadMarket(): Promise<void> {
const { data } = await this.axios.get<{ data: ExtendedMarket[] }>("/api/v1/info/markets", {
params: { market: [this.options.market] },
});
const market = (data?.data ?? [])[0];
if (!market) {
throw new Error(`Extended market not found: ${this.options.market}`);
}
this.marketInfo = market;
}
private async loadFees(): Promise<void> {
const { data } = await this.axios.get<{ data: ExtendedFees[] }>("/api/v1/user/fees", {
params: { market: [this.options.market] },
});
const fees = (data?.data ?? [])[0];
if (!fees) {
throw new Error("Unable to fetch Extended fee configuration");
}
this.fees = fees;
}
private async loadDomain(): Promise<void> {
const { data } = await this.axios.get<{ data: ExtendedStarknetDomain }>("/api/v1/info/starknet");
if (!data?.data) {
throw new Error("Failed to load Extended Starknet domain");
}
this.domain = data.data;
}
private async loadInitialAccount(): Promise<void> {
try {
const [balanceRes, positionsRes, ordersRes] = await Promise.all([
this.axios.get<{ data: ExtendedBalance }>("/api/v1/user/balance"),
this.axios.get<{ data: ExtendedPosition[] }>("/api/v1/user/positions", {
params: { market: [this.options.market] },
}),
this.axios.get<{ data: ExtendedOrder[] }>("/api/v1/user/orders", {
params: { market: [this.options.market] },
}),
]);
this.accountState.balance = balanceRes.data?.data ?? null;
this.accountState.positions = positionsRes.data?.data ?? [];
this.accountState.orders = ordersRes.data?.data ?? [];
this.emitAccount();
this.emitOrders();
} catch (error) {
this.logger("loadInitialAccount", error);
}
}
private buildOrderContext(): ExtendedOrderContext {
if (!this.marketInfo || !this.fees || !this.domain) {
throw new Error("Extended gateway not ready");
}
return {
market: this.marketInfo,
fees: this.fees,
domain: this.domain,
vaultId: this.options.vaultId,
starkPrivateKey: this.options.starkPrivateKey,
builderFeeRate: this.fees.builderFeeRate,
};
}
// ---- WebSocket handling -------------------------------------------------
private startAccountStream(): void {
const host = this.options.privateStreamHost ?? this.options.streamHost ?? "api.starknet.extended.exchange";
const url = `wss://${host}/stream.extended.exchange/v1/account`;
this.accountWs = this.createSocket(
url,
{ headers: { "X-Api-Key": this.options.apiKey, "User-Agent": this.options.userAgent ?? "ritmex-bot" } },
"account",
(payload) => {
this.handleAccountMessage(payload);
},
(socket) => {
this.accountWs = socket;
}
);
}
private startDepthStream(): void {
const host = this.options.streamHost ?? "api.starknet.extended.exchange";
const url = `wss://${host}/stream.extended.exchange/v1/orderbooks/${encodeURIComponent(this.options.market)}?depth=1`;
this.depthWs = this.createSocket(
url,
undefined,
"depth",
(payload) => {
this.handleDepthMessage(payload as ExtendedDepthMessage);
},
(socket) => {
this.depthWs = socket;
}
);
}
private startTradesStream(): void {
const host = this.options.streamHost ?? "api.starknet.extended.exchange";
const url = `wss://${host}/stream.extended.exchange/v1/publicTrades/${encodeURIComponent(this.options.market)}`;
this.tradesWs = this.createSocket(
url,
undefined,
"trades",
(payload) => {
this.handleTradesMessage(payload as { data?: ExtendedTrade[]; ts?: number; seq?: number });
},
(socket) => {
this.tradesWs = socket;
}
);
}
private ensureCandleStream(interval: string): void {
if (this.candleWs.has(interval)) return;
const host = this.options.streamHost ?? "api.starknet.extended.exchange";
const url = `wss://${host}/stream.extended.exchange/v1/candles/${encodeURIComponent(this.options.market)}/trades?interval=${encodeURIComponent(interval)}`;
const ws = this.createSocket(
url,
undefined,
`candle:${interval}`,
(payload) => {
this.handleCandleMessage(interval, payload as { data?: ExtendedCandle[] });
},
(socket) => {
this.candleWs.set(interval, socket);
}
);
this.candleWs.set(interval, ws);
}
private createSocket(
url: string,
options: NodeWebSocket.ClientOptions | undefined,
context: string,
handler: (data: any) => void,
onCreate?: (socket: WebSocket) => void
): WebSocket {
const ws = new WebSocketCtor(url, options as any);
if (onCreate) {
onCreate(ws as any);
}
let pingTimer: ReturnType<typeof setInterval> | null = null;
ws.onopen = () => {
if (pingTimer) clearInterval(pingTimer);
pingTimer = setInterval(() => {
try {
ws.send(JSON.stringify({ type: "ping" }));
} catch (_error) {
/* ignore */
}
}, 15_000);
this.logInfo(`${context}:open`, url);
};
ws.onmessage = (event) => {
try {
const data = typeof event.data === "string" ? event.data : event.data.toString();
if (data === "ping") {
ws.send("pong");
return;
}
handler(JSON.parse(data));
} catch (error) {
this.logger(`${context}:message`, error);
}
};
ws.onerror = (error) => {
this.logError(`${context}:error`, error);
};
ws.onclose = () => {
if (pingTimer) clearInterval(pingTimer);
const timer = setTimeout(() => {
this.logInfo(`${context}:reconnect`, `reconnecting to ${url}`);
this.reconnectTimers = this.reconnectTimers.filter((t) => t !== timer);
this.createSocket(url, options, context, handler, onCreate);
}, RECONNECT_DELAY_MS);
this.reconnectTimers.push(timer);
this.logInfo(`${context}:close`, `closed ${url}`);
};
return ws;
}
private closeSocket(socket: WebSocket | null): void {
if (socket && socket.readyState === socket.OPEN) {
socket.close();
}
}
// ---- WS handlers -------------------------------------------------------
private handleAccountMessage(message: any): void {
if (!message || typeof message !== "object") return;
const type = message.type;
const data = message.data ?? {};
if (type === "BALANCE" && data.balance) {
this.accountState.balance = data.balance as ExtendedBalance;
this.emitAccount();
this.logInfo("account:balance", "updated");
return;
}
if (type === "POSITION" && Array.isArray(data.positions)) {
this.accountState.positions = data.positions as ExtendedPosition[];
this.emitAccount();
this.logInfo("account:positions", `positions=${data.positions.length}`);
return;
}
if (type === "ORDER" && Array.isArray(data.orders)) {
this.accountState.orders = data.orders as ExtendedOrder[];
this.emitOrders();
this.logInfo("account:orders", `orders=${data.orders.length}`);
return;
}
if (type === "TRADE" && Array.isArray(data.trades)) {
// Trades can update fills; refresh orders if present.
if (Array.isArray(data.orders)) {
this.accountState.orders = data.orders as ExtendedOrder[];
this.emitOrders();
}
return;
}
// Fallback: handle messages where data is nested without type
const payload = message.data ?? message;
if (payload?.balance) {
this.accountState.balance = payload.balance as ExtendedBalance;
this.emitAccount();
this.logInfo("account:balance", "snapshot");
}
if (payload?.positions) {
this.accountState.positions = payload.positions as ExtendedPosition[];
this.emitAccount();
this.logInfo("account:positions", `positions=${(payload.positions as ExtendedPosition[]).length}`);
}
if (payload?.orders) {
this.accountState.orders = payload.orders as ExtendedOrder[];
this.emitOrders();
this.logInfo("account:orders", `orders=${(payload.orders as ExtendedOrder[]).length}`);
}
}
private handleDepthMessage(message: ExtendedDepthMessage): void {
if (!message?.data) return;
const bids = (message.data.b ?? []).map((level) => [String(level.p), String(level.q)] as [string, string]);
const asks = (message.data.a ?? []).map((level) => [String(level.p), String(level.q)] as [string, string]);
this.lastDepth = { bids, asks, seq: message.seq ?? Date.now(), ts: message.ts ?? Date.now() };
this.emitDepth();
this.emitTicker();
if (bids.length || asks.length) {
this.logDebug("depth", `bids=${bids[0]?.[0] ?? "-"} asks=${asks[0]?.[0] ?? "-"}`);
}
}
private handleTradesMessage(message: { data?: ExtendedTrade[]; ts?: number }): void {
const trades = message?.data ?? [];
if (!trades.length) return;
this.lastTrade = trades[trades.length - 1];
this.emitTicker();
this.logDebug("trades", `last=${this.lastTrade.p} side=${this.lastTrade.S}`);
}
private handleCandleMessage(interval: string, payload: { data?: ExtendedCandle[] }): void {
const candles = payload?.data ?? [];
if (!candles.length) return;
const mapped = candles.map((candle) => this.mapKline(candle));
const listeners = this.klineListeners.get(interval);
if (!listeners) return;
listeners.forEach((listener) => {
try {
listener(mapped);
} catch (error) {
this.logger(`kline:${interval}`, error);
}
});
}
// ---- Emitters ----------------------------------------------------------
private emitAccount(): void {
const snapshot = this.buildAccountSnapshot();
if (!snapshot) return;
for (const listener of this.accountListeners) {
try {
listener(snapshot);
} catch (error) {
this.logError("accountListener", error);
}
}
}
private emitOrders(): void {
const mapped = this.accountState.orders.map((order) => this.mapOrder(order));
for (const listener of this.orderListeners) {
try {
listener(mapped);
} catch (error) {
this.logError("orderListener", error);
}
}
}
private emitDepth(): void {
if (!this.lastDepth) return;
const depth = this.mapDepth(this.lastDepth);
for (const listener of this.depthListeners) {
try {
listener(depth);
} catch (error) {
this.logError("depthListener", error);
}
}
}
private emitTicker(): void {
const ticker = this.buildTicker();
if (!ticker) return;
for (const listener of this.tickerListeners) {
try {
listener(ticker);
} catch (error) {
this.logError("tickerListener", error);
}
}
}
// ---- Mapping helpers ---------------------------------------------------
private buildAccountSnapshot(): AsterAccountSnapshot | null {
const balance = this.accountState.balance;
if (!balance) return null;
const positions = (this.accountState.positions ?? []).map((position) => this.mapPosition(position));
const totalUnrealized = positions.reduce((acc, pos) => acc + (Number(pos.unrealizedProfit) || 0), 0);
const assets: AsterAccountAsset[] = [
{
asset: balance.collateralName ?? "USDC",
walletBalance: balance.balance ?? "0",
availableBalance: balance.availableForTrade ?? balance.balance ?? "0",
updateTime: balance.updatedTime ?? Date.now(),
unrealizedProfit: balance.unrealisedPnl,
marginBalance: balance.equity,
maxWithdrawAmount: balance.availableForWithdrawal,
},
];
return {
canTrade: true,
canDeposit: true,
canWithdraw: true,
updateTime: balance.updatedTime ?? Date.now(),
totalWalletBalance: balance.balance ?? "0",
totalUnrealizedProfit: Number.isFinite(totalUnrealized) ? String(totalUnrealized) : "0",
availableBalance: balance.availableForTrade ?? balance.balance ?? "0",
maxWithdrawAmount: balance.availableForWithdrawal,
positions,
assets,
};
}
private mapPosition(position: ExtendedPosition): AsterAccountPosition {
const size = toDecimal(position.size);
const signed = position.side === "SHORT" ? size.negated() : size;
return {
symbol: position.market,
positionAmt: signed.toString(10),
entryPrice: position.openPrice ?? "0",
unrealizedProfit: position.unrealisedPnl ?? "0",
positionSide: position.side === "SHORT" ? "SHORT" : "LONG",
updateTime: position.updatedAt ?? Date.now(),
markPrice: position.markPrice,
leverage: position.leverage,
liquidationPrice: position.liquidationPrice,
marginType: "cross",
};
}
private mapOrder(order: ExtendedOrder): AsterOrder {
const stopPrice =
order.trigger?.triggerPrice ??
order.takeProfit?.triggerPrice ??
order.stopLoss?.triggerPrice ??
null;
return {
orderId: order.id,
clientOrderId: order.externalId ?? String(order.id),
symbol: order.market,
side: order.side,
type: this.normalizeOrderType(order.type),
status: order.status,
price: order.price ?? "0",
origQty: order.qty ?? "0",
executedQty: order.filledQty ?? "0",
stopPrice: stopPrice ? String(stopPrice) : "0",
time: order.createdTime ?? Date.now(),
updateTime: order.updatedTime ?? order.createdTime ?? Date.now(),
reduceOnly: Boolean(order.reduceOnly),
closePosition: Boolean(order.reduceOnly),
workingType: order.trigger?.triggerPriceType,
activationPrice: stopPrice ? String(stopPrice) : undefined,
avgPrice: order.averagePrice ?? undefined,
cumQuote: order.payedFee,
timeInForce: order.type === "MARKET" ? "IOC" : "GTC",
};
}
private mapDepth(state: DepthState): AsterDepth {
return {
lastUpdateId: state.seq,
bids: state.bids,
asks: state.asks,
eventTime: state.ts,
tradeTime: state.ts,
};
}
private mapKline(candle: ExtendedCandle): AsterKline {
return {
eventType: "kline",
eventTime: candle.T,
openTime: candle.T,
closeTime: candle.T,
interval: "stream",
open: candle.o,
high: candle.h,
low: candle.l,
close: candle.c,
volume: candle.v ?? "0",
numberOfTrades: 0,
isClosed: true,
};
}
private buildTicker(): AsterTicker | null {
const lastPrice = this.lastTrade?.p ?? this.marketInfo?.marketStats?.lastPrice;
if (!lastPrice && !this.lastDepth) return null;
return {
symbol: this.options.market,
lastPrice: lastPrice ?? "0",
openPrice: this.marketInfo?.marketStats?.indexPrice ?? lastPrice ?? "0",
highPrice: this.marketInfo?.marketStats?.markPrice ?? lastPrice ?? "0",
lowPrice: this.marketInfo?.marketStats?.markPrice ?? lastPrice ?? "0",
volume: "0",
quoteVolume: "0",
bidPrice: this.lastDepth?.bids?.[0]?.[0],
askPrice: this.lastDepth?.asks?.[0]?.[0],
eventTime: Date.now(),
};
}
private estimateMarketPrice(side: "BUY" | "SELL", fallback?: number): number | undefined {
const bestBid = Number(this.lastDepth?.bids?.[0]?.[0]);
const bestAsk = Number(this.lastDepth?.asks?.[0]?.[0]);
if (side === "BUY" && Number.isFinite(bestAsk)) return bestAsk;
if (side === "SELL" && Number.isFinite(bestBid)) return bestBid;
if (Number.isFinite(fallback)) return fallback;
const last = Number(this.lastTrade?.p ?? this.marketInfo?.marketStats?.lastPrice);
return Number.isFinite(last) ? last : undefined;
}
private normalizeOrderType(type: string): AsterOrder["type"] {
const normalized = type.toUpperCase();
if (normalized === "LIMIT" || normalized === "MARKET") return normalized as any;
return "STOP_MARKET";
}
private logInfo(context: string, message: unknown): void {
console.info(`[ExtendedGateway] ${context}: ${message as string}`);
}
private logDebug(context: string, message: unknown): void {
if (process.env.EXTENDED_DEBUG === "1" || process.env.EXTENDED_DEBUG === "true") {
console.debug(`[ExtendedGateway] ${context}: ${message as string}`);
}
}
private logError(context: string, error: unknown): void {
this.logger(context, error);
if (process.env.EXTENDED_DEBUG === "1" || process.env.EXTENDED_DEBUG === "true") {
console.error(`[ExtendedGateway] ${context}`, error);
}
}
}
-27
View File
@@ -1,27 +0,0 @@
import BigNumber from "bignumber.js";
export const Decimal = BigNumber;
export type Decimal = BigNumber;
export type Long = BigNumber;
export type RoundingMode = BigNumber.RoundingMode;
export function toDecimal(value: string | number | BigNumber): Decimal {
return new Decimal(value);
}
export function toLong(value: string | number | BigNumber): Long {
return new Decimal(value);
}
export function roundToStep(value: Decimal, step: Decimal, mode: RoundingMode = Decimal.ROUND_DOWN): Decimal {
if (step.lte(0)) return value;
return value
.div(step)
.decimalPlaces(0, mode)
.times(step)
.decimalPlaces(step.decimalPlaces());
}
export function isValidNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}
-166
View File
@@ -1,166 +0,0 @@
import type { CreateOrderParams, OrderType } from "../types";
import { Decimal, roundToStep, toDecimal, toLong } from "./math";
import { calcStarknetExpiration, generateNonce, getStarkPublicKey, getStarknetOrderMsgHash, signMessageHash, toHexString } from "./signing";
import type { ExtendedFees, ExtendedMarket, ExtendedStarknetDomain } from "./types";
export interface ExtendedOrderContext {
market: ExtendedMarket;
fees: ExtendedFees;
domain: ExtendedStarknetDomain;
vaultId: string;
starkPrivateKey: string;
builderId?: string;
builderFeeRate?: string;
}
export interface BuiltOrderPayload {
payload: Record<string, unknown>;
orderId: string;
params: CreateOrderParams;
}
function resolveTimeInForce(input?: CreateOrderParams["timeInForce"]): "GTT" | "IOC" {
if (!input) return "GTT";
const normalized = input.toString().toUpperCase();
if (normalized === "IOC" || normalized === "FOK") return "IOC";
return "GTT";
}
function resolveTotalFeeRate(ctx: ExtendedOrderContext, postOnly?: boolean): Decimal {
const maker = toDecimal(ctx.fees.makerFeeRate);
const taker = toDecimal(ctx.fees.takerFeeRate);
const builderRate = ctx.builderFeeRate ? toDecimal(ctx.builderFeeRate) : null;
const base = postOnly ? maker : Decimal.max(maker, taker);
return builderRate ? base.plus(builderRate) : base;
}
function resolveOrderType(input: OrderType, stopPrice?: number | undefined): { type: "LIMIT" | "MARKET" | "CONDITIONAL"; triggerPrice?: Decimal } {
if (input === "MARKET") {
return { type: "MARKET" };
}
if (input === "STOP" || input === "STOP_MARKET" || input === "TAKE_PROFIT" || input === "TAKE_PROFIT_MARKET") {
const triggerPrice = stopPrice !== undefined ? toDecimal(stopPrice) : undefined;
return { type: "CONDITIONAL", triggerPrice };
}
return { type: "LIMIT" };
}
export class ExtendedOrderBuilder {
private readonly ctx: ExtendedOrderContext;
constructor(context: ExtendedOrderContext) {
this.ctx = context;
}
build(params: CreateOrderParams & { marketPrice?: number; now?: number }): BuiltOrderPayload {
const market = this.ctx.market;
const l2 = market.l2Config;
const trading = market.tradingConfig;
const isPostOnly = params.timeInForce === "GTX";
const timeInForce = resolveTimeInForce(isPostOnly ? "GTT" : params.timeInForce);
const now = params.now ?? Date.now();
const expiryEpochMillis = now + 60 * 60 * 1000;
const nonce = toLong(generateNonce());
const minPriceChange = toDecimal(trading.minPriceChange);
const minQtyChange = toDecimal(trading.minOrderSizeChange);
const qty = roundToStep(toDecimal(params.quantity ?? 0), minQtyChange, Decimal.ROUND_DOWN);
const orderType = resolveOrderType(params.type, params.stopPrice);
const basePrice = this.resolvePrice(params, orderType, params.marketPrice, minPriceChange);
const price = roundToStep(basePrice, minPriceChange, params.side === "BUY" ? Decimal.ROUND_UP : Decimal.ROUND_DOWN);
const totalFeeRate = resolveTotalFeeRate(this.ctx, isPostOnly);
const collateralAmount = qty.times(price);
const fee = totalFeeRate.times(collateralAmount);
const roundingMode = params.side === "BUY" ? Decimal.ROUND_UP : Decimal.ROUND_DOWN;
const collateralAmountStark = collateralAmount.times(l2.collateralResolution).integerValue(roundingMode);
const feeStark = fee.times(l2.collateralResolution).integerValue(Decimal.ROUND_UP);
const syntheticAmountStark = qty.times(l2.syntheticResolution).integerValue(roundingMode);
const expiration = calcStarknetExpiration(expiryEpochMillis).toString(10);
const starkPublicKey = toHexString(getStarkPublicKey(this.ctx.starkPrivateKey as any));
const starknetOrderHash = getStarknetOrderMsgHash({
positionId: toLong(this.ctx.vaultId).toString(10),
baseAssetIdHex: toHexString(l2.syntheticId),
baseAmount: syntheticAmountStark.toString(10),
quoteAssetIdHex: toHexString(l2.collateralId),
quoteAmount: collateralAmountStark.toString(10),
feeAssetIdHex: toHexString(l2.collateralId),
feeAmount: feeStark.toString(10),
expiration,
salt: nonce.toString(10),
starkPublicKey,
domain: this.ctx.domain,
});
const signature = signMessageHash(starknetOrderHash, this.ctx.starkPrivateKey as any);
const reduceOnly = params.reduceOnly === "true" || params.reduceOnly === true;
const payload: Record<string, unknown> = {
id: starknetOrderHash,
market: market.name,
type: orderType.type,
side: params.side,
qty: qty.toString(10),
price: price.toString(10),
timeInForce,
expiryEpochMillis,
fee: totalFeeRate.toString(10),
nonce: nonce.toString(10),
reduceOnly,
postOnly: isPostOnly,
settlement: {
signature: {
r: toHexString(signature.signature.r),
s: toHexString(signature.signature.s),
},
starkKey: toHexString(signature.starkKey),
collateralPosition: toLong(this.ctx.vaultId).toString(10),
},
};
if (orderType.type === "CONDITIONAL" && orderType.triggerPrice) {
const direction = params.side === "BUY" ? "UP" : "DOWN";
payload.trigger = {
triggerPrice: orderType.triggerPrice.toString(10),
triggerPriceType: params.triggerType ?? "LAST",
direction,
triggerPriceDirection: direction,
executionPriceType: params.type === "STOP_MARKET" || params.type === "TAKE_PROFIT_MARKET" ? "MARKET" : "LIMIT",
};
}
return {
payload,
orderId: starknetOrderHash,
params,
};
}
private resolvePrice(
params: CreateOrderParams,
orderType: { type: "LIMIT" | "MARKET" | "CONDITIONAL"; triggerPrice?: Decimal },
marketPrice: number | undefined,
minPriceChange: Decimal
): Decimal {
if (params.price !== undefined && params.price !== null) {
return toDecimal(params.price);
}
const fallbackPrice = Number.isFinite(marketPrice) ? marketPrice ?? NaN : NaN;
if (orderType.type === "MARKET") {
const basis = Number.isFinite(fallbackPrice) ? fallbackPrice! : minPriceChange.toNumber() || 1;
const factor = params.side === "BUY" ? 1.0075 : 0.9925;
return roundToStep(toDecimal(basis * factor), minPriceChange, params.side === "BUY" ? Decimal.ROUND_UP : Decimal.ROUND_DOWN);
}
if (orderType.type === "CONDITIONAL" && orderType.triggerPrice) {
return orderType.triggerPrice;
}
const base = Number.isFinite(fallbackPrice) ? fallbackPrice! : minPriceChange.toNumber() || 1;
return toDecimal(base);
}
}
-95
View File
@@ -1,95 +0,0 @@
import type { AsterOrder, CreateOrderParams } from "../types";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
if (params.quantity === undefined) {
params.quantity = intent.quantity;
}
if (params.timeInForce === undefined && intent.timeInForce) {
params.timeInForce = intent.timeInForce;
}
if (intent.reduceOnly !== undefined) {
params.reduceOnly = toStringBoolean(intent.reduceOnly);
}
if (intent.closePosition !== undefined) {
params.closePosition = toStringBoolean(intent.closePosition);
}
return params;
}
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "LIMIT",
quantity: intent.quantity,
price: intent.price,
timeInForce: intent.timeInForce ?? "GTC",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
price: intent.expectedPrice ?? undefined,
timeInForce: "IOC",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "STOP_MARKET",
quantity: intent.quantity,
stopPrice: intent.stopPrice,
price: intent.stopPrice,
timeInForce: intent.timeInForce ?? "GTC",
triggerType: intent.triggerType ?? (intent.side === "BUY" ? "TAKE_PROFIT" : "STOP_LOSS"),
closePosition: toStringBoolean(intent.closePosition ?? false),
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
throw new Error("Extended exchange does not support trailing stop orders");
}
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
reduceOnly: "true",
closePosition: toStringBoolean(intent.closePosition ?? true),
timeInForce: "IOC",
},
intent
);
return intent.adapter.createOrder(params);
}
-206
View File
@@ -1,206 +0,0 @@
import { readFileSync } from "fs";
import { createRequire } from "module";
import path from "path";
import wasmInit, {
get_order_msg as wasmGetOrderMsgHash,
sign_message as wasmSignMessage,
} from "@x10xchange/stark-crypto-wrapper-wasm";
import { ec as starkEc, hash as starkHash, selector as starkSelector, shortString as starkShortString } from "starknet";
import type { ExtendedStarknetDomain } from "./types";
export type HexString = `0x${string}`;
export function isHexString(value: string | undefined | null): value is HexString {
return typeof value === "string" && value.startsWith("0x");
}
export function toHexString(value: string): HexString {
return (value.startsWith("0x") ? value : `0x${value}`) as HexString;
}
export function fromHexString(value: HexString): string {
return value.slice(2);
}
export async function tryInitExtendedWasm(): Promise<void> {
try {
const require = createRequire(import.meta.url);
const wasmDir = path.dirname(require.resolve("@x10xchange/stark-crypto-wrapper-wasm"));
const wasmBuffer = readFileSync(path.join(wasmDir, "stark_crypto_wrapper_wasm_bg.wasm"));
await wasmInit({ module_or_path: wasmBuffer });
} catch (error) {
// Revert to JS implementation if wasm init fails; do not throw to keep bot running.
if (process.env.EXTENDED_DEBUG === "1" || process.env.EXTENDED_DEBUG === "true") {
console.warn("[ExtendedSigning] WASM init failed, falling back to JS:", error);
}
}
}
const STARKNET_SETTLEMENT_BUFFER_SECONDS = 14 * 24 * 60 * 60;
const MILLIS_IN_SECOND = 1000;
export function calcStarknetExpiration(expiryEpochMillis: number): number {
return Math.ceil(expiryEpochMillis / MILLIS_IN_SECOND) + STARKNET_SETTLEMENT_BUFFER_SECONDS;
}
export function getStarkPublicKey(privateKey: HexString): string {
return fromHexString(starkEc.starkCurve.getStarkKey(privateKey) as HexString);
}
export function signMessageHash(messageHash: string, starkPrivateKey: HexString): { signature: { r: string; s: string }; starkKey: string } {
const starkPublicKey = getStarkPublicKey(starkPrivateKey);
try {
const signature = wasmSignMessage(starkPrivateKey, messageHash);
const result = {
signature: {
r: fromHexString(signature.r as HexString),
s: fromHexString(signature.s as HexString),
},
starkKey: starkPublicKey,
};
if (typeof signature.free === "function") {
signature.free();
}
return result;
} catch {
const signature = starkEc.starkCurve.sign(messageHash, starkPrivateKey);
return {
signature: {
r: signature.r.toString(16),
s: signature.s.toString(16),
},
starkKey: starkPublicKey,
};
}
}
function jsGetObjMsgHash(domainHash: string, publicKey: string, objHash: string): string {
const messageFelt = starkShortString.encodeShortString("StarkNet Message");
return starkHash.computePoseidonHashOnElements([messageFelt, domainHash, publicKey, objHash]);
}
function jsGetStarknetDomainObjHash(domain: ExtendedStarknetDomain): string {
const selector = starkSelector.getSelector(
'"StarknetDomain"("name":"shortstring","version":"shortstring","chainId":"shortstring","revision":"shortstring")'
);
return starkHash.computePoseidonHashOnElements([
selector,
starkShortString.encodeShortString(domain.name),
starkShortString.encodeShortString(domain.version),
starkShortString.encodeShortString(domain.chainId),
domain.revision,
]);
}
function jsGetOrderMsgHash(
positionId: string,
baseAssetIdHex: string,
baseAmount: string,
quoteAssetIdHex: string,
quoteAmount: string,
feeAssetIdHex: string,
feeAmount: string,
expiration: string,
salt: string,
userPublicKeyHex: string,
domainName: string,
domainVersion: string,
domainChainId: string,
domainRevision: string
): string {
const domainHash = jsGetStarknetDomainObjHash({
name: domainName,
version: domainVersion,
chainId: domainChainId,
revision: parseInt(domainRevision, 10),
});
const orderSelector = starkSelector.getSelector(
'"Order"("position_id":"felt","base_asset_id":"AssetId","base_amount":"i64","quote_asset_id":"AssetId","quote_amount":"i64","fee_asset_id":"AssetId","fee_amount":"u64","expiration":"Timestamp","salt":"felt")"PositionId"("value":"u32")"AssetId"("value":"felt")"Timestamp"("seconds":"u64")'
);
const orderHash = starkHash.computePoseidonHashOnElements([
orderSelector,
positionId,
baseAssetIdHex,
baseAmount,
quoteAssetIdHex,
quoteAmount,
feeAssetIdHex,
feeAmount,
expiration,
salt,
]);
return jsGetObjMsgHash(domainHash, userPublicKeyHex, orderHash);
}
export function getStarknetOrderMsgHash(args: {
positionId: string;
baseAssetIdHex: string;
baseAmount: string;
quoteAssetIdHex: string;
quoteAmount: string;
feeAssetIdHex: string;
feeAmount: string;
expiration: string;
salt: string;
starkPublicKey: string;
domain: ExtendedStarknetDomain;
}): string {
const {
positionId,
baseAssetIdHex,
baseAmount,
quoteAssetIdHex,
quoteAmount,
feeAssetIdHex,
feeAmount,
expiration,
salt,
starkPublicKey,
domain,
} = args;
try {
const wasmHash = wasmGetOrderMsgHash(
positionId,
baseAssetIdHex,
baseAmount,
quoteAssetIdHex,
quoteAmount,
feeAssetIdHex,
feeAmount,
expiration,
salt,
starkPublicKey,
domain.name,
domain.version,
domain.chainId,
domain.revision.toString()
);
return fromHexString(wasmHash as HexString);
} catch (error) {
if (process.env.EXTENDED_DEBUG === "1" || process.env.EXTENDED_DEBUG === "true") {
console.warn("[ExtendedSigning] wasm order hash failed, using JS", error);
}
return jsGetOrderMsgHash(
positionId,
baseAssetIdHex,
baseAmount,
quoteAssetIdHex,
quoteAmount,
feeAssetIdHex,
feeAmount,
expiration,
salt,
starkPublicKey,
domain.name,
domain.version,
domain.chainId,
domain.revision.toString()
);
}
}
export function generateNonce(): number {
return Math.floor(Math.random() * (2 ** 31 - 1));
}
-152
View File
@@ -1,152 +0,0 @@
export interface ExtendedMarket {
name: string;
assetName?: string;
collateralAssetName?: string;
marketStats?: {
lastPrice?: string;
askPrice?: string;
bidPrice?: string;
markPrice?: string;
indexPrice?: string;
};
tradingConfig: {
minOrderSize: string;
minOrderSizeChange: string;
minPriceChange: string;
maxPositionValue: string;
};
l2Config: {
collateralId: string;
collateralResolution: number;
syntheticId: string;
syntheticResolution: number;
};
}
export interface ExtendedFees {
market: string;
makerFeeRate: string;
takerFeeRate: string;
builderFeeRate?: string;
}
export interface ExtendedBalance {
collateralName?: string;
balance: string;
equity?: string;
availableForTrade?: string;
availableForWithdrawal?: string;
unrealisedPnl?: string;
initialMargin?: string;
marginRatio?: string;
updatedTime?: number;
exposure?: string;
leverage?: string;
}
export interface ExtendedPosition {
id?: number | string;
accountId?: number | string;
market: string;
side: "LONG" | "SHORT";
leverage?: string;
size: string;
value?: string;
openPrice?: string;
markPrice?: string;
liquidationPrice?: string;
margin?: string;
unrealisedPnl?: string;
realisedPnl?: string;
tpTriggerPrice?: string;
tpLimitPrice?: string;
slTriggerPrice?: string;
slLimitPrice?: string;
adl?: number;
createdAt?: number;
updatedAt?: number;
}
export interface ExtendedTrigger {
triggerPrice?: string;
triggerPriceType?: "LAST" | "MARK" | "INDEX";
triggerPriceDirection?: "UP" | "DOWN";
direction?: "UP" | "DOWN";
executionPriceType?: "LIMIT" | "MARKET";
}
export interface ExtendedOrder {
id: string | number;
externalId?: string;
accountId?: string | number;
market: string;
type: "LIMIT" | "MARKET" | "CONDITIONAL" | "TPSL" | string;
side: "BUY" | "SELL";
status: string;
price?: string;
averagePrice?: string;
qty: string;
filledQty?: string;
payedFee?: string;
reduceOnly?: boolean;
postOnly?: boolean;
trigger?: ExtendedTrigger;
takeProfit?: {
triggerPrice?: string;
triggerPriceType?: ExtendedTrigger["triggerPriceType"];
price?: string;
priceType?: "LIMIT" | "MARKET";
};
stopLoss?: {
triggerPrice?: string;
triggerPriceType?: ExtendedTrigger["triggerPriceType"];
price?: string;
priceType?: "LIMIT" | "MARKET";
};
tpSlType?: "ORDER" | "POSITION";
createdTime: number;
updatedTime?: number;
expireTime?: number;
}
export interface ExtendedTrade {
m: string; // market
S: "BUY" | "SELL";
tT: string;
T: number;
p: string;
q: string;
i: number;
}
export interface ExtendedCandle {
T: number;
o: string;
h: string;
l: string;
c: string;
v?: string;
}
export interface ExtendedStarknetDomain {
name: string;
version: string;
chainId: string;
revision: number;
}
export interface ExtendedDepthLevel {
p: string;
q: string;
}
export interface ExtendedDepthMessage {
ts: number;
type?: "SNAPSHOT" | "DELTA";
data: {
m: string;
b?: ExtendedDepthLevel[];
a?: ExtendedDepthLevel[];
};
seq?: number;
}
+2 -1
View File
@@ -1201,6 +1201,7 @@ function toApiOrderPayload(order: GrvtSignedOrder): IOrder {
const metadata = order.metadata ? toApiOrderMetadata(order.metadata) : undefined;
return {
...order,
client_order_id: order.metadata?.client_order_id,
time_in_force: toApiTimeInForce(order.time_in_force),
metadata,
};
@@ -1300,7 +1301,7 @@ function buildUnsignedOrder(params: {
const trigger = buildTriggerMetadata(orderParams);
const metadata = {
client_order_id: generateClientOrderId(),
client_order_id: orderParams.clientOrderId ?? generateClientOrderId(),
...(trigger ? { trigger } : {}),
};
+4 -1
View File
@@ -22,6 +22,9 @@ function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent):
if (intent.closePosition !== undefined) {
params.closePosition = toStringBoolean(intent.closePosition);
}
if (intent.clientOrderId) {
params.clientOrderId = intent.clientOrderId;
}
return params;
}
@@ -34,6 +37,7 @@ export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterO
quantity: intent.quantity,
price: intent.price,
timeInForce: intent.timeInForce ?? "GTX",
clientOrderId: intent.clientOrderId,
},
intent
);
@@ -89,4 +93,3 @@ export async function createClosePositionOrder(intent: ClosePositionIntent): Pro
);
return intent.adapter.createOrder(params);
}
+3 -10
View File
@@ -13,9 +13,8 @@ import * as backpackOrders from "./backpack/order";
import * as grvtOrders from "./grvt/order";
import * as lighterOrders from "./lighter/order";
import * as paradexOrders from "./paradex/order";
import * as extendedOrders from "./extended/order";
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex" | "extended";
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex";
interface ExchangeOrderHandlers {
limit(intent: LimitOrderIntent): Promise<AsterOrder>;
@@ -61,16 +60,9 @@ const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
trailingStop: paradexOrders.createTrailingStopOrder,
close: paradexOrders.createClosePositionOrder,
},
extended: {
limit: extendedOrders.createLimitOrder,
market: extendedOrders.createMarketOrder,
stop: extendedOrders.createStopOrder,
trailingStop: extendedOrders.createTrailingStopOrder,
close: extendedOrders.createClosePositionOrder,
},
};
const knownExchanges: ExchangeKey[] = ["aster", "backpack", "grvt", "lighter", "paradex", "extended"];
const knownExchanges: ExchangeKey[] = ["aster", "backpack", "grvt", "lighter", "paradex"];
function normalizeExchangeId(value: string | undefined | null): string | undefined {
if (!value) return undefined;
@@ -123,3 +115,4 @@ export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise
export function routeCloseOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
return getHandlers(intent).close(intent);
}
+1 -1
View File
@@ -9,6 +9,7 @@ export interface BaseOrderIntent {
reduceOnly?: boolean;
closePosition?: boolean;
timeInForce?: TimeInForce | "GTX";
clientOrderId?: string;
}
export interface LimitOrderIntent extends BaseOrderIntent {
@@ -39,4 +40,3 @@ export function toStringBoolean(value: boolean | undefined): "true" | "false" |
if (value === undefined) return undefined;
return value ? "true" : "false";
}
+2 -2
View File
@@ -599,8 +599,8 @@ export class ParadexGateway {
try {
const isClosePosition = (extraParams as any).closePosition === true;
// Only omit amount for MARKET close-position orders; STOP requires explicit size
const shouldOmitAmount = isClosePosition && type === "market";
// Paradex requires explicit size for close-position MARKET orders; always send amount
const shouldOmitAmount = false;
const amountArg: any = shouldOmitAmount ? undefined : amount;
if (!shouldOmitAmount && amountArg != null && extraParams.size === undefined) {
extraParams.size = amountArg.toString();
-25
View File
@@ -4,7 +4,6 @@ import type { AsterCredentials } from "./aster-adapter";
import type { LighterCredentials } from "./lighter/adapter";
import type { BackpackCredentials } from "./backpack/adapter";
import type { ParadexCredentials } from "./paradex/adapter";
import type { ExtendedCredentials } from "./extended/adapter";
interface BuildAdapterOptions {
symbol: string;
@@ -35,11 +34,6 @@ export function buildAdapterFromEnv(options: BuildAdapterOptions): ExchangeAdapt
return createExchangeAdapter({ exchange: id, symbol, paradex: credentials });
}
if (id === "extended") {
const credentials = resolveExtendedCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, extended: credentials });
}
return createExchangeAdapter({ exchange: id, symbol, grvt: { symbol } });
}
@@ -120,25 +114,6 @@ function resolveParadexCredentials(): ParadexCredentials {
return credentials;
}
function resolveExtendedCredentials(symbol: string): ExtendedCredentials {
const apiKey = process.env.EXTENDED_API_KEY;
const starkPrivateKey = process.env.EXTENDED_STARK_PRIVATE_KEY;
const vaultId = process.env.EXTENDED_VAULT_ID;
if (!apiKey || !starkPrivateKey || !vaultId) {
throw new Error("缺少 EXTENDED_API_KEY / EXTENDED_STARK_PRIVATE_KEY / EXTENDED_VAULT_ID 环境变量");
}
return {
apiKey,
starkPrivateKey,
vaultId,
market: process.env.EXTENDED_MARKET ?? symbol,
apiHost: process.env.EXTENDED_API_HOST,
streamHost: process.env.EXTENDED_STREAM_HOST,
privateStreamHost: process.env.EXTENDED_PRIVATE_STREAM_HOST,
userAgent: process.env.EXTENDED_USER_AGENT,
};
}
function isHex32(value: string): boolean {
return /^0x[0-9a-fA-F]{64}$/.test(value.trim());
}
+1
View File
@@ -25,6 +25,7 @@ export interface CreateOrderParams {
reduceOnly?: StringBoolean;
closePosition?: StringBoolean;
triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
clientOrderId?: string;
}
export interface AsterAccountPosition {
-79
View File
@@ -1,79 +0,0 @@
import { promises as fs } from "fs";
import path from "path";
import type { GridDirection } from "../../config";
const DATA_DIR = process.env.GRID_DATA_DIR?.trim() || path.resolve("data");
const GRID_FILE = path.resolve(DATA_DIR, "grid-record.json");
export interface StoredGridState {
symbol: string;
lowerPrice: number;
upperPrice: number;
gridLevels: number;
orderSize: number;
maxPositionSize: number;
direction: GridDirection;
longExposure: Record<string, number>;
shortExposure: Record<string, number>;
updatedAt: number;
}
type GridStateMap = Record<string, StoredGridState>;
async function ensureDataDir(): Promise<void> {
try {
await fs.mkdir(DATA_DIR, { recursive: true });
} catch {
// ignore
}
}
async function readStateFile(): Promise<GridStateMap> {
try {
const content = await fs.readFile(GRID_FILE, "utf8");
const parsed = JSON.parse(content);
if (parsed && typeof parsed === "object") {
return parsed as GridStateMap;
}
return {};
} catch (error: any) {
if (error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
return {};
}
throw error;
}
}
export async function loadGridState(symbol: string): Promise<StoredGridState | null> {
const map = await readStateFile();
const snapshot = map[symbol];
return snapshot ?? null;
}
export async function saveGridState(snapshot: StoredGridState): Promise<void> {
await ensureDataDir();
const map = await readStateFile();
map[snapshot.symbol] = snapshot;
await fs.writeFile(GRID_FILE, JSON.stringify(map, null, 2), "utf8");
}
export async function clearGridState(symbol: string): Promise<void> {
const map = await readStateFile();
if (!Object.prototype.hasOwnProperty.call(map, symbol)) {
return;
}
delete map[symbol];
const entries = Object.keys(map);
if (!entries.length) {
try {
await fs.unlink(GRID_FILE);
} catch (error: any) {
if (!error || (error.code !== "ENOENT" && error.code !== "ENOTDIR")) {
throw error;
}
}
return;
}
await ensureDataDir();
await fs.writeFile(GRID_FILE, JSON.stringify(map, null, 2), "utf8");
}
+484 -1106
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -118,10 +118,10 @@ export function GridApp({ onExit }: GridAppProps) {
<Box flexDirection="column" marginBottom={1}>
<Text color="cyanBright">Grid Strategy Dashboard</Text>
<Text>
: {exchangeName} : {snapshot.symbol} : {snapshot.running ? "运行中" : "暂停"} : {snapshot.direction}
: {exchangeName} : {snapshot.symbol} : {snapshot.running ? "运行中" : "暂停"}
</Text>
<Text>
: {formatNumber(snapshot.lastPrice, 4)} : {formatNumber(snapshot.lowerPrice, 4)} : {formatNumber(snapshot.upperPrice, 4)} : {snapshot.gridLines.length}
: {formatNumber(snapshot.lastPrice, 4)} : {formatNumber(snapshot.centerPrice, 4)} : {formatNumber(snapshot.lowerPrice, 4)} : {formatNumber(snapshot.upperPrice, 4)} : {snapshot.gridLines.length}
</Text>
<Text color="gray">:
{feedEntries.map((entry, index) => (
@@ -139,10 +139,10 @@ export function GridApp({ onExit }: GridAppProps) {
<Box flexDirection="column" marginRight={4}>
<Text color="greenBright"></Text>
<Text>
: {formatNumber(gridConfig.orderSize, 6)} : {formatNumber(gridConfig.maxPositionSize, 6)}
: {formatNumber(gridConfig.tradeAmount, 6)} : {gridConfig.levelsPerSide} : {formatNumber(gridConfig.maxPositionSize, 6)}
</Text>
<Text>
: {(gridConfig.stopLossPct * 100).toFixed(2)}% : {(gridConfig.restartTriggerPct * 100).toFixed(2)}% : {gridConfig.autoRestart ? "启用" : "关闭"}
: {(gridConfig.spacingPct * 100).toFixed(3)}% : {(gridConfig.stopLossBufferPct * 100).toFixed(3)}%
</Text>
<Text>
: {gridConfig.refreshIntervalMs} ms