commit 667ede7ca8505e695e80d5188f254a59b166775e Author: discountry Date: Tue Sep 23 01:26:49 2025 +0800 init diff --git a/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc b/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc new file mode 100644 index 0000000..b8100b7 --- /dev/null +++ b/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc @@ -0,0 +1,111 @@ +--- +description: Use Bun instead of Node.js, npm, pnpm, or vite. +globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json" +alwaysApply: false +--- + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; + +// import .css files directly and it works +import './index.css'; + +import { createRoot } from "react-dom/client"; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b416e07 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,19 @@ +# Repository Guidelines + +## Project Structure & Module Organization +The Bun-based entry point (`index.ts`) lives at the root for quick smoke checks, alongside `tsconfig.json` and the Bun lockfile. The production-ready trading agents reside in `legacy/`, which is a pnpm-managed workspace. Inside `legacy/`, strategy scripts (`trendV2.ts`, `maker.ts`, `bot.ts`) and the CLI live at the top level, shared helpers are under `legacy/utils/`, and exchange adapters plus tests sit in `legacy/exchanges/`. Reference environment samples are provided in `legacy/env.example`, and longer-form docs are kept in `legacy/docs/`. + +## Build, Test, and Development Commands +Run `bun install` at the root to satisfy the lightweight Bun demo. Change into `legacy/` for real work: `pnpm install` bootstraps dependencies, `pnpm start` launches the default trend strategy, `pnpm maker` starts the market-making loop, `pnpm cli:start` triggers the dual-exchange hedging flow, and `pnpm test` executes the full Vitest suite. Use `pnpm aster:test` when you only need the Aster adapter checks. + +## Coding Style & Naming Conventions +All code is modern TypeScript using native ES modules. Follow the existing two-space indentation, keep imports sorted from external to local, and prefer `camelCase` for variables/functions with `PascalCase` for classes and enums. Strategy files stay in the project root with descriptive verbs (for example `maker.ts`), while shared utilities belong under `legacy/utils/`. Comments should stay concise and explain non-obvious trading logic. + +## Testing Guidelines +Vitest powers unit and integration checks. Place new suites beside their subjects using the `.test.ts` pattern (e.g., `legacy/exchanges/aster.test.ts`). Ensure strategies ship with coverage for order flow, risk guardrails, and websocket edge cases before opening a pull request. Run `pnpm test --watch` during development to keep feedback tight. + +## Commit & Pull Request Guidelines +The repository history is empty, so adopt a lightweight Conventional Commits style (for example `feat: add hedging status panel`) to keep future changelogs clear. Commits should stay scoped to one strategy or module. Pull requests need a concise summary, reproduction or validation notes (commands run, environments touched), and screenshots or log excerpts when behavior changes. Link tracking issues or tasks to help downstream coordination. + +## Environment & Secrets +Copy `legacy/env.example` to `.env` and populate API keys for Bitget and AsterDex before running live strategies. Never commit secrets; rely on local dotenv files or your deployment platform's secret manager. Rotate keys immediately if logs or configs leave the sandbox. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1fe211 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# ritmex-bot + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.21. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..084e18a --- /dev/null +++ b/bun.lock @@ -0,0 +1,318 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "ritmex-bot", + "dependencies": { + "ccxt": "^4.5.5", + "dotenv": "^17.2.2", + "ink": "^6.3.1", + "ink-table": "^3.1.0", + "react": "^19.1.1", + "ws": "^8.18.3", + }, + "devDependencies": { + "@types/bun": "latest", + "vitest": "^3.2.4", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@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=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.10", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.10", "", { "os": "android", "cpu": "arm64" }, "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.10", "", { "os": "android", "cpu": "x64" }, "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.10", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.10", "", { "os": "linux", "cpu": "arm" }, "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.10", "", { "os": "linux", "cpu": "ia32" }, "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.10", "", { "os": "none", "cpu": "x64" }, "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.10", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.10", "", { "os": "openbsd", "cpu": "x64" }, "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.10", "", { "os": "sunos", "cpu": "x64" }, "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.10", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.0", "", { "os": "android", "cpu": "arm64" }, "sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YCdWlY/8ltN6H78HnMsRHYlPiKvqKagBP1r+D7SSylxX+HnsgXGCmLiV3Y4nSyY9hW8qr8U9LDUx/Lo7M6MfmQ=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-z4nw6y1j+OOSGzuVbSWdIp1IUks9qNw4dc7z7lWuWDKojY38VMWBlEN7F9jk5UXOkUcp97vA1N213DF+Lz8BRg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Q/dv9Yvyr5rKlK8WQJZVrp5g2SOYeZUs9u/t2f9cQ2E0gJjYB/BWoedXfUT0EcDJefi2zzVfhcOj8drWCzTviw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kdBsLs4Uile/fbjZVvCRcKB4q64R+1mUq0Yd7oU1CMm1Av336ajIFqNFovByipciuUQjBCPMxwJhCgfG2re3rg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.0", "", { "os": "linux", "cpu": "arm" }, "sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.0", "", { "os": "linux", "cpu": "x64" }, "sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.0", "", { "os": "none", "cpu": "arm64" }, "sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YQugafP/rH0eOOHGjmNgDURrpYHrIX0yuojOI8bwCyXwxC9ZdTd3vYkmddPX0oHONLXu9Rb1dDmT0VNpjkzGGw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zYdUYhi3Qe2fndujBqL5FjAFzvNeLxtIqfzNEVKD1I7C37/chv1VxhscWSQHTNfjPCrBFQMnynwA3kpZpZ8w4A=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ=="], + + "@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=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@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=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + + "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + + "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + + "@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=="], + + "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=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], + + "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=="], + + "ccxt": ["ccxt@4.5.5", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-AyhwTFLkx4sO985ImIOfumEBox7AHD/iqk5tPGICObUSZG6wTXg0aRzU8Hjz974aCMG4msFwLk3A/iXPKAU4wA=="], + + "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=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], + + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], + + "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=="], + + "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], + + "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=="], + + "dotenv": ["dotenv@17.2.2", "", {}, "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q=="], + + "emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "es-toolkit": ["es-toolkit@1.39.10", "", {}, "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w=="], + + "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=="], + + "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + + "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-table": ["ink-table@3.1.0", "", { "dependencies": { "object-hash": "^2.0.3" }, "peerDependencies": { "ink": ">=3.0.0", "react": ">=16.8.0" } }, "sha512-qxVb4DIaEaJryvF9uZGydnmP9Hkmas3DCKVpEcBYC0E4eJd3qNgNe+PZKuzgCERFe9LfAS1TNWxCr9+AU4v3YA=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], + + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "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=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="], + + "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], + + "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=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "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=="], + + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + + "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=="], + + "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=="], + + "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], + + "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=="], + + "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=="], + + "cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + } +} diff --git a/docs/v2-api.md b/docs/v2-api.md new file mode 100644 index 0000000..c43ded4 --- /dev/null +++ b/docs/v2-api.md @@ -0,0 +1,4540 @@ +- [General Info](#general-info) + - [General API Information](#general-api-information) + - [HTTP Return Codes](#http-return-codes) + - [Error Codes and Messages](#error-codes-and-messages) + - [General Information on Endpoints](#general-information-on-endpoints) + - [LIMITS](#limits) + - [IP Limits](#ip-limits) + - [Order Rate Limits](#order-rate-limits) + - [Endpoint Security Type](#endpoint-security-type) + - [SIGNED (TRADE and USER_DATA) Endpoint Security](#signed-trade-and-user_data-endpoint-security) + - [Timing Security](#timing-security) + - [SIGNED Endpoint Examples for POST /fapi/v1/order](#signed-endpoint-examples-for-post-fapiv1order) + - [Example 1: As a query string](#example-1-as-a-query-string) + - [Example 2: As a request body](#example-2-as-a-request-body) + - [Example 3: Mixed query string and request body](#example-3-mixed-query-string-and-request-body) + - [Public Endpoints Info](#public-endpoints-info) + - [Terminology](#terminology) + - [ENUM definitions](#enum-definitions) + - [Filters](#filters) + - [Symbol filters](#symbol-filters) + - [PRICE_FILTER](#price_filter) + - [LOT_SIZE](#lot_size) + - [MARKET_LOT_SIZE](#market_lot_size) + - [MAX_NUM_ORDERS](#max_num_orders) + - [MAX_NUM_ALGO_ORDERS](#max_num_algo_orders) + - [PERCENT_PRICE](#percent_price) + - [MIN_NOTIONAL](#min_notional) +- [Market Data Endpoints](#market-data-endpoints) + - [Test Connectivity](#test-connectivity) + - [Check Server Time](#check-server-time) + - [Exchange Information](#exchange-information) + - [Order Book](#order-book) + - [Recent Trades List](#recent-trades-list) + - [Old Trades Lookup (MARKET_DATA)](#old-trades-lookup-market_data) + - [Compressed/Aggregate Trades List](#compressedaggregate-trades-list) + - [Kline/Candlestick Data](#klinecandlestick-data) + - [Index Price Kline/Candlestick Data](#index-price-klinecandlestick-data) + - [Mark Price Kline/Candlestick Data](#mark-price-klinecandlestick-data) + - [Mark Price](#mark-price) + - [Get Funding Rate History](#get-funding-rate-history) + - [Get Funding Rate Config](#get-funding-rate-config) + - [24hr Ticker Price Change Statistics](#24hr-ticker-price-change-statistics) + - [Symbol Price Ticker](#symbol-price-ticker) + - [Symbol Order Book Ticker](#symbol-order-book-ticker) +- [Websocket Market Streams](#websocket-market-streams) + - [Live Subscribing/Unsubscribing to streams](#live-subscribingunsubscribing-to-streams) + - [Subscribe to a stream](#subscribe-to-a-stream) + - [Unsubscribe to a stream](#unsubscribe-to-a-stream) + - [Listing Subscriptions](#listing-subscriptions) + - [Setting Properties](#setting-properties) + - [Retrieving Properties](#retrieving-properties) + - [Error Messages](#error-messages) + - [Aggregate Trade Streams](#aggregate-trade-streams) + - [Mark Price Stream](#mark-price-stream) + - [Mark Price Stream for All market](#mark-price-stream-for-all-market) + - [Kline/Candlestick Streams](#klinecandlestick-streams) + - [Individual Symbol Mini Ticker Stream](#individual-symbol-mini-ticker-stream) + - [All Market Mini Tickers Stream](#all-market-mini-tickers-stream) + - [Individual Symbol Ticker Streams](#individual-symbol-ticker-streams) + - [All Market Tickers Streams](#all-market-tickers-streams) + - [Individual Symbol Book Ticker Streams](#individual-symbol-book-ticker-streams) + - [All Book Tickers Stream](#all-book-tickers-stream) + - [Liquidation Order Streams](#liquidation-order-streams) + - [All Market Liquidation Order Streams](#all-market-liquidation-order-streams) + - [Partial Book Depth Streams](#partial-book-depth-streams) + - [Diff. Book Depth Streams](#diff-book-depth-streams) + - [How to manage a local order book correctly](#how-to-manage-a-local-order-book-correctly) +- [Account/Trades Endpoints](#accounttrades-endpoints) + - [Change Position Mode(TRADE)](#change-position-modetrade) + - [Get Current Position Mode(USER_DATA)](#get-current-position-modeuser_data) + - [Change Multi-Assets Mode (TRADE)](#change-multi-assets-mode-trade) + - [Get Current Multi-Assets Mode (USER_DATA)](#get-current-multi-assets-mode-user_data) + - [New Order (TRADE)](#new-order--trade) + - [Place Multiple Orders (TRADE)](#place-multiple-orders--trade) + - [Transfer Between Futures And Spot (USER_DATA)](#transfer-between-futures-and-spot-user_data) + - [Query Order (USER_DATA)](#query-order-user_data) + - [Cancel Order (TRADE)](#cancel-order-trade) + - [Cancel All Open Orders (TRADE)](#cancel-all-open-orders-trade) + - [Cancel Multiple Orders (TRADE)](#cancel-multiple-orders-trade) + - [Auto-Cancel All Open Orders (TRADE)](#auto-cancel-all-open-orders-trade) + - [Query Current Open Order (USER_DATA)](#query-current-open-order-user_data) + - [Current All Open Orders (USER_DATA)](#current-all-open-orders-user_data) + - [All Orders (USER_DATA)](#all-orders-user_data) + - [Futures Account Balance V2 (USER_DATA)](#futures-account-balance-v2-user_data) + - [Account Information V2 (USER_DATA)](#account-information-v2-user_data) + - [Change Initial Leverage (TRADE)](#change-initial-leverage-trade) + - [Change Margin Type (TRADE)](#change-margin-type-trade) + - [Modify Isolated Position Margin (TRADE)](#modify-isolated-position-margin-trade) + - [Get Position Margin Change History (TRADE)](#get-position-margin-change-history-trade) + - [Position Information V2 (USER_DATA)](#position-information-v2-user_data) + - [Account Trade List (USER_DATA)](#account-trade-list-user_data) + - [Get Income History(USER_DATA)](#get-income-historyuser_data) + - [Notional and Leverage Brackets (USER_DATA)](#notional-and-leverage-brackets-user_data) + - [Position ADL Quantile Estimation (USER_DATA)](#position-adl-quantile-estimation-user_data) + - [User's Force Orders (USER_DATA)](#users-force-orders-user_data) + - [User Commission Rate (USER_DATA)](#user-commission-rate-user_data) +- [User Data Streams](#user-data-streams) + - [Start User Data Stream (USER_STREAM)](#start-user-data-stream-user_stream) + - [Keepalive User Data Stream (USER_STREAM)](#keepalive-user-data-stream-user_stream) + - [Close User Data Stream (USER_STREAM)](#close-user-data-stream-user_stream) + - [Event: User Data Stream Expired](#event-user-data-stream-expired) + - [Event: Margin Call](#event-margin-call) + - [Event: Balance and Position Update](#event-balance-and-position-update) + - [Event: Order Update](#event-order-update) + - [Event: Account Configuration Update previous Leverage Update](#event-account-configuration-update-previous-leverage-update) +- [Error Codes](#error-codes) + - [10xx - General Server or Network issues](#10xx---general-server-or-network-issues) + - [11xx - Request issues](#11xx---request-issues) + - [20xx - Processing Issues](#20xx---processing-issues) + - [40xx - Filters and other Issues](#40xx---filters-and-other-issues) + +# General Info + +## General API Information + +* Some endpoints will require an API Key. Please refer to [this page](https://www.asterdex.com/) +* The base endpoint is: **https://fapi.asterdex.com** +* All endpoints return either a JSON object or array. +* Data is returned in **ascending** order. Oldest first, newest last. +* All time and timestamp related fields are in milliseconds. +* All data types adopt definition in JAVA. + +### HTTP Return Codes + +* HTTP `4XX` return codes are used for for malformed requests; + the issue is on the sender's side. +* HTTP `403` return code is used when the WAF Limit (Web Application Firewall) has been violated. +* HTTP `429` return code is used when breaking a request rate limit. +* HTTP `418` return code is used when an IP has been auto-banned for continuing to send requests after receiving `429` codes. +* HTTP `5XX` return codes are used for internal errors; the issue is on + Aster's side. +* HTTP `503` return code is used when the API successfully sent the message but not get a response within the timeout period. + It is important to **NOT** treat this as a failure operation; the execution status is + **UNKNOWN** and could have been a success. + +### Error Codes and Messages + +* Any endpoint can return an ERROR + +> ***The error payload is as follows:*** + +```javascript +{ + "code": -1121, + "msg": "Invalid symbol." +} +``` + +* Specific error codes and messages defined in [Error Codes](#error-codes). + +### General Information on Endpoints + +* For `GET` endpoints, parameters must be sent as a `query string`. +* For `POST`, `PUT`, and `DELETE` endpoints, the parameters may be sent as a + `query string` or in the `request body` with content type + `application/x-www-form-urlencoded`. You may mix parameters between both the + `query string` and `request body` if you wish to do so. +* Parameters may be sent in any order. +* If a parameter sent in both the `query string` and `request body`, the + `query string` parameter will be used. + +## LIMITS +* The `/fapi/v1/exchangeInfo` `rateLimits` array contains objects related to the exchange's `RAW_REQUEST`, `REQUEST_WEIGHT`, and `ORDER` rate limits. These are further defined in the `ENUM definitions` section under `Rate limiters (rateLimitType)`. +* A `429` will be returned when either rate limit is violated. + + + +### IP Limits +* Every request will contain `X-MBX-USED-WEIGHT-(intervalNum)(intervalLetter)` in the response headers which has the current used weight for the IP for all request rate limiters defined. +* Each route has a `weight` which determines for the number of requests each endpoint counts for. Heavier endpoints and endpoints that do operations on multiple symbols will have a heavier `weight`. +* When a 429 is received, it's your obligation as an API to back off and not spam the API. +* **Repeatedly violating rate limits and/or failing to back off after receiving 429s will result in an automated IP ban (HTTP status 418).** +* IP bans are tracked and **scale in duration** for repeat offenders, **from 2 minutes to 3 days**. +* **The limits on the API are based on the IPs, not the API keys.** + + + +### Order Rate Limits +* Every order response will contain a `X-MBX-ORDER-COUNT-(intervalNum)(intervalLetter)` header which has the current order count for the account for all order rate limiters defined. +* Rejected/unsuccessful orders are not guaranteed to have `X-MBX-ORDER-COUNT-**` headers in the response. +* **The order rate limit is counted against each account**. + +## Endpoint Security Type +* Each endpoint has a security type that determines the how you will + interact with it. +* API-keys are passed into the Rest API via the `X-MBX-APIKEY` + header. +* API-keys and secret-keys **are case sensitive**. +* API-keys can be configured to only access certain types of secure endpoints. + For example, one API-key could be used for TRADE only, while another API-key + can access everything except for TRADE routes. +* By default, API-keys can access all secure routes. + +Security Type | Description +------------ | ------------ +NONE | Endpoint can be accessed freely. +TRADE | Endpoint requires sending a valid API-Key and signature. +USER_DATA | Endpoint requires sending a valid API-Key and signature. +USER_STREAM | Endpoint requires sending a valid API-Key. +MARKET_DATA | Endpoint requires sending a valid API-Key. + + +* `TRADE` and `USER_DATA` endpoints are `SIGNED` endpoints. + +## SIGNED (TRADE and USER_DATA) Endpoint Security +* `SIGNED` endpoints require an additional parameter, `signature`, to be + sent in the `query string` or `request body`. +* Endpoints use `HMAC SHA256` signatures. The `HMAC SHA256 signature` is a keyed `HMAC SHA256` operation. + Use your `secretKey` as the key and `totalParams` as the value for the HMAC operation. +* The `signature` is **not case sensitive**. +* Please make sure the `signature` is the end part of your `query string` or `request body`. +* `totalParams` is defined as the `query string` concatenated with the + `request body`. + +### Timing Security +* A `SIGNED` endpoint also requires a parameter, `timestamp`, to be sent which + should be the millisecond timestamp of when the request was created and sent. +* An additional parameter, `recvWindow`, may be sent to specify the number of + milliseconds after `timestamp` the request is valid for. If `recvWindow` + is not sent, **it defaults to 5000**. + +> The logic is as follows: + +```javascript + if (timestamp < (serverTime + 1000) && (serverTime - timestamp) <= recvWindow){ + // process request + } + else { + // reject request + } +``` + +**Serious trading is about timing.** Networks can be unstable and unreliable, +which can lead to requests taking varying amounts of time to reach the +servers. With `recvWindow`, you can specify that the request must be +processed within a certain number of milliseconds or be rejected by the +server. + + + +### SIGNED Endpoint Examples for POST /fapi/v1/order +Here is a step-by-step example of how to send a vaild signed payload from the +Linux command line using `echo`, `openssl`, and `curl`. + +Key | Value +------------ | ------------ +apiKey | dbefbc809e3e83c283a984c3a1459732ea7db1360ca80c5c2c8867408d28cc83 +secretKey | 2b5eb11e18796d12d88f13dc27dbbd02c2cc51ff7059765ed9821957d82bb4d9 + + +Parameter | Value +------------ | ------------ +symbol | BTCUSDT +side | BUY +type | LIMIT +timeInForce | GTC +quantity | 1 +price | 9000 +recvWindow | 5000 +timestamp | 1591702613943 + + +#### Example 1: As a query string + +> **Example 1** + +> **HMAC SHA256 signature:** + +```shell + $ echo -n "symbol=BTCUSDT&side=BUY&type=LIMIT&quantity=1&price=9000&timeInForce=GTC&recvWindow=5000×tamp=1591702613943" | openssl dgst -sha256 -hmac "2b5eb11e18796d12d88f13dc27dbbd02c2cc51ff7059765ed9821957d82bb4d9" + (stdin)= 3c661234138461fcc7a7d8746c6558c9842d4e10870d2ecbedf7777cad694af9 +``` +> **curl command:** + +```shell + (HMAC SHA256) + $ curl -H "X-MBX-APIKEY: dbefbc809e3e83c283a984c3a1459732ea7db1360ca80c5c2c8867408d28cc83" -X POST 'https://fapi/asterdex.com/fapi/v1/order?symbol=BTCUSDT&side=BUY&type=LIMIT&quantity=1&price=9000&timeInForce=GTC&recvWindow=5000×tamp=1591702613943&signature= 3c661234138461fcc7a7d8746c6558c9842d4e10870d2ecbedf7777cad694af9' +``` +* **queryString:** + + symbol=BTCUSDT + &side=BUY + &type=LIMIT + &timeInForce=GTC + &quantity=1 + &price=9000 + &recvWindow=5000 + ×tamp=1591702613943 + + + + +#### Example 2: As a request body + +> **Example 2** + +> **HMAC SHA256 signature:** + +```shell + $ echo -n "symbol=BTCUSDT&side=BUY&type=LIMIT&quantity=1&price=9000&timeInForce=GTC&recvWindow=5000×tamp=1591702613943" | openssl dgst -sha256 -hmac "2b5eb11e18796d12d88f13dc27dbbd02c2cc51ff7059765ed9821957d82bb4d9" + (stdin)= 3c661234138461fcc7a7d8746c6558c9842d4e10870d2ecbedf7777cad694af9 +``` + + +> **curl command:** + +```shell + (HMAC SHA256) + $ curl -H "X-MBX-APIKEY: dbefbc809e3e83c283a984c3a1459732ea7db1360ca80c5c2c8867408d28cc83" -X POST 'https://fapi/asterdex.com/fapi/v1/order' -d 'symbol=BTCUSDT&side=BUY&type=LIMIT&quantity=1&price=9000&timeInForce=GTC&recvWindow=5000×tamp=1591702613943&signature= 3c661234138461fcc7a7d8746c6558c9842d4e10870d2ecbedf7777cad694af9' +``` + +* **requestBody:** + + symbol=BTCUSDT + &side=BUY + &type=LIMIT + &timeInForce=GTC + &quantity=1 + &price=9000 + &recvWindow=5000 + ×tamp=1591702613943 + + + +#### Example 3: Mixed query string and request body + +> **Example 3** + +> **HMAC SHA256 signature:** + +```shell + $ echo -n "symbol=BTCUSDT&side=BUY&type=LIMIT&quantity=1&price=9000&timeInForce=GTC&recvWindow=5000×tamp=1591702613943" | openssl dgst -sha256 -hmac "2b5eb11e18796d12d88f13dc27dbbd02c2cc51ff7059765ed9821957d82bb4d9" + (stdin)= 3c661234138461fcc7a7d8746c6558c9842d4e10870d2ecbedf7777cad694af9 +``` + +> **curl command:** + +```shell + (HMAC SHA256) + $ curl -H "X-MBX-APIKEY: dbefbc809e3e83c283a984c3a1459732ea7db1360ca80c5c2c8867408d28cc83" -X POST 'https://fapi/asterdex.com/fapi/v1/order?symbol=BTCUSDT&side=BUY&type=LIMIT&timeInForce=GTC' -d 'quantity=1&price=9000&recvWindow=5000×tamp=1591702613943&signature=3c661234138461fcc7a7d8746c6558c9842d4e10870d2ecbedf7777cad694af9' +``` + +* **queryString:** symbol=BTCUSDT&side=BUY&type=LIMIT&timeInForce=GTC +* **requestBody:** quantity=1&price=9000&recvWindow=5000×tamp= 1591702613943 + + +Note that the signature is different in example 3. +There is no & between "GTC" and "quantity=1". + + +## Public Endpoints Info +### Terminology +* `base asset` refers to the asset that is the `quantity` of a symbol. +* `quote asset` refers to the asset that is the `price` of a symbol. + + +### ENUM definitions + +**Symbol type:** + +* FUTURE + +**Contract type (contractType):** + +* PERPETUAL + +**Contract status(contractStatus,status):** + +* PENDING_TRADING +* TRADING +* PRE_SETTLE +* SETTLING +* CLOSE + + +**Order status (status):** + +* NEW +* PARTIALLY_FILLED +* FILLED +* CANCELED +* REJECTED +* EXPIRED + +**Order types (orderTypes, type):** + +* LIMIT +* MARKET +* STOP +* STOP_MARKET +* TAKE_PROFIT +* TAKE_PROFIT_MARKET +* TRAILING_STOP_MARKET + +**Order side (side):** + +* BUY +* SELL + +**Position side (positionSide):** + +* BOTH +* LONG +* SHORT + +**Time in force (timeInForce):** + +* GTC - Good Till Cancel +* IOC - Immediate or Cancel +* FOK - Fill or Kill +* GTX - Good Till Crossing (Post Only) +* HIDDEN - HIDDEN This type of order is not visible in the order book + +**Working Type (workingType)** + +* MARK_PRICE +* CONTRACT_PRICE + +**Response Type (newOrderRespType)** + +* ACK +* RESULT + + +**Kline/Candlestick chart intervals:** + +m -> minutes; h -> hours; d -> days; w -> weeks; M -> months + +* 1m +* 3m +* 5m +* 15m +* 30m +* 1h +* 2h +* 4h +* 6h +* 8h +* 12h +* 1d +* 3d +* 1w +* 1M + +**Rate limiters (rateLimitType)** + +> REQUEST_WEIGHT + +```javascript + { + "rateLimitType": "REQUEST_WEIGHT", + "interval": "MINUTE", + "intervalNum": 1, + "limit": 2400 + } +``` + +> ORDERS + +```javascript + { + "rateLimitType": "ORDERS", + "interval": "MINUTE", + "intervalNum": 1, + "limit": 1200 + } +``` + + +* REQUEST_WEIGHT + +* ORDERS + + +**Rate limit intervals (interval)** + +* MINUTE + + + + +## Filters +Filters define trading rules on a symbol or an exchange. + +### Symbol filters +#### PRICE_FILTER + +> **/exchangeInfo format:** + +```javascript + { + "filterType": "PRICE_FILTER", + "minPrice": "0.00000100", + "maxPrice": "100000.00000000", + "tickSize": "0.00000100" + } +``` + +The `PRICE_FILTER` defines the `price` rules for a symbol. There are 3 parts: + +* `minPrice` defines the minimum `price`/`stopPrice` allowed; disabled on `minPrice` == 0. +* `maxPrice` defines the maximum `price`/`stopPrice` allowed; disabled on `maxPrice` == 0. +* `tickSize` defines the intervals that a `price`/`stopPrice` can be increased/decreased by; disabled on `tickSize` == 0. + +Any of the above variables can be set to 0, which disables that rule in the `price filter`. In order to pass the `price filter`, the following must be true for `price`/`stopPrice` of the enabled rules: + +* `price` >= `minPrice` +* `price` <= `maxPrice` +* (`price`-`minPrice`) % `tickSize` == 0 + + +#### LOT_SIZE + +> **/exchangeInfo format:** + +```javascript + { + "filterType": "LOT_SIZE", + "minQty": "0.00100000", + "maxQty": "100000.00000000", + "stepSize": "0.00100000" + } +``` + +The `LOT_SIZE` filter defines the `quantity` (aka "lots" in auction terms) rules for a symbol. There are 3 parts: + +* `minQty` defines the minimum `quantity` allowed. +* `maxQty` defines the maximum `quantity` allowed. +* `stepSize` defines the intervals that a `quantity` can be increased/decreased by. + +In order to pass the `lot size`, the following must be true for `quantity`: + +* `quantity` >= `minQty` +* `quantity` <= `maxQty` +* (`quantity`-`minQty`) % `stepSize` == 0 + + + +#### MARKET_LOT_SIZE + + +> **/exchangeInfo format:** + +```javascript + { + "filterType": "MARKET_LOT_SIZE", + "minQty": "0.00100000", + "maxQty": "100000.00000000", + "stepSize": "0.00100000" + } +``` + +The `MARKET_LOT_SIZE` filter defines the `quantity` (aka "lots" in auction terms) rules for `MARKET` orders on a symbol. There are 3 parts: + +* `minQty` defines the minimum `quantity` allowed. +* `maxQty` defines the maximum `quantity` allowed. +* `stepSize` defines the intervals that a `quantity` can be increased/decreased by. + +In order to pass the `market lot size`, the following must be true for `quantity`: + +* `quantity` >= `minQty` +* `quantity` <= `maxQty` +* (`quantity`-`minQty`) % `stepSize` == 0 + + +#### MAX_NUM_ORDERS + +> **/exchangeInfo format:** + +```javascript + { + "filterType": "MAX_NUM_ORDERS", + "limit": 200 + } +``` + +The `MAX_NUM_ORDERS` filter defines the maximum number of orders an account is allowed to have open on a symbol. + +Note that both "algo" orders and normal orders are counted for this filter. + + +#### MAX_NUM_ALGO_ORDERS + +> **/exchangeInfo format:** + +```javascript + { + "filterType": "MAX_NUM_ALGO_ORDERS", + "limit": 100 + } +``` + +The `MAX_NUM_ALGO_ORDERS ` filter defines the maximum number of all kinds of algo orders an account is allowed to have open on a symbol. + +The algo orders include `STOP`, `STOP_MARKET`, `TAKE_PROFIT`, `TAKE_PROFIT_MARKET`, and `TRAILING_STOP_MARKET` orders. + + +#### PERCENT_PRICE + +> **/exchangeInfo format:** + +```javascript + { + "filterType": "PERCENT_PRICE", + "multiplierUp": "1.1500", + "multiplierDown": "0.8500", + "multiplierDecimal": 4 + } +``` + +The `PERCENT_PRICE` filter defines valid range for a price based on the mark price. + +In order to pass the `percent price`, the following must be true for `price`: + +* BUY: `price` <= `markPrice` * `multiplierUp` +* SELL: `price` >= `markPrice` * `multiplierDown` + + +#### MIN_NOTIONAL + +> **/exchangeInfo format:** + +```javascript + { + "filterType": "MIN_NOTIONAL", + "notional": "1" + } +``` + +The `MIN_NOTIONAL` filter defines the minimum notional value allowed for an order on a symbol. +An order's notional value is the `price` * `quantity`. +Since `MARKET` orders have no price, the mark price is used. + + + +--- + +# Market Data Endpoints + +## Test Connectivity + + +> **Response:** + +```javascript +{} +``` + + +`` +GET /fapi/v1/ping +`` + +Test connectivity to the Rest API. + +**Weight:** +1 + +**Parameters:** +NONE + + + +## Check Server Time + +> **Response:** + +```javascript +{ + "serverTime": 1499827319559 +} +``` + +`` +GET /fapi/v1/time +`` + +Test connectivity to the Rest API and get the current server time. + +**Weight:** +1 + +**Parameters:** +NONE + + +## Exchange Information + +> **Response:** + +```javascript +{ + "exchangeFilters": [], + "rateLimits": [ + { + "interval": "MINUTE", + "intervalNum": 1, + "limit": 2400, + "rateLimitType": "REQUEST_WEIGHT" + }, + { + "interval": "MINUTE", + "intervalNum": 1, + "limit": 1200, + "rateLimitType": "ORDERS" + } + ], + "serverTime": 1565613908500, // Ignore please. If you want to check current server time, please check via "GET /fapi/v1/time" + "assets": [ // assets information + { + "asset": "BUSD", + "marginAvailable": true, // whether the asset can be used as margin in Multi-Assets mode + "autoAssetExchange": 0 // auto-exchange threshold in Multi-Assets margin mode + }, + { + "asset": "USDT", + "marginAvailable": true, + "autoAssetExchange": 0 + }, + { + "asset": "BTC", + "marginAvailable": false, + "autoAssetExchange": null + } + ], + "symbols": [ + { + "symbol": "DOGEUSDT", + "pair": "DOGEUSDT", + "contractType": "PERPETUAL", + "deliveryDate": 4133404800000, + "onboardDate": 1598252400000, + "status": "TRADING", + "maintMarginPercent": "2.5000", // ignore + "requiredMarginPercent": "5.0000", // ignore + "baseAsset": "BLZ", + "quoteAsset": "USDT", + "marginAsset": "USDT", + "pricePrecision": 5, // please do not use it as tickSize + "quantityPrecision": 0, // please do not use it as stepSize + "baseAssetPrecision": 8, + "quotePrecision": 8, + "underlyingType": "COIN", + "underlyingSubType": ["STORAGE"], + "settlePlan": 0, + "triggerProtect": "0.15", // threshold for algo order with "priceProtect" + "filters": [ + { + "filterType": "PRICE_FILTER", + "maxPrice": "300", + "minPrice": "0.0001", + "tickSize": "0.0001" + }, + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "1", + "stepSize": "1" + }, + { + "filterType": "MARKET_LOT_SIZE", + "maxQty": "590119", + "minQty": "1", + "stepSize": "1" + }, + { + "filterType": "MAX_NUM_ORDERS", + "limit": 200 + }, + { + "filterType": "MAX_NUM_ALGO_ORDERS", + "limit": 100 + }, + { + "filterType": "MIN_NOTIONAL", + "notional": "1", + }, + { + "filterType": "PERCENT_PRICE", + "multiplierUp": "1.1500", + "multiplierDown": "0.8500", + "multiplierDecimal": 4 + } + ], + "OrderType": [ + "LIMIT", + "MARKET", + "STOP", + "STOP_MARKET", + "TAKE_PROFIT", + "TAKE_PROFIT_MARKET", + "TRAILING_STOP_MARKET" + ], + "timeInForce": [ + "GTC", + "IOC", + "FOK", + "GTX", + "HIDDEN" + ], + "liquidationFee": "0.010000", // liquidation fee rate + "marketTakeBound": "0.30", // the max price difference rate( from mark price) a market order can make + } + ], + "timezone": "UTC" +} + +``` + +`` +GET /fapi/v1/exchangeInfo +`` + +Current exchange trading rules and symbol information + +**Weight:** +1 + +**Parameters:** +NONE + + + + +## Order Book + + +> **Response:** + +```javascript +{ + "lastUpdateId": 1027024, + "E": 1589436922972, // Message output time + "T": 1589436922959, // Transaction time + "bids": [ + [ + "4.00000000", // PRICE + "431.00000000" // QTY + ] + ], + "asks": [ + [ + "4.00000200", + "12.00000000" + ] + ] +} +``` + +`` +GET /fapi/v1/depth +`` + +**Weight:** + +Adjusted based on the limit: + + +Limit | Weight +------------ | ------------ +5, 10, 20, 50 | 2 +100 | 5 +500 | 10 +1000 | 20 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +limit | INT | NO | Default 500; Valid limits:[5, 10, 20, 50, 100, 500, 1000] + + + + +## Recent Trades List + +> **Response:** + +```javascript +[ + { + "id": 28457, + "price": "4.00000100", + "qty": "12.00000000", + "quoteQty": "48.00", + "time": 1499865549590, + "isBuyerMaker": true, + } +] +``` + +`` +GET /fapi/v1/trades +`` + +Get recent market trades + +**Weight:** +1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +limit | INT | NO | Default 500; max 1000. + +* Market trades means trades filled in the order book. Only market trades will be returned, which means the insurance fund trades and ADL trades won't be returned. + + +## Old Trades Lookup (MARKET_DATA) + +> **Response:** + +```javascript +[ + { + "id": 28457, + "price": "4.00000100", + "qty": "12.00000000", + "quoteQty": "8000.00", + "time": 1499865549590, + "isBuyerMaker": true, + } +] +``` + +`` +GET /fapi/v1/historicalTrades +`` + +Get older market historical trades. + +**Weight:** +20 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +limit | INT | NO | Default 500; max 1000. +fromId | LONG | NO | TradeId to fetch from. Default gets most recent trades. + +* Market trades means trades filled in the order book. Only market trades will be returned, which means the insurance fund trades and ADL trades won't be returned. + + +## Compressed/Aggregate Trades List + +> **Response:** + +```javascript +[ + { + "a": 26129, // Aggregate tradeId + "p": "0.01633102", // Price + "q": "4.70443515", // Quantity + "f": 27781, // First tradeId + "l": 27781, // Last tradeId + "T": 1498793709153, // Timestamp + "m": true, // Was the buyer the maker? + } +] +``` + +`` +GET /fapi/v1/aggTrades +`` + +Get compressed, aggregate market trades. Market trades that fill at the time, from the same order, with the same price will have the quantity aggregated. + +**Weight:** +20 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +fromId | LONG | NO | ID to get aggregate trades from INCLUSIVE. +startTime | LONG | NO | Timestamp in ms to get aggregate trades from INCLUSIVE. +endTime | LONG | NO | Timestamp in ms to get aggregate trades until INCLUSIVE. +limit | INT | NO | Default 500; max 1000. + +* If both startTime and endTime are sent, time between startTime and endTime must be less than 1 hour. +* If fromId, startTime, and endTime are not sent, the most recent aggregate trades will be returned. +* Only market trades will be aggregated and returned, which means the insurance fund trades and ADL trades won't be aggregated. + + + +## Kline/Candlestick Data + + +> **Response:** + +```javascript +[ + [ + 1499040000000, // Open time + "0.01634790", // Open + "0.80000000", // High + "0.01575800", // Low + "0.01577100", // Close + "148976.11427815", // Volume + 1499644799999, // Close time + "2434.19055334", // Quote asset volume + 308, // Number of trades + "1756.87402397", // Taker buy base asset volume + "28.46694368", // Taker buy quote asset volume + "17928899.62484339" // Ignore. + ] +] +``` + +`` +GET /fapi/v1/klines +`` + +Kline/candlestick bars for a symbol. +Klines are uniquely identified by their open time. + +**Weight:** based on parameter `LIMIT` + +LIMIT | weight +---|--- +[1,100) | 1 +[100, 500) | 2 +[500, 1000] | 5 +> 1000 | 10 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +interval | ENUM | YES | +startTime | LONG | NO | +endTime | LONG | NO | +limit | INT | NO | Default 500; max 1500. + +* If startTime and endTime are not sent, the most recent klines are returned. + + +## Index Price Kline/Candlestick Data + +> **Response:** + +```javascript +[ + [ + 1591256400000, // Open time + "9653.69440000", // Open + "9653.69640000", // High + "9651.38600000", // Low + "9651.55200000", // Close (or latest price) + "0 ", // Ignore + 1591256459999, // Close time + "0", // Ignore + 60, // Number of bisic data + "0", // Ignore + "0", // Ignore + "0" // Ignore + ] +] +``` + +`` +GET /fapi/v1/indexPriceKlines +`` + +Kline/candlestick bars for the index price of a pair. + +Klines are uniquely identified by their open time. + +**Weight:** based on parameter `LIMIT` + +LIMIT | weight +---|--- +[1,100) | 1 +[100, 500) | 2 +[500, 1000] | 5 +> 1000 | 10 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +pair | STRING | YES | +interval | ENUM | YES | +startTime | LONG | NO | +endTime | LONG | NO | +limit | INT | NO | Default 500; max 1500. + +* If startTime and endTime are not sent, the most recent klines are returned. + + +## Mark Price Kline/Candlestick Data + +> **Response:** + +```javascript +[ + [ + 1591256460000, // Open time + "9653.29201333", // Open + "9654.56401333", // High + "9653.07367333", // Low + "9653.07367333", // Close (or latest price) + "0 ", // Ignore + 1591256519999, // Close time + "0", // Ignore + 60, // Number of bisic data + "0", // Ignore + "0", // Ignore + "0" // Ignore + ] +] +``` + +`` +GET /fapi/v1/markPriceKlines +`` + +Kline/candlestick bars for the mark price of a symbol. + +Klines are uniquely identified by their open time. + + +**Weight:** based on parameter `LIMIT` + +LIMIT | weight +---|--- +[1,100) | 1 +[100, 500) | 2 +[500, 1000] | 5 +> 1000 | 10 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +interval | ENUM | YES | +startTime | LONG | NO | +endTime | LONG | NO | +limit | INT | NO | Default 500; max 1500. + +* If startTime and endTime are not sent, the most recent klines are returned. + + +## Mark Price + + +> **Response:** + +```javascript +{ + "symbol": "BTCUSDT", + "markPrice": "11793.63104562", // mark price + "indexPrice": "11781.80495970", // index price + "estimatedSettlePrice": "11781.16138815", // Estimated Settle Price, only useful in the last hour before the settlement starts. + "lastFundingRate": "0.00038246", // This is the lasted funding rate + "nextFundingTime": 1597392000000, + "interestRate": "0.00010000", + "time": 1597370495002 +} +``` + +> **OR (when symbol not sent)** + +```javascript +[ + { + "symbol": "BTCUSDT", + "markPrice": "11793.63104562", // mark price + "indexPrice": "11781.80495970", // index price + "estimatedSettlePrice": "11781.16138815", // Estimated Settle Price, only useful in the last hour before the settlement starts. + "lastFundingRate": "0.00038246", // This is the lasted funding rate + "nextFundingTime": 1597392000000, + "interestRate": "0.00010000", + "time": 1597370495002 + } +] +``` + + +`` +GET /fapi/v1/premiumIndex +`` + +Mark Price and Funding Rate + +**Weight:** +1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | NO | + + + +## Get Funding Rate History + +> **Response:** + +```javascript +[ + { + "symbol": "BTCUSDT", + "fundingRate": "-0.03750000", + "fundingTime": 1570608000000, + }, + { + "symbol": "BTCUSDT", + "fundingRate": "0.00010000", + "fundingTime": 1570636800000, + } +] +``` + +`` +GET /fapi/v1/fundingRate +`` + + +**Weight:** +1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | NO | +startTime | LONG | NO | Timestamp in ms to get funding rate from INCLUSIVE. +endTime | LONG | NO | Timestamp in ms to get funding rate until INCLUSIVE. +limit | INT | NO | Default 100; max 1000 + + +* If `startTime` and `endTime` are not sent, the most recent `limit` datas are returned. +* If the number of data between `startTime` and `endTime` is larger than `limit`, return as `startTime` + `limit`. +* In ascending order. + +## Get Funding Rate Config + +> **Response:** + +```javascript +[ + { + "symbol": "INJUSDT", + "interestRate": "0.00010000", + "time": 1756197479000, + "fundingIntervalHours": 8, + "fundingFeeCap": 0.03, + "fundingFeeFloor": -0.03 + }, + { + "symbol": "ZORAUSDT", + "interestRate": "0.00005000", + "time": 1756197479000, + "fundingIntervalHours": 4, + "fundingFeeCap": 0.02, + "fundingFeeFloor": -0.02 + } +] +``` + +`` +GET /fapi/v1/fundingInfo +`` + + +**Weight:** +1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | NO | + + + +## 24hr Ticker Price Change Statistics + +> **Response:** + +```javascript +{ + "symbol": "BTCUSDT", + "priceChange": "-94.99999800", + "priceChangePercent": "-95.960", + "weightedAvgPrice": "0.29628482", + "prevClosePrice": "0.10002000", + "lastPrice": "4.00000200", + "lastQty": "200.00000000", + "openPrice": "99.00000000", + "highPrice": "100.00000000", + "lowPrice": "0.10000000", + "volume": "8913.30000000", + "quoteVolume": "15.30000000", + "openTime": 1499783499040, + "closeTime": 1499869899040, + "firstId": 28385, // First tradeId + "lastId": 28460, // Last tradeId + "count": 76 // Trade count +} +``` + +> OR + +```javascript +[ + { + "symbol": "BTCUSDT", + "priceChange": "-94.99999800", + "priceChangePercent": "-95.960", + "weightedAvgPrice": "0.29628482", + "prevClosePrice": "0.10002000", + "lastPrice": "4.00000200", + "lastQty": "200.00000000", + "openPrice": "99.00000000", + "highPrice": "100.00000000", + "lowPrice": "0.10000000", + "volume": "8913.30000000", + "quoteVolume": "15.30000000", + "openTime": 1499783499040, + "closeTime": 1499869899040, + "firstId": 28385, // First tradeId + "lastId": 28460, // Last tradeId + "count": 76 // Trade count + } +] +``` + +`` +GET /fapi/v1/ticker/24hr +`` + +24 hour rolling window price change statistics. +**Careful** when accessing this with no symbol. + +**Weight:** +1 for a single symbol; +**40** when the symbol parameter is omitted + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | NO | + +* If the symbol is not sent, tickers for all symbols will be returned in an array. + + + +## Symbol Price Ticker + +> **Response:** + +```javascript +{ + "symbol": "BTCUSDT", + "price": "6000.01", + "time": 1589437530011 // Transaction time +} +``` + +> OR + + +```javascript +[ + { + "symbol": "BTCUSDT", + "price": "6000.01", + "time": 1589437530011 + } +] +``` + +`` +GET /fapi/v1/ticker/price +`` + +Latest price for a symbol or symbols. + +**Weight:** +1 for a single symbol; +**2** when the symbol parameter is omitted + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | NO | + +* If the symbol is not sent, prices for all symbols will be returned in an array. + + + +## Symbol Order Book Ticker + + +> **Response:** + +```javascript +{ + "symbol": "BTCUSDT", + "bidPrice": "4.00000000", + "bidQty": "431.00000000", + "askPrice": "4.00000200", + "askQty": "9.00000000", + "time": 1589437530011 // Transaction time +} +``` + +> OR + + +```javascript +[ + { + "symbol": "BTCUSDT", + "bidPrice": "4.00000000", + "bidQty": "431.00000000", + "askPrice": "4.00000200", + "askQty": "9.00000000", + "time": 1589437530011 + } +] +``` + +`` +GET /fapi/v1/ticker/bookTicker +`` + +Best price/qty on the order book for a symbol or symbols. + +**Weight:** +1 for a single symbol; +**2** when the symbol parameter is omitted + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | NO | + +* If the symbol is not sent, bookTickers for all symbols will be returned in an array. + + + +# Websocket Market Streams + +* The baseurl for websocket is **wss://fstream.asterdex.com** +* Streams can be access either in a single raw stream or a combined stream +* Raw streams are accessed at **/ws/\** +* Combined streams are accessed at **/stream?streams=\/\/\** +* Combined stream events are wrapped as follows: **{"stream":"\","data":\}** +* All symbols for streams are **lowercase** +* A single connection is only valid for 24 hours; expect to be disconnected at the 24 hour mark +* The websocket server will send a `ping frame` every 5 minutes. If the websocket server does not receive a `pong frame` back from the connection within a 15 minute period, the connection will be disconnected. Unsolicited `pong frames` are allowed. +* WebSocket connections have a limit of 10 incoming messages per second. +* A connection that goes beyond the limit will be disconnected; IPs that are repeatedly disconnected may be banned. +* A single connection can listen to a maximum of **200** streams. +* Considering the possible data latency from RESTful endpoints during an extremely volatile market, it is highly recommended to get the order status, position, etc from the Websocket user data stream. + + +## Live Subscribing/Unsubscribing to streams + +* The following data can be sent through the websocket instance in order to subscribe/unsubscribe from streams. Examples can be seen below. +* The `id` used in the JSON payloads is an unsigned INT used as an identifier to uniquely identify the messages going back and forth. + +### Subscribe to a stream + +> **Response** + + ```javascript + { + "result": null, + "id": 1 + } + ``` + +* **Request** + + { + "method": "SUBSCRIBE", + "params": + [ + "btcusdt@aggTrade", + "btcusdt@depth" + ], + "id": 1 + } + + + +### Unsubscribe to a stream + +> **Response** + + ```javascript + { + "result": null, + "id": 312 + } + ``` + + +* **Request** + + { + "method": "UNSUBSCRIBE", + "params": + [ + "btcusdt@depth" + ], + "id": 312 + } + + + +### Listing Subscriptions + +> **Response** + + ```javascript + { + "result": [ + "btcusdt@aggTrade" + ], + "id": 3 + } + ``` + + +* **Request** + + { + "method": "LIST_SUBSCRIPTIONS", + "id": 3 + } + + + +### Setting Properties +Currently, the only property can be set is to set whether `combined` stream payloads are enabled are not. +The combined property is set to `false` when connecting using `/ws/` ("raw streams") and `true` when connecting using `/stream/`. + +> **Response** + + ```javascript + { + "result": null, + "id": 5 + } + ``` + +* **Request** + + { + "method": "SET_PROPERTY", + "params": + [ + "combined", + true + ], + "id": 5 + } + + + + +### Retrieving Properties + +> **Response** + + ```javascript + { + "result": true, // Indicates that combined is set to true. + "id": 2 + } + ``` + +* **Request** + + { + "method": "GET_PROPERTY", + "params": + [ + "combined" + ], + "id": 2 + } + + + + +### Error Messages + +Error Message | Description +---|--- +{"code": 0, "msg": "Unknown property"} | Parameter used in the `SET_PROPERTY` or `GET_PROPERTY` was invalid +{"code": 1, "msg": "Invalid value type: expected Boolean"} | Value should only be `true` or `false` +{"code": 2, "msg": "Invalid request: property name must be a string"}| Property name provided was invalid +{"code": 2, "msg": "Invalid request: request ID must be an unsigned integer"}| Parameter `id` had to be provided or the value provided in the `id` parameter is an unsupported type +{"code": 2, "msg": "Invalid request: unknown variant %s, expected one of `SUBSCRIBE`, `UNSUBSCRIBE`, `LIST_SUBSCRIPTIONS`, `SET_PROPERTY`, `GET_PROPERTY` at line 1 column 28"} | Possible typo in the provided method or provided method was neither of the expected values +{"code": 2, "msg": "Invalid request: too many parameters"}| Unnecessary parameters provided in the data +{"code": 2, "msg": "Invalid request: property name must be a string"} | Property name was not provided +{"code": 2, "msg": "Invalid request: missing field `method` at line 1 column 73"} | `method` was not provided in the data +{"code":3,"msg":"Invalid JSON: expected value at line %s column %s"} | JSON data sent has incorrect syntax. + + + + + + +## Aggregate Trade Streams + + +> **Payload:** + +```javascript +{ + "e": "aggTrade", // Event type + "E": 123456789, // Event time + "s": "BTCUSDT", // Symbol + "a": 5933014, // Aggregate trade ID + "p": "0.001", // Price + "q": "100", // Quantity + "f": 100, // First trade ID + "l": 105, // Last trade ID + "T": 123456785, // Trade time + "m": true, // Is the buyer the market maker? +} +``` + +The Aggregate Trade Streams push market trade information that is aggregated for a single taker order every 100 milliseconds. + +**Stream Name:** +``@aggTrade`` + +**Update Speed:** 100ms + +* Only market trades will be aggregated, which means the insurance fund trades and ADL trades won't be aggregated. + +## Mark Price Stream + +> **Payload:** + +```javascript + { + "e": "markPriceUpdate", // Event type + "E": 1562305380000, // Event time + "s": "BTCUSDT", // Symbol + "p": "11794.15000000", // Mark price + "i": "11784.62659091", // Index price + "P": "11784.25641265", // Estimated Settle Price, only useful in the last hour before the settlement starts + "r": "0.00038167", // Funding rate + "T": 1562306400000 // Next funding time + } +``` + +Mark price and funding rate for a single symbol pushed every 3 seconds or every second. + +**Stream Name:** +``@markPrice`` or ``@markPrice@1s`` + +**Update Speed:** 3000ms or 1000ms + + + +## Mark Price Stream for All market + +> **Payload:** + +```javascript +[ + { + "e": "markPriceUpdate", // Event type + "E": 1562305380000, // Event time + "s": "BTCUSDT", // Symbol + "p": "11185.87786614", // Mark price + "i": "11784.62659091" // Index price + "P": "11784.25641265", // Estimated Settle Price, only useful in the last hour before the settlement starts + "r": "0.00030000", // Funding rate + "T": 1562306400000 // Next funding time + } +] +``` + +Mark price and funding rate for all symbols pushed every 3 seconds or every second. + +**Stream Name:** +``!markPrice@arr`` or ``!markPrice@arr@1s`` + +**Update Speed:** 3000ms or 1000ms + + + +## Kline/Candlestick Streams + + +> **Payload:** + +```javascript +{ + "e": "kline", // Event type + "E": 123456789, // Event time + "s": "BTCUSDT", // Symbol + "k": { + "t": 123400000, // Kline start time + "T": 123460000, // Kline close time + "s": "BTCUSDT", // Symbol + "i": "1m", // Interval + "f": 100, // First trade ID + "L": 200, // Last trade ID + "o": "0.0010", // Open price + "c": "0.0020", // Close price + "h": "0.0025", // High price + "l": "0.0015", // Low price + "v": "1000", // Base asset volume + "n": 100, // Number of trades + "x": false, // Is this kline closed? + "q": "1.0000", // Quote asset volume + "V": "500", // Taker buy base asset volume + "Q": "0.500", // Taker buy quote asset volume + "B": "123456" // Ignore + } +} +``` + +The Kline/Candlestick Stream push updates to the current klines/candlestick every 250 milliseconds (if existing). + +**Kline/Candlestick chart intervals:** + +m -> minutes; h -> hours; d -> days; w -> weeks; M -> months + +* 1m +* 3m +* 5m +* 15m +* 30m +* 1h +* 2h +* 4h +* 6h +* 8h +* 12h +* 1d +* 3d +* 1w +* 1M + +**Stream Name:** +``@kline_`` + +**Update Speed:** 250ms + + +## Individual Symbol Mini Ticker Stream + + +> **Payload:** + +```javascript + { + "e": "24hrMiniTicker", // Event type + "E": 123456789, // Event time + "s": "BTCUSDT", // Symbol + "c": "0.0025", // Close price + "o": "0.0010", // Open price + "h": "0.0025", // High price + "l": "0.0010", // Low price + "v": "10000", // Total traded base asset volume + "q": "18" // Total traded quote asset volume + } +``` + +24hr rolling window mini-ticker statistics for a single symbol. These are NOT the statistics of the UTC day, but a 24hr rolling window from requestTime to 24hrs before. + +**Stream Name:** +``@miniTicker`` + +**Update Speed:** 500ms + + + + +## All Market Mini Tickers Stream + + +> **Payload:** + +```javascript +[ + { + "e": "24hrMiniTicker", // Event type + "E": 123456789, // Event time + "s": "BTCUSDT", // Symbol + "c": "0.0025", // Close price + "o": "0.0010", // Open price + "h": "0.0025", // High price + "l": "0.0010", // Low price + "v": "10000", // Total traded base asset volume + "q": "18" // Total traded quote asset volume + } +] +``` + +24hr rolling window mini-ticker statistics for all symbols. These are NOT the statistics of the UTC day, but a 24hr rolling window from requestTime to 24hrs before. Note that only tickers that have changed will be present in the array. + +**Stream Name:** +``!miniTicker@arr`` + +**Update Speed:** 1000ms + + + +## Individual Symbol Ticker Streams + + +> **Payload:** + +```javascript +{ + "e": "24hrTicker", // Event type + "E": 123456789, // Event time + "s": "BTCUSDT", // Symbol + "p": "0.0015", // Price change + "P": "250.00", // Price change percent + "w": "0.0018", // Weighted average price + "c": "0.0025", // Last price + "Q": "10", // Last quantity + "o": "0.0010", // Open price + "h": "0.0025", // High price + "l": "0.0010", // Low price + "v": "10000", // Total traded base asset volume + "q": "18", // Total traded quote asset volume + "O": 0, // Statistics open time + "C": 86400000, // Statistics close time + "F": 0, // First trade ID + "L": 18150, // Last trade Id + "n": 18151 // Total number of trades +} +``` + +24hr rollwing window ticker statistics for a single symbol. These are NOT the statistics of the UTC day, but a 24hr rolling window from requestTime to 24hrs before. + +**Stream Name:** +``@ticker`` + +**Update Speed:** 500ms + + + +## All Market Tickers Streams + + +> **Payload:** + +```javascript +[ + { + "e": "24hrTicker", // Event type + "E": 123456789, // Event time + "s": "BTCUSDT", // Symbol + "p": "0.0015", // Price change + "P": "250.00", // Price change percent + "w": "0.0018", // Weighted average price + "c": "0.0025", // Last price + "Q": "10", // Last quantity + "o": "0.0010", // Open price + "h": "0.0025", // High price + "l": "0.0010", // Low price + "v": "10000", // Total traded base asset volume + "q": "18", // Total traded quote asset volume + "O": 0, // Statistics open time + "C": 86400000, // Statistics close time + "F": 0, // First trade ID + "L": 18150, // Last trade Id + "n": 18151 // Total number of trades + } +] +``` + +24hr rollwing window ticker statistics for all symbols. These are NOT the statistics of the UTC day, but a 24hr rolling window from requestTime to 24hrs before. Note that only tickers that have changed will be present in the array. + +**Stream Name:** +``!ticker@arr`` + +**Update Speed:** 1000ms + + + + + +## Individual Symbol Book Ticker Streams + +> **Payload:** + +```javascript +{ + "e":"bookTicker", // event type + "u":400900217, // order book updateId + "E": 1568014460893, // event time + "T": 1568014460891, // transaction time + "s":"BNBUSDT", // symbol + "b":"25.35190000", // best bid price + "B":"31.21000000", // best bid qty + "a":"25.36520000", // best ask price + "A":"40.66000000" // best ask qty +} +``` + + +Pushes any update to the best bid or ask's price or quantity in real-time for a specified symbol. + +**Stream Name:** `@bookTicker` + +**Update Speed:** Real-time + + + + + +## All Book Tickers Stream + +> **Payload:** + +```javascript +{ + // Same as @bookTicker payload +} +``` + +Pushes any update to the best bid or ask's price or quantity in real-time for all symbols. + +**Stream Name:** `!bookTicker` + +**Update Speed:** Real-time + + + +## Liquidation Order Streams + +> **Payload:** + +```javascript +{ + + "e":"forceOrder",                   // Event Type + "E":1568014460893,                  // Event Time + "o":{ + + "s":"BTCUSDT",                   // Symbol + "S":"SELL",                      // Side + "o":"LIMIT",                     // Order Type + "f":"IOC",                       // Time in Force + "q":"0.014",                     // Original Quantity + "p":"9910",                      // Price + "ap":"9910",                     // Average Price + "X":"FILLED",                    // Order Status + "l":"0.014",                     // Order Last Filled Quantity + "z":"0.014",                     // Order Filled Accumulated Quantity + "T":1568014460893,          // Order Trade Time + + } + +} +``` + +The Liquidation Order Snapshot Streams push force liquidation order information for specific symbol. + +For each symbol,only the latest one liquidation order within 1000ms will be pushed as the snapshot. If no liquidation happens in the interval of 1000ms, no stream will be pushed. + +**Stream Name:**  ``@forceOrder`` + +**Update Speed:** 1000ms + + + +## All Market Liquidation Order Streams + +> **Payload:** + +```javascript +{ + + "e":"forceOrder",                   // Event Type + "E":1568014460893,                  // Event Time + "o":{ + + "s":"BTCUSDT",                   // Symbol + "S":"SELL",                      // Side + "o":"LIMIT",                     // Order Type + "f":"IOC",                       // Time in Force + "q":"0.014",                     // Original Quantity + "p":"9910",                      // Price + "ap":"9910",                     // Average Price + "X":"FILLED",                    // Order Status + "l":"0.014",                     // Order Last Filled Quantity + "z":"0.014",                     // Order Filled Accumulated Quantity + "T":1568014460893,          // Order Trade Time + + } + +} +``` + +The All Liquidation Order Snapshot Streams push force liquidation order information for all symbols in the market. + +For each symbol,only the latest one liquidation order within 1000ms will be pushed as the snapshot. If no liquidation happens in the interval of 1000ms, no stream will be pushed. + +**Stream Name:** ``!forceOrder@arr`` + +**Update Speed:** 1000ms + + + + + + +## Partial Book Depth Streams + +> **Payload:** + +```javascript +{ + "e": "depthUpdate", // Event type + "E": 1571889248277, // Event time + "T": 1571889248276, // Transaction time + "s": "BTCUSDT", + "U": 390497796, + "u": 390497878, + "pu": 390497794, + "b": [ // Bids to be updated + [ + "7403.89", // Price Level to be + "0.002" // Quantity + ], + [ + "7403.90", + "3.906" + ], + [ + "7404.00", + "1.428" + ], + [ + "7404.85", + "5.239" + ], + [ + "7405.43", + "2.562" + ] + ], + "a": [ // Asks to be updated + [ + "7405.96", // Price level to be + "3.340" // Quantity + ], + [ + "7406.63", + "4.525" + ], + [ + "7407.08", + "2.475" + ], + [ + "7407.15", + "4.800" + ], + [ + "7407.20", + "0.175" + ] + ] +} +``` + +Top **** bids and asks, Valid **** are 5, 10, or 20. + +**Stream Names:** `@depth` OR `@depth@500ms` OR `@depth@100ms`. + +**Update Speed:** 250ms, 500ms or 100ms + + + + + + +## Diff. Book Depth Streams + + +> **Payload:** + +```javascript +{ + "e": "depthUpdate", // Event type + "E": 123456789, // Event time + "T": 123456788, // Transaction time + "s": "BTCUSDT", // Symbol + "U": 157, // First update ID in event + "u": 160, // Final update ID in event + "pu": 149, // Final update Id in last stream(ie `u` in last stream) + "b": [ // Bids to be updated + [ + "0.0024", // Price level to be updated + "10" // Quantity + ] + ], + "a": [ // Asks to be updated + [ + "0.0026", // Price level to be updated + "100" // Quantity + ] + ] +} +``` + +Bids and asks, pushed every 250 milliseconds, 500 milliseconds, 100 milliseconds (if existing) + +**Stream Name:** +``@depth`` OR ``@depth@500ms`` OR ``@depth@100ms`` + +**Update Speed:** 250ms, 500ms, 100ms + + + +## How to manage a local order book correctly +1. Open a stream to **wss://fstream.asterdex.com/stream?streams=btcusdt@depth**. +2. Buffer the events you receive from the stream. For same price, latest received update covers the previous one. +3. Get a depth snapshot from **https://fapi.asterdex.com/fapi/v1/depth?symbol=BTCUSDT&limit=1000** . +4. Drop any event where `u` is < `lastUpdateId` in the snapshot. +5. The first processed event should have `U` <= `lastUpdateId` **AND** `u` >= `lastUpdateId` +6. While listening to the stream, each new event's `pu` should be equal to the previous event's `u`, otherwise initialize the process from step 3. +7. The data in each event is the **absolute** quantity for a price level. +8. If the quantity is 0, **remove** the price level. +9. Receiving an event that removes a price level that is not in your local order book can happen and is normal. + + + +# Account/Trades Endpoints + + + +## Change Position Mode(TRADE) + +> **Response:** + +```javascript +{ + "code": 200, + "msg": "success" +} +``` + +`` +POST /fapi/v1/positionSide/dual (HMAC SHA256) +`` + +Change user's position mode (Hedge Mode or One-way Mode ) on ***EVERY symbol*** + +**Weight:** +1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +dualSidePosition | STRING | YES | "true": Hedge Mode; "false": One-way Mode +recvWindow | LONG | NO | +timestamp | LONG | YES | + + +## Get Current Position Mode(USER_DATA) + +> **Response:** + +```javascript +{ + "dualSidePosition": true // "true": Hedge Mode; "false": One-way Mode +} +``` + +`` +GET /fapi/v1/positionSide/dual (HMAC SHA256) +`` + +Get user's position mode (Hedge Mode or One-way Mode ) on ***EVERY symbol*** + +**Weight:** +30 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +recvWindow | LONG | NO | +timestamp | LONG | YES | + + +## Change Multi-Assets Mode (TRADE) + +> **Response:** + +```javascript +{ + "code": 200, + "msg": "success" +} +``` + +`` +POST /fapi/v1/multiAssetsMargin (HMAC SHA256) +`` + +Change user's Multi-Assets mode (Multi-Assets Mode or Single-Asset Mode) on ***Every symbol*** + +**Weight:** +1 + +**Parameters:** + +Name | Type | Mandatory | Description +---------- | ------ | -------- | ----------------- +multiAssetsMargin | STRING | YES | "true": Multi-Assets Mode; "false": Single-Asset Mode +recvWindow | LONG | NO | +timestamp | LONG | YES | + + + +## Get Current Multi-Assets Mode (USER_DATA) + +> **Response:** + +```javascript +{ + "multiAssetsMargin": true // "true": Multi-Assets Mode; "false": Single-Asset Mode +} +``` + +`` +GET /fapi/v1/multiAssetsMargin (HMAC SHA256) +`` + +Get user's Multi-Assets mode (Multi-Assets Mode or Single-Asset Mode) on ***Every symbol*** + +**Weight:** +30 + +**Parameters:** + +Name | Type | Mandatory | Description +---------- | ------ | -------- | ----------------- +recvWindow | LONG | NO | +timestamp | LONG | YES | + + +## New Order (TRADE) + + +> **Response:** + +```javascript +{ + "clientOrderId": "testOrder", + "cumQty": "0", + "cumQuote": "0", + "executedQty": "0", + "orderId": 22542179, + "avgPrice": "0.00000", + "origQty": "10", + "price": "0", + "reduceOnly": false, + "side": "BUY", + "positionSide": "SHORT", + "status": "NEW", + "stopPrice": "9300", // please ignore when order type is TRAILING_STOP_MARKET + "closePosition": false, // if Close-All + "symbol": "BTCUSDT", + "timeInForce": "GTC", + "type": "TRAILING_STOP_MARKET", + "origType": "TRAILING_STOP_MARKET", + "activatePrice": "9020", // activation price, only return with TRAILING_STOP_MARKET order + "priceRate": "0.3", // callback rate, only return with TRAILING_STOP_MARKET order + "updateTime": 1566818724722, + "workingType": "CONTRACT_PRICE", + "priceProtect": false // if conditional order trigger is protected +} +``` + + +`` +POST /fapi/v1/order (HMAC SHA256) +`` + +Send in a new order. + +**Weight:** +1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +side | ENUM | YES | +positionSide | ENUM | NO | Default `BOTH` for One-way Mode ; `LONG` or `SHORT` for Hedge Mode. It must be sent in Hedge Mode. +type | ENUM | YES | +timeInForce | ENUM | NO | +quantity | DECIMAL | NO | Cannot be sent with `closePosition`=`true`(Close-All) +reduceOnly | STRING | NO | "true" or "false". default "false". Cannot be sent in Hedge Mode; cannot be sent with `closePosition`=`true` +price | DECIMAL | NO | +newClientOrderId | STRING | NO | A unique id among open orders. Automatically generated if not sent. Can only be string following the rule: `^[\.A-Z\:/a-z0-9_-]{1,36}$` +stopPrice | DECIMAL | NO | Used with `STOP/STOP_MARKET` or `TAKE_PROFIT/TAKE_PROFIT_MARKET` orders. +closePosition | STRING | NO | `true`, `false`;Close-All,used with `STOP_MARKET` or `TAKE_PROFIT_MARKET`. +activationPrice | DECIMAL | NO | Used with `TRAILING_STOP_MARKET` orders, default as the latest price(supporting different `workingType`) +callbackRate | DECIMAL | NO | Used with `TRAILING_STOP_MARKET` orders, min 0.1, max 5 where 1 for 1% +workingType | ENUM | NO | stopPrice triggered by: "MARK_PRICE", "CONTRACT_PRICE". Default "CONTRACT_PRICE" +priceProtect | STRING | NO | "TRUE" or "FALSE", default "FALSE". Used with `STOP/STOP_MARKET` or `TAKE_PROFIT/TAKE_PROFIT_MARKET` orders. +newOrderRespType | ENUM | NO | "ACK", "RESULT", default "ACK" +recvWindow | LONG | NO | +timestamp | LONG | YES | + +Additional mandatory parameters based on `type`: + +Type | Additional mandatory parameters +------------ | ------------ +`LIMIT` | `timeInForce`, `quantity`, `price` +`MARKET` | `quantity` +`STOP/TAKE_PROFIT` | `quantity`, `price`, `stopPrice` +`STOP_MARKET/TAKE_PROFIT_MARKET` | `stopPrice` +`TRAILING_STOP_MARKET` | `callbackRate` + +* Order with type `STOP`, parameter `timeInForce` can be sent ( default `GTC`). +* Order with type `TAKE_PROFIT`, parameter `timeInForce` can be sent ( default `GTC`). +* Condition orders will be triggered when: + + * If parameter`priceProtect`is sent as true: + * when price reaches the `stopPrice` ,the difference rate between "MARK_PRICE" and "CONTRACT_PRICE" cannot be larger than the "triggerProtect" of the symbol + * "triggerProtect" of a symbol can be got from `GET /fapi/v1/exchangeInfo` + + * `STOP`, `STOP_MARKET`: + * BUY: latest price ("MARK_PRICE" or "CONTRACT_PRICE") >= `stopPrice` + * SELL: latest price ("MARK_PRICE" or "CONTRACT_PRICE") <= `stopPrice` + * `TAKE_PROFIT`, `TAKE_PROFIT_MARKET`: + * BUY: latest price ("MARK_PRICE" or "CONTRACT_PRICE") <= `stopPrice` + * SELL: latest price ("MARK_PRICE" or "CONTRACT_PRICE") >= `stopPrice` + * `TRAILING_STOP_MARKET`: + * BUY: the lowest price after order placed <= `activationPrice`, and the latest price >= the lowest price * (1 + `callbackRate`) + * SELL: the highest price after order placed >= `activationPrice`, and the latest price <= the highest price * (1 - `callbackRate`) + +* For `TRAILING_STOP_MARKET`, if you got such error code. + ``{"code": -2021, "msg": "Order would immediately trigger."}`` + means that the parameters you send do not meet the following requirements: + * BUY: `activationPrice` should be smaller than latest price. + * SELL: `activationPrice` should be larger than latest price. + +* If `newOrderRespType ` is sent as `RESULT` : + * `MARKET` order: the final FILLED result of the order will be return directly. + * `LIMIT` order with special `timeInForce`: the final status result of the order(FILLED or EXPIRED) will be returned directly. + +* `STOP_MARKET`, `TAKE_PROFIT_MARKET` with `closePosition`=`true`: + * Follow the same rules for condition orders. + * If triggered,**close all** current long position( if `SELL`) or current short position( if `BUY`). + * Cannot be used with `quantity` paremeter + * Cannot be used with `reduceOnly` parameter + * In Hedge Mode,cannot be used with `BUY` orders in `LONG` position side. and cannot be used with `SELL` orders in `SHORT` position side + + +## Place Multiple Orders (TRADE) + + +> **Response:** + +```javascript +[ + { + "clientOrderId": "testOrder", + "cumQty": "0", + "cumQuote": "0", + "executedQty": "0", + "orderId": 22542179, + "avgPrice": "0.00000", + "origQty": "10", + "price": "0", + "reduceOnly": false, + "side": "BUY", + "positionSide": "SHORT", + "status": "NEW", + "stopPrice": "9300", // please ignore when order type is TRAILING_STOP_MARKET + "symbol": "BTCUSDT", + "timeInForce": "GTC", + "type": "TRAILING_STOP_MARKET", + "origType": "TRAILING_STOP_MARKET", + "activatePrice": "9020", // activation price, only return with TRAILING_STOP_MARKET order + "priceRate": "0.3", // callback rate, only return with TRAILING_STOP_MARKET order + "updateTime": 1566818724722, + "workingType": "CONTRACT_PRICE", + "priceProtect": false // if conditional order trigger is protected + }, + { + "code": -2022, + "msg": "ReduceOnly Order is rejected." + } +] +``` + +`` +POST /fapi/v1/batchOrders (HMAC SHA256) +`` + +**Weight:** +5 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +batchOrders | LIST | YES | order list. Max 5 orders +recvWindow | LONG | NO +timestamp | LONG | YES + +**Where ``batchOrders`` is the list of order parameters in JSON** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +side | ENUM | YES | +positionSide | ENUM | NO | Default `BOTH` for One-way Mode ; `LONG` or `SHORT` for Hedge Mode. It must be sent with Hedge Mode. +type | ENUM | YES | +timeInForce | ENUM | NO | +quantity | DECIMAL | YES | +reduceOnly | STRING | NO | "true" or "false". default "false". +price | DECIMAL | NO | +newClientOrderId | STRING | NO | A unique id among open orders. Automatically generated if not sent. Can only be string following the rule: `^[\.A-Z\:/a-z0-9_-]{1,36}$` +stopPrice | DECIMAL | NO | Used with `STOP/STOP_MARKET` or `TAKE_PROFIT/TAKE_PROFIT_MARKET` orders. +activationPrice | DECIMAL | NO | Used with `TRAILING_STOP_MARKET` orders, default as the latest price(supporting different `workingType`) +callbackRate | DECIMAL | NO | Used with `TRAILING_STOP_MARKET` orders, min 0.1, max 4 where 1 for 1% +workingType | ENUM | NO | stopPrice triggered by: "MARK_PRICE", "CONTRACT_PRICE". Default "CONTRACT_PRICE" +priceProtect | STRING | NO | "TRUE" or "FALSE", default "FALSE". Used with `STOP/STOP_MARKET` or `TAKE_PROFIT/TAKE_PROFIT_MARKET` orders. +newOrderRespType | ENUM | NO | "ACK", "RESULT", default "ACK" + + +* Paremeter rules are same with `New Order` +* Batch orders are processed concurrently, and the order of matching is not guaranteed. +* The order of returned contents for batch orders is the same as the order of the order list. + +## Transfer Between Futures And Spot (USER_DATA) + +> **Response:** + +```javascript +{ + "tranId": 21841, //transaction id + "status": "SUCCESS" //status +} +``` + +`` +POST /fapi/v1/asset/wallet/transfer (HMAC SHA256) +`` + +**Weight:** +5 + +**Parameters:** + +Name | Type | Mandatory | Description +---------------- | ------- | -------- | ---- +amount | DECIMAL | YES | amount +asset | STRING | YES | asset +clientTranId | STRING | YES | transaction id +kindType | STRING | YES | kindType +timestamp | LONG | YES | timestamp + +Notes: + +* kindType can take the following values: + FUTURE_SPOT (futures converted to spot) + SPOT_FUTURE (spot converted to futures) + + +## Query Order (USER_DATA) + + +> **Response:** + +```javascript +{ + "avgPrice": "0.00000", + "clientOrderId": "abc", + "cumQuote": "0", + "executedQty": "0", + "orderId": 1917641, + "origQty": "0.40", + "origType": "TRAILING_STOP_MARKET", + "price": "0", + "reduceOnly": false, + "side": "BUY", + "positionSide": "SHORT", + "status": "NEW", + "stopPrice": "9300", // please ignore when order type is TRAILING_STOP_MARKET + "closePosition": false, // if Close-All + "symbol": "BTCUSDT", + "time": 1579276756075, // order time + "timeInForce": "GTC", + "type": "TRAILING_STOP_MARKET", + "activatePrice": "9020", // activation price, only return with TRAILING_STOP_MARKET order + "priceRate": "0.3", // callback rate, only return with TRAILING_STOP_MARKET order + "updateTime": 1579276756075, // update time + "workingType": "CONTRACT_PRICE", + "priceProtect": false // if conditional order trigger is protected +} +``` + +`` +GET /fapi/v1/order (HMAC SHA256) +`` + +Check an order's status. + +**Weight:** +1 + +* These orders will not be found: + * order status is `CANCELED` or `EXPIRED`, **AND** + * order has NO filled trade, **AND** + * created time + 7 days < current time + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +orderId | LONG | NO | +origClientOrderId | STRING | NO | +recvWindow | LONG | NO | +timestamp | LONG | YES | + +Notes: + +* Either `orderId` or `origClientOrderId` must be sent. + + + +## Cancel Order (TRADE) + +> **Response:** + +```javascript +{ + "clientOrderId": "myOrder1", + "cumQty": "0", + "cumQuote": "0", + "executedQty": "0", + "orderId": 283194212, + "origQty": "11", + "origType": "TRAILING_STOP_MARKET", + "price": "0", + "reduceOnly": false, + "side": "BUY", + "positionSide": "SHORT", + "status": "CANCELED", + "stopPrice": "9300", // please ignore when order type is TRAILING_STOP_MARKET + "closePosition": false, // if Close-All + "symbol": "BTCUSDT", + "timeInForce": "GTC", + "type": "TRAILING_STOP_MARKET", + "activatePrice": "9020", // activation price, only return with TRAILING_STOP_MARKET order + "priceRate": "0.3", // callback rate, only return with TRAILING_STOP_MARKET order + "updateTime": 1571110484038, + "workingType": "CONTRACT_PRICE", + "priceProtect": false // if conditional order trigger is protected +} +``` + +`` +DELETE /fapi/v1/order (HMAC SHA256) +`` + +Cancel an active order. + +**Weight:** +1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +orderId | LONG | NO | +origClientOrderId | STRING | NO | +recvWindow | LONG | NO | +timestamp | LONG | YES | + +Either `orderId` or `origClientOrderId` must be sent. + + +## Cancel All Open Orders (TRADE) + +> **Response:** + +```javascript +{ + "code": "200", + "msg": "The operation of cancel all open order is done." +} +``` + +`` +DELETE /fapi/v1/allOpenOrders (HMAC SHA256) +`` + +**Weight:** +1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +recvWindow | LONG | NO | +timestamp | LONG | YES | + + +## Cancel Multiple Orders (TRADE) + +> **Response:** + +```javascript +[ + { + "clientOrderId": "myOrder1", + "cumQty": "0", + "cumQuote": "0", + "executedQty": "0", + "orderId": 283194212, + "origQty": "11", + "origType": "TRAILING_STOP_MARKET", + "price": "0", + "reduceOnly": false, + "side": "BUY", + "positionSide": "SHORT", + "status": "CANCELED", + "stopPrice": "9300", // please ignore when order type is TRAILING_STOP_MARKET + "closePosition": false, // if Close-All + "symbol": "BTCUSDT", + "timeInForce": "GTC", + "type": "TRAILING_STOP_MARKET", + "activatePrice": "9020", // activation price, only return with TRAILING_STOP_MARKET order + "priceRate": "0.3", // callback rate, only return with TRAILING_STOP_MARKET order + "updateTime": 1571110484038, + "workingType": "CONTRACT_PRICE", + "priceProtect": false // if conditional order trigger is protected + }, + { + "code": -2011, + "msg": "Unknown order sent." + } +] +``` + +`` +DELETE /fapi/v1/batchOrders (HMAC SHA256) +`` + +**Weight:** +1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +orderIdList | LIST\ | NO | max length 10
e.g. [1234567,2345678] +origClientOrderIdList | LIST\ | NO | max length 10
e.g. ["my_id_1","my_id_2"], encode the double quotes. No space after comma. +recvWindow | LONG | NO | +timestamp | LONG | YES | + +Either `orderIdList` or `origClientOrderIdList ` must be sent. + + + + +## Auto-Cancel All Open Orders (TRADE) + +> **Response:** + +```javascript +{ + "symbol": "BTCUSDT", + "countdownTime": "100000" +} +``` + + +Cancel all open orders of the specified symbol at the end of the specified countdown. + +`` +POST /fapi/v1/countdownCancelAll (HMAC SHA256) +`` + +**Weight:** +10 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +countdownTime | LONG | YES | countdown time, 1000 for 1 second. 0 to cancel the timer +recvWindow | LONG | NO | +timestamp | LONG | YES | + +* The endpoint should be called repeatedly as heartbeats so that the existing countdown time can be canceled and replaced by a new one. + +* Example usage: + Call this endpoint at 30s intervals with an countdownTime of 120000 (120s). + If this endpoint is not called within 120 seconds, all your orders of the specified symbol will be automatically canceled. + If this endpoint is called with an countdownTime of 0, the countdown timer will be stopped. + +* The system will check all countdowns **approximately every 10 milliseconds**, so please note that sufficient redundancy should be considered when using this function. We do not recommend setting the countdown time to be too precise or too small. + + + + + +## Query Current Open Order (USER_DATA) + +> **Response:** + +```javascript + +{ + "avgPrice": "0.00000", + "clientOrderId": "abc", + "cumQuote": "0", + "executedQty": "0", + "orderId": 1917641, + "origQty": "0.40", + "origType": "TRAILING_STOP_MARKET", + "price": "0", + "reduceOnly": false, + "side": "BUY", + "positionSide": "SHORT", + "status": "NEW", + "stopPrice": "9300", // please ignore when order type is TRAILING_STOP_MARKET + "closePosition": false, // if Close-All + "symbol": "BTCUSDT", + "time": 1579276756075, // order time + "timeInForce": "GTC", + "type": "TRAILING_STOP_MARKET", + "activatePrice": "9020", // activation price, only return with TRAILING_STOP_MARKET order + "priceRate": "0.3", // callback rate, only return with TRAILING_STOP_MARKET order + "updateTime": 1579276756075, + "workingType": "CONTRACT_PRICE", + "priceProtect": false // if conditional order trigger is protected +} +``` + +`` +GET /fapi/v1/openOrder (HMAC SHA256) +`` + + +**Weight:** 1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +orderId | LONG | NO | +origClientOrderId | STRING | NO | +recvWindow | LONG | NO | +timestamp | LONG | YES | + +* Either`orderId` or `origClientOrderId` must be sent +* If the queried order has been filled or cancelled, the error message "Order does not exist" will be returned. + + + + +## Current All Open Orders (USER_DATA) + +> **Response:** + +```javascript +[ + { + "avgPrice": "0.00000", + "clientOrderId": "abc", + "cumQuote": "0", + "executedQty": "0", + "orderId": 1917641, + "origQty": "0.40", + "origType": "TRAILING_STOP_MARKET", + "price": "0", + "reduceOnly": false, + "side": "BUY", + "positionSide": "SHORT", + "status": "NEW", + "stopPrice": "9300", // please ignore when order type is TRAILING_STOP_MARKET + "closePosition": false, // if Close-All + "symbol": "BTCUSDT", + "time": 1579276756075, // order time + "timeInForce": "GTC", + "type": "TRAILING_STOP_MARKET", + "activatePrice": "9020", // activation price, only return with TRAILING_STOP_MARKET order + "priceRate": "0.3", // callback rate, only return with TRAILING_STOP_MARKET order + "updateTime": 1579276756075, // update time + "workingType": "CONTRACT_PRICE", + "priceProtect": false // if conditional order trigger is protected + } +] +``` + +`` +GET /fapi/v1/openOrders (HMAC SHA256) +`` + +Get all open orders on a symbol. **Careful** when accessing this with no symbol. + +**Weight:** +1 for a single symbol; **40** when the symbol parameter is omitted + + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | NO | +recvWindow | LONG | NO | +timestamp | LONG | YES | + + +* If the symbol is not sent, orders for all symbols will be returned in an array. + +## All Orders (USER_DATA) + + +> **Response:** + +```javascript +[ + { + "avgPrice": "0.00000", + "clientOrderId": "abc", + "cumQuote": "0", + "executedQty": "0", + "orderId": 1917641, + "origQty": "0.40", + "origType": "TRAILING_STOP_MARKET", + "price": "0", + "reduceOnly": false, + "side": "BUY", + "positionSide": "SHORT", + "status": "NEW", + "stopPrice": "9300", // please ignore when order type is TRAILING_STOP_MARKET + "closePosition": false, // if Close-All + "symbol": "BTCUSDT", + "time": 1579276756075, // order time + "timeInForce": "GTC", + "type": "TRAILING_STOP_MARKET", + "activatePrice": "9020", // activation price, only return with TRAILING_STOP_MARKET order + "priceRate": "0.3", // callback rate, only return with TRAILING_STOP_MARKET order + "updateTime": 1579276756075, // update time + "workingType": "CONTRACT_PRICE", + "priceProtect": false // if conditional order trigger is protected + } +] +``` + +`` +GET /fapi/v1/allOrders (HMAC SHA256) +`` + +Get all account orders; active, canceled, or filled. + +* These orders will not be found: + * order status is `CANCELED` or `EXPIRED`, **AND** + * order has NO filled trade, **AND** + * created time + 7 days < current time + +**Weight:** +5 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +orderId | LONG | NO | +startTime | LONG | NO | +endTime | LONG | NO | +limit | INT | NO | Default 500; max 1000. +recvWindow | LONG | NO | +timestamp | LONG | YES | + +**Notes:** + +* If `orderId` is set, it will get orders >= that `orderId`. Otherwise most recent orders are returned. +* The query time period must be less then 7 days( default as the recent 7 days). + + + +## Futures Account Balance V2 (USER_DATA) + +> **Response:** + +```javascript +[ + { + "accountAlias": "SgsR", // unique account code + "asset": "USDT", // asset name + "balance": "122607.35137903", // wallet balance + "crossWalletBalance": "23.72469206", // crossed wallet balance + "crossUnPnl": "0.00000000" // unrealized profit of crossed positions + "availableBalance": "23.72469206", // available balance + "maxWithdrawAmount": "23.72469206", // maximum amount for transfer out + "marginAvailable": true, // whether the asset can be used as margin in Multi-Assets mode + "updateTime": 1617939110373 + } +] +``` + +`` +GET /fapi/v2/balance (HMAC SHA256) +`` + +**Weight:** +5 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +recvWindow | LONG | NO | +timestamp | LONG | YES + + + + +## Account Information V4 (USER_DATA) + + +> **Response:** + +```javascript + +{ + "feeTier": 0, // account commisssion tier + "canTrade": true, // if can trade + "canDeposit": true, // if can transfer in asset + "canWithdraw": true, // if can transfer out asset + "updateTime": 0, + "totalInitialMargin": "0.00000000", // total initial margin required with current mark price (useless with isolated positions), only for USDT asset + "totalMaintMargin": "0.00000000", // total maintenance margin required, only for USDT asset + "totalWalletBalance": "23.72469206", // total wallet balance, using BidRate/AskRate for value caculation under multi-asset mode + "totalUnrealizedProfit": "0.00000000", // total unrealized profit in USDT + "totalMarginBalance": "23.72469206", // total margin balance, using BidRate/AskRate for value caculation under multi-asset mode + "totalPositionInitialMargin": "0.00000000", // initial margin required for positions with current mark price, only for USDT asset + "totalOpenOrderInitialMargin": "0.00000000", // initial margin required for open orders with current mark price, only for USDT asset + "totalCrossWalletBalance": "23.72469206", // crossed wallet balance, using BidRate/AskRate for value caculation under multi-asset mode + "totalCrossUnPnl": "0.00000000", // unrealized profit of crossed positions in USDT + "availableBalance": "23.72469206", // available balance, only for USDT asset + "maxWithdrawAmount": "23.72469206" // maximum amount for transfer out, using BidRate for value caculation under multi-asset mode + "assets": [ + { + "asset": "USDT", // asset name + "walletBalance": "23.72469206", // wallet balance + "unrealizedProfit": "0.00000000", // unrealized profit + "marginBalance": "23.72469206", // margin balance + "maintMargin": "0.00000000", // maintenance margin required + "initialMargin": "0.00000000", // total initial margin required with current mark price + "positionInitialMargin": "0.00000000", //initial margin required for positions with current mark price + "openOrderInitialMargin": "0.00000000", // initial margin required for open orders with current mark price + "crossWalletBalance": "23.72469206", // crossed wallet balance + "crossUnPnl": "0.00000000" // unrealized profit of crossed positions + "availableBalance": "23.72469206", // available balance + "maxWithdrawAmount": "23.72469206", // maximum amount for transfer out + "marginAvailable": true, // whether the asset can be used as margin in Multi-Assets mode + "updateTime": 1625474304765 // last update time + }, + { + "asset": "BUSD", // asset name + "walletBalance": "103.12345678", // wallet balance + "unrealizedProfit": "0.00000000", // unrealized profit + "marginBalance": "103.12345678", // margin balance + "maintMargin": "0.00000000", // maintenance margin required + "initialMargin": "0.00000000", // total initial margin required with current mark price + "positionInitialMargin": "0.00000000", //initial margin required for positions with current mark price + "openOrderInitialMargin": "0.00000000", // initial margin required for open orders with current mark price + "crossWalletBalance": "103.12345678", // crossed wallet balance + "crossUnPnl": "0.00000000" // unrealized profit of crossed positions + "availableBalance": "103.12345678", // available balance + "maxWithdrawAmount": "103.12345678", // maximum amount for transfer out + "marginAvailable": true, // whether the asset can be used as margin in Multi-Assets mode + "updateTime": 1625474304765 // last update time + } + ], + "positions": [ // positions of all symbols in the market are returned + // only "BOTH" positions will be returned with One-way mode + // only "LONG" and "SHORT" positions will be returned with Hedge mode + { + "symbol": "BTCUSDT", // symbol name + "initialMargin": "0", // initial margin required with current mark price + "maintMargin": "0", // maintenance margin required + "unrealizedProfit": "0.00000000", // unrealized profit + "positionInitialMargin": "0", // initial margin required for positions with current mark price + "openOrderInitialMargin": "0", // initial margin required for open orders with current mark price + "leverage": "100", // current initial leverage + "isolated": true, // if the position is isolated + "entryPrice": "0.00000", // average entry price + "maxNotional": "250000", // maximum available notional with current leverage + "positionSide": "BOTH", // position side + "positionAmt": "0", // position amount + "updateTime": 0 // last update time + } + ] +} +``` + + +`` +GET /fapi/v4/account (HMAC SHA256) +`` + +Get current account information. + +**Weight:** +5 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +recvWindow | LONG | NO | +timestamp | LONG | YES | + + + + + + + +## Change Initial Leverage (TRADE) + +> **Response:** + +```javascript +{ + "leverage": 21, + "maxNotionalValue": "1000000", + "symbol": "BTCUSDT" +} +``` + +`` +POST /fapi/v1/leverage (HMAC SHA256) +`` + +Change user's initial leverage of specific symbol market. + +**Weight:** +1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +leverage | INT | YES | target initial leverage: int from 1 to 125 +recvWindow | LONG | NO | +timestamp | LONG | YES | + + +## Change Margin Type (TRADE) + +> **Response:** + +```javascript +{ + "code": 200, + "msg": "success" +} +``` + +`` +POST /fapi/v1/marginType (HMAC SHA256) +`` + + +**Weight:** +1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES +marginType | ENUM | YES | ISOLATED, CROSSED +recvWindow | LONG | NO +timestamp | LONG | YES + + +## Modify Isolated Position Margin (TRADE) + +> **Response:** + +```javascript +{ + "amount": 100.0, + "code": 200, + "msg": "Successfully modify position margin.", + "type": 1 +} +``` + +`` +POST /fapi/v1/positionMargin (HMAC SHA256) +`` + +**Weight:** +1 + + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES +positionSide | ENUM | NO | Default `BOTH` for One-way Mode ; `LONG` or `SHORT` for Hedge Mode. It must be sent with Hedge Mode. +amount | DECIMAL | YES +type | INT | YES | 1: Add position margin,2: Reduce position margin +recvWindow | LONG | NO +timestamp | LONG | YES + +* Only for isolated symbol + + +## Get Position Margin Change History (TRADE) + +> **Response:** + +```javascript +[ + { + "amount": "23.36332311", + "asset": "USDT", + "symbol": "BTCUSDT", + "time": 1578047897183, + "type": 1, + "positionSide": "BOTH" + }, + { + "amount": "100", + "asset": "USDT", + "symbol": "BTCUSDT", + "time": 1578047900425, + "type": 1, + "positionSide": "LONG" + } +] +``` + +`` +GET /fapi/v1/positionMargin/history (HMAC SHA256) +`` + +**Weight:** +1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES +type | INT | NO | 1: Add position margin,2: Reduce position margin +startTime | LONG | NO +endTime | LONG | NO +limit | INT | NO | Default: 500 +recvWindow | LONG | NO +timestamp | LONG | YES + + + + + + +## Position Information V2 (USER_DATA) + + +> **Response:** + +> For One-way position mode: + +```javascript +[ + { + "entryPrice": "0.00000", + "marginType": "isolated", + "isAutoAddMargin": "false", + "isolatedMargin": "0.00000000", + "leverage": "10", + "liquidationPrice": "0", + "markPrice": "6679.50671178", + "maxNotionalValue": "20000000", + "positionAmt": "0.000", + "symbol": "BTCUSDT", + "unRealizedProfit": "0.00000000", + "positionSide": "BOTH", + "updateTime": 0 + } +] +``` + +> For Hedge position mode: + +```javascript +[ + { + "entryPrice": "6563.66500", + "marginType": "isolated", + "isAutoAddMargin": "false", + "isolatedMargin": "15517.54150468", + "leverage": "10", + "liquidationPrice": "5930.78", + "markPrice": "6679.50671178", + "maxNotionalValue": "20000000", + "positionAmt": "20.000", + "symbol": "BTCUSDT", + "unRealizedProfit": "2316.83423560" + "positionSide": "LONG", + "updateTime": 1625474304765 + }, + { + "entryPrice": "0.00000", + "marginType": "isolated", + "isAutoAddMargin": "false", + "isolatedMargin": "5413.95799991", + "leverage": "10", + "liquidationPrice": "7189.95", + "markPrice": "6679.50671178", + "maxNotionalValue": "20000000", + "positionAmt": "-10.000", + "symbol": "BTCUSDT", + "unRealizedProfit": "-1156.46711780" + "positionSide": "SHORT", + "updateTime": 0 + } +] +``` + +`` +GET /fapi/v2/positionRisk (HMAC SHA256) +`` + +Get current position information. + +**Weight:** +5 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | NO +recvWindow | LONG | NO | +timestamp | LONG | YES | + +**Note** +Please use with user data stream `ACCOUNT_UPDATE` to meet your timeliness and accuracy needs. + + + +## Account Trade List (USER_DATA) + + +> **Response:** + +```javascript +[ + { + "buyer": false, + "commission": "-0.07819010", + "commissionAsset": "USDT", + "id": 698759, + "maker": false, + "orderId": 25851813, + "price": "7819.01", + "qty": "0.002", + "quoteQty": "15.63802", + "realizedPnl": "-0.91539999", + "side": "SELL", + "positionSide": "SHORT", + "symbol": "BTCUSDT", + "time": 1569514978020 + } +] +``` + +`` +GET /fapi/v1/userTrades (HMAC SHA256) +`` + +Get trades for a specific account and symbol. + +**Weight:** +5 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES | +startTime | LONG | NO | +endTime | LONG | NO | +fromId | LONG | NO | Trade id to fetch from. Default gets most recent trades. +limit | INT | NO | Default 500; max 1000. +recvWindow | LONG | NO | +timestamp | LONG | YES | + +* If `startTime` and `endTime` are both not sent, then the last 7 days' data will be returned. +* The time between `startTime` and `endTime` cannot be longer than 7 days. +* The parameter `fromId` cannot be sent with `startTime` or `endTime`. + + +## Get Income History(USER_DATA) + + +> **Response:** + +```javascript +[ + { + "symbol": "", // trade symbol, if existing + "incomeType": "TRANSFER", // income type + "income": "-0.37500000", // income amount + "asset": "USDT", // income asset + "info":"TRANSFER", // extra information + "time": 1570608000000, + "tranId":"9689322392", // transaction id + "tradeId":"" // trade id, if existing + }, + { + "symbol": "BTCUSDT", + "incomeType": "COMMISSION", + "income": "-0.01000000", + "asset": "USDT", + "info":"COMMISSION", + "time": 1570636800000, + "tranId":"9689322392", + "tradeId":"2059192" + } +] +``` + +`` +GET /fapi/v1/income (HMAC SHA256) +`` + +**Weight:** +30 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | NO| +incomeType | STRING | NO | "TRANSFER","WELCOME_BONUS", "REALIZED_PNL","FUNDING_FEE", "COMMISSION", "INSURANCE_CLEAR", and "MARKET_MERCHANT_RETURN_REWARD" +startTime | LONG | NO | Timestamp in ms to get funding from INCLUSIVE. +endTime | LONG | NO | Timestamp in ms to get funding until INCLUSIVE. +limit | INT | NO | Default 100; max 1000 +recvWindow|LONG|NO| +timestamp|LONG|YES| + +* If neither `startTime` nor `endTime` is sent, the recent 7-day data will be returned. +* If `incomeType ` is not sent, all kinds of flow will be returned +* "trandId" is unique in the same incomeType for a user + + +## Notional and Leverage Brackets (USER_DATA) + + +> **Response:** + +```javascript +[ + { + "symbol": "ETHUSDT", + "brackets": [ + { + "bracket": 1, // Notional bracket + "initialLeverage": 75, // Max initial leverage for this bracket + "notionalCap": 10000, // Cap notional of this bracket + "notionalFloor": 0, // Notional threshold of this bracket + "maintMarginRatio": 0.0065, // Maintenance ratio for this bracket + "cum":0 // Auxiliary number for quick calculation + + }, + ] + } +] +``` + +> **OR** (if symbol sent) + +```javascript + +{ + "symbol": "ETHUSDT", + "brackets": [ + { + "bracket": 1, + "initialLeverage": 75, + "notionalCap": 10000, + "notionalFloor": 0, + "maintMarginRatio": 0.0065, + "cum":0 + }, + ] +} +``` + + +`` +GET /fapi/v1/leverageBracket +`` + + +**Weight:** 1 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | NO +recvWindow|LONG|NO| +timestamp|LONG|YES| + + + +## Position ADL Quantile Estimation (USER_DATA) + + +> **Response:** + +```javascript +[ + { + "symbol": "ETHUSDT", + "adlQuantile": + { + // if the positions of the symbol are crossed margined in Hedge Mode, "LONG" and "SHORT" will be returned a same quantile value, and "HEDGE" will be returned instead of "BOTH". + "LONG": 3, + "SHORT": 3, + "HEDGE": 0 // only a sign, ignore the value + } + }, + { + "symbol": "BTCUSDT", + "adlQuantile": + { + // for positions of the symbol are in One-way Mode or isolated margined in Hedge Mode + "LONG": 1, // adl quantile for "LONG" position in hedge mode + "SHORT": 2, // adl qauntile for "SHORT" position in hedge mode + "BOTH": 0 // adl qunatile for position in one-way mode + } + } + ] +``` + +`` +GET /fapi/v1/adlQuantile +`` + + +**Weight:** 5 + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | NO +recvWindow|LONG|NO| +timestamp|LONG|YES| + +* Values update every 30s. + +* Values 0, 1, 2, 3, 4 shows the queue position and possibility of ADL from low to high. + +* For positions of the symbol are in One-way Mode or isolated margined in Hedge Mode, "LONG", "SHORT", and "BOTH" will be returned to show the positions' adl quantiles of different position sides. + +* If the positions of the symbol are crossed margined in Hedge Mode: + * "HEDGE" as a sign will be returned instead of "BOTH"; + * A same value caculated on unrealized pnls on long and short sides' positions will be shown for "LONG" and "SHORT" when there are positions in both of long and short sides. + + + +## User's Force Orders (USER_DATA) + + +> **Response:** + +```javascript +[ + { + "orderId": 6071832819, + "symbol": "BTCUSDT", + "status": "FILLED", + "clientOrderId": "autoclose-1596107620040000020", + "price": "10871.09", + "avgPrice": "10913.21000", + "origQty": "0.001", + "executedQty": "0.001", + "cumQuote": "10.91321", + "timeInForce": "IOC", + "type": "LIMIT", + "reduceOnly": false, + "closePosition": false, + "side": "SELL", + "positionSide": "BOTH", + "stopPrice": "0", + "workingType": "CONTRACT_PRICE", + "origType": "LIMIT", + "time": 1596107620044, + "updateTime": 1596107620087 + } + { + "orderId": 6072734303, + "symbol": "BTCUSDT", + "status": "FILLED", + "clientOrderId": "adl_autoclose", + "price": "11023.14", + "avgPrice": "10979.82000", + "origQty": "0.001", + "executedQty": "0.001", + "cumQuote": "10.97982", + "timeInForce": "GTC", + "type": "LIMIT", + "reduceOnly": false, + "closePosition": false, + "side": "BUY", + "positionSide": "SHORT", + "stopPrice": "0", + "workingType": "CONTRACT_PRICE", + "origType": "LIMIT", + "time": 1596110725059, + "updateTime": 1596110725071 + } +] +``` + + +`` +GET /fapi/v1/forceOrders +`` + + +**Weight:** 20 with symbol, 50 without symbol + +**Parameters:** + + Name | Type | Mandatory | Description +------------- | ------ | --------- | ----------------------------------------------------------- +symbol | STRING | NO | +autoCloseType | ENUM | NO | "LIQUIDATION" for liquidation orders, "ADL" for ADL orders. +startTime | LONG | NO | +endTime | LONG | NO | +limit | INT | NO | Default 50; max 100. +recvWindow | LONG | NO | +timestamp | LONG | YES | + +* If "autoCloseType" is not sent, orders with both of the types will be returned +* If "startTime" is not sent, data within 7 days before "endTime" can be queried + + + +## User Commission Rate (USER_DATA) + +> **Response:** + +```javascript +{ + "symbol": "BTCUSDT", + "makerCommissionRate": "0.0002", // 0.02% + "takerCommissionRate": "0.0004" // 0.04% +} +``` + +`` +GET /fapi/v1/commissionRate (HMAC SHA256) +`` + +**Weight:** +20 + + +**Parameters:** + +Name | Type | Mandatory | Description +------------ | ------------ | ------------ | ------------ +symbol | STRING | YES +recvWindow | LONG | NO +timestamp | LONG | YES + + + + + +# User Data Streams + +* The base API endpoint is: **https://fapi.asterdex.com** +* A User Data Stream `listenKey` is valid for 60 minutes after creation. +* Doing a `PUT` on a `listenKey` will extend its validity for 60 minutes. +* Doing a `DELETE` on a `listenKey` will close the stream and invalidate the `listenKey`. +* Doing a `POST` on an account with an active `listenKey` will return the currently active `listenKey` and extend its validity for 60 minutes. +* The baseurl for websocket is **wss://fstream.asterdex.com** +* User Data Streams are accessed at **/ws/\** +* User data stream payloads are **not guaranteed** to be in order during heavy periods; **make sure to order your updates using E** +* A single connection to **fstream.asterdex.com** is only valid for 24 hours; expect to be disconnected at the 24 hour mark + + +## Start User Data Stream (USER_STREAM) + + +> **Response:** + +```javascript +{ + "listenKey": "pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a65a1" +} +``` + +`` +POST /fapi/v1/listenKey +`` + +Start a new user data stream. The stream will close after 60 minutes unless a keepalive is sent. If the account has an active `listenKey`, that `listenKey` will be returned and its validity will be extended for 60 minutes. + +**Weight:** +1 + +**Parameters:** + +None + + + +## Keepalive User Data Stream (USER_STREAM) + +> **Response:** + +```javascript +{} +``` + +`` +PUT /fapi/v1/listenKey +`` + +Keepalive a user data stream to prevent a time out. User data streams will close after 60 minutes. It's recommended to send a ping about every 60 minutes. + +**Weight:** +1 + +**Parameters:** + +None + + + +## Close User Data Stream (USER_STREAM) + + +> **Response:** + +```javascript +{} +``` + +`` +DELETE /fapi/v1/listenKey +`` + +Close out a user data stream. + +**Weight:** +1 + +**Parameters:** + +None + + +## Event: User Data Stream Expired + +> **Payload:** + +```javascript +{ + 'e': 'listenKeyExpired', // event type + 'E': 1576653824250 // event time +} +``` + +When the `listenKey` used for the user data stream turns expired, this event will be pushed. + +**Notice:** + +* This event is not related to the websocket disconnection. +* This event will be received only when a valid `listenKey` in connection got expired. +* No more user data event will be updated after this event received until a new valid `listenKey` used. + + + + + +## Event: Margin Call + +> **Payload:** + +```javascript +{ + "e":"MARGIN_CALL", // Event Type + "E":1587727187525, // Event Time + "cw":"3.16812045", // Cross Wallet Balance. Only pushed with crossed position margin call + "p":[ // Position(s) of Margin Call + { + "s":"ETHUSDT", // Symbol + "ps":"LONG", // Position Side + "pa":"1.327", // Position Amount + "mt":"CROSSED", // Margin Type + "iw":"0", // Isolated Wallet (if isolated position) + "mp":"187.17127", // Mark Price + "up":"-1.166074", // Unrealized PnL + "mm":"1.614445" // Maintenance Margin Required + } + ] +} + +``` + + +* When the user's position risk ratio is too high, this stream will be pushed. +* This message is only used as risk guidance information and is not recommended for investment strategies. +* In the case of a highly volatile market, there may be the possibility that the user's position has been liquidated at the same time when this stream is pushed out. + + + + + +## Event: Balance and Position Update + + +> **Payload:** + +```javascript +{ + "e": "ACCOUNT_UPDATE", // Event Type + "E": 1564745798939, // Event Time + "T": 1564745798938 , // Transaction + "a": // Update Data + { + "m":"ORDER", // Event reason type + "B":[ // Balances + { + "a":"USDT", // Asset + "wb":"122624.12345678", // Wallet Balance + "cw":"100.12345678", // Cross Wallet Balance + "bc":"50.12345678" // Balance Change except PnL and Commission + }, + { + "a":"BUSD", + "wb":"1.00000000", + "cw":"0.00000000", + "bc":"-49.12345678" + } + ], + "P":[ + { + "s":"BTCUSDT", // Symbol + "pa":"0", // Position Amount + "ep":"0.00000", // Entry Price + "cr":"200", // (Pre-fee) Accumulated Realized + "up":"0", // Unrealized PnL + "mt":"isolated", // Margin Type + "iw":"0.00000000", // Isolated Wallet (if isolated position) + "ps":"BOTH" // Position Side + }, + { + "s":"BTCUSDT", + "pa":"20", + "ep":"6563.66500", + "cr":"0", + "up":"2850.21200", + "mt":"isolated", + "iw":"13200.70726908", + "ps":"LONG" + }, + { + "s":"BTCUSDT", + "pa":"-10", + "ep":"6563.86000", + "cr":"-45.04000000", + "up":"-1423.15600", + "mt":"isolated", + "iw":"6570.42511771", + "ps":"SHORT" + } + ] + } +} +``` + +Event type is `ACCOUNT_UPDATE`. + +* When balance or position get updated, this event will be pushed. + * `ACCOUNT_UPDATE` will be pushed only when update happens on user's account, including changes on balances, positions, or margin type. + * Unfilled orders or cancelled orders will not make the event `ACCOUNT_UPDATE` pushed, since there's no change on positions. + * Only positions of symbols with non-zero isolatd wallet or non-zero position amount will be pushed in the "position" part of the event `ACCOUNT_UPDATE` when any position changes. + +* When "FUNDING FEE" changes to the user's balance, the event will be pushed with the brief message: + * When "FUNDING FEE" occurs in a **crossed position**, `ACCOUNT_UPDATE` will be pushed with only the balance `B`(including the "FUNDING FEE" asset only), without any position `P` message. + * When "FUNDING FEE" occurs in an **isolated position**, `ACCOUNT_UPDATE` will be pushed with only the balance `B`(including the "FUNDING FEE" asset only) and the relative position message `P`( including the isolated position on which the "FUNDING FEE" occurs only, without any other position message). + +* The field "m" represents the reason type for the event and may shows the following possible types: + * DEPOSIT + * WITHDRAW + * ORDER + * FUNDING_FEE + * WITHDRAW_REJECT + * ADJUSTMENT + * INSURANCE_CLEAR + * ADMIN_DEPOSIT + * ADMIN_WITHDRAW + * MARGIN_TRANSFER + * MARGIN_TYPE_CHANGE + * ASSET_TRANSFER + * OPTIONS_PREMIUM_FEE + * OPTIONS_SETTLE_PROFIT + * AUTO_EXCHANGE + +* The field "bc" represents the balance change except for PnL and commission. + +## Event: Order Update + + +> **Payload:** + +```javascript +{ + + "e":"ORDER_TRADE_UPDATE", // Event Type + "E":1568879465651, // Event Time + "T":1568879465650, // Transaction Time + "o":{ + "s":"BTCUSDT", // Symbol + "c":"TEST", // Client Order Id + // special client order id: + // starts with "autoclose-": liquidation order + // "adl_autoclose": ADL auto close order + "S":"SELL", // Side + "o":"TRAILING_STOP_MARKET", // Order Type + "f":"GTC", // Time in Force + "q":"0.001", // Original Quantity + "p":"0", // Original Price + "ap":"0", // Average Price + "sp":"7103.04", // Stop Price. Please ignore with TRAILING_STOP_MARKET order + "x":"NEW", // Execution Type + "X":"NEW", // Order Status + "i":8886774, // Order Id + "l":"0", // Order Last Filled Quantity + "z":"0", // Order Filled Accumulated Quantity + "L":"0", // Last Filled Price + "N":"USDT", // Commission Asset, will not push if no commission + "n":"0", // Commission, will not push if no commission + "T":1568879465651, // Order Trade Time + "t":0, // Trade Id + "b":"0", // Bids Notional + "a":"9.91", // Ask Notional + "m":false, // Is this trade the maker side? + "R":false, // Is this reduce only + "wt":"CONTRACT_PRICE", // Stop Price Working Type + "ot":"TRAILING_STOP_MARKET", // Original Order Type + "ps":"LONG", // Position Side + "cp":false, // If Close-All, pushed with conditional order + "AP":"7476.89", // Activation Price, only puhed with TRAILING_STOP_MARKET order + "cr":"5.0", // Callback Rate, only puhed with TRAILING_STOP_MARKET order + "rp":"0" // Realized Profit of the trade + } + +} +``` + + +When new order created, order status changed will push such event. +event type is `ORDER_TRADE_UPDATE`. + + + + + +**Side** + +* BUY +* SELL + +**Order Type** + +* MARKET +* LIMIT +* STOP +* TAKE_PROFIT +* LIQUIDATION + +**Execution Type** + +* NEW +* CANCELED +* CALCULATED - Liquidation Execution +* EXPIRED +* TRADE + +**Order Status** + +* NEW +* PARTIALLY_FILLED +* FILLED +* CANCELED +* EXPIRED +* NEW_INSURANCE - Liquidation with Insurance Fund +* NEW_ADL - Counterparty Liquidation` + +**Time in force** + +* GTC +* IOC +* FOK +* GTX +* HIDDEN + +**Working Type** + +* MARK_PRICE +* CONTRACT_PRICE + + + +## Event: Account Configuration Update previous Leverage Update + +> **Payload:** + +```javascript +{ + "e":"ACCOUNT_CONFIG_UPDATE", // Event Type + "E":1611646737479, // Event Time + "T":1611646737476, // Transaction Time + "ac":{ + "s":"BTCUSDT", // symbol + "l":25 // leverage + + } +} + +``` + +> **Or** + +```javascript +{ + "e":"ACCOUNT_CONFIG_UPDATE", // Event Type + "E":1611646737479, // Event Time + "T":1611646737476, // Transaction Time + "ai":{ // User's Account Configuration + "j":true // Multi-Assets Mode + } +} +``` + +When the account configuration is changed, the event type will be pushed as `ACCOUNT_CONFIG_UPDATE` + +When the leverage of a trade pair changes, the payload will contain the object `ac` to represent the account configuration of the trade pair, where `s` represents the specific trade pair and `l` represents the leverage + +When the user Multi-Assets margin mode changes the payload will contain the object `ai` representing the user account configuration, where `j` represents the user Multi-Assets margin mode + + + +# Error Codes + +> Here is the error JSON payload: + +```javascript +{ + "code":-1121, + "msg":"Invalid symbol." +} +``` + +Errors consist of two parts: an error code and a message. +Codes are universal,but messages can vary. + + + +## 10xx - General Server or Network issues +> -1000 UNKNOWN + * An unknown error occured while processing the request. + +> -1001 DISCONNECTED + * Internal error; unable to process your request. Please try again. + +> -1002 UNAUTHORIZED + * You are not authorized to execute this request. + +> -1003 TOO_MANY_REQUESTS + * Too many requests queued. + * Too many requests; please use the websocket for live updates. + * Too many requests; current limit is %s requests per minute. Please use the websocket for live updates to avoid polling the API. + * Way too many requests; IP banned until %s. Please use the websocket for live updates to avoid bans. + +> -1004 DUPLICATE_IP + * This IP is already on the white list + +> -1005 NO_SUCH_IP + * No such IP has been white listed + +> -1006 UNEXPECTED_RESP + * An unexpected response was received from the message bus. Execution status unknown. + +> -1007 TIMEOUT + * Timeout waiting for response from backend server. Send status unknown; execution status unknown. + +> -1010 ERROR_MSG_RECEIVED + * ERROR_MSG_RECEIVED. + +> -1011 NON_WHITE_LIST + * This IP cannot access this route. + +> -1013 INVALID_MESSAGE +* INVALID_MESSAGE. + +> -1014 UNKNOWN_ORDER_COMPOSITION + * Unsupported order combination. + +> -1015 TOO_MANY_ORDERS + * Too many new orders. + * Too many new orders; current limit is %s orders per %s. + +> -1016 SERVICE_SHUTTING_DOWN + * This service is no longer available. + +> -1020 UNSUPPORTED_OPERATION + * This operation is not supported. + +> -1021 INVALID_TIMESTAMP + * Timestamp for this request is outside of the recvWindow. + * Timestamp for this request was 1000ms ahead of the server's time. + +> -1022 INVALID_SIGNATURE + * Signature for this request is not valid. + +> -1023 START_TIME_GREATER_THAN_END_TIME + * Start time is greater than end time. + + +## 11xx - Request issues +> -1100 ILLEGAL_CHARS + * Illegal characters found in a parameter. + * Illegal characters found in parameter '%s'; legal range is '%s'. + +> -1101 TOO_MANY_PARAMETERS + * Too many parameters sent for this endpoint. + * Too many parameters; expected '%s' and received '%s'. + * Duplicate values for a parameter detected. + +> -1102 MANDATORY_PARAM_EMPTY_OR_MALFORMED + * A mandatory parameter was not sent, was empty/null, or malformed. + * Mandatory parameter '%s' was not sent, was empty/null, or malformed. + * Param '%s' or '%s' must be sent, but both were empty/null! + +> -1103 UNKNOWN_PARAM + * An unknown parameter was sent. + +> -1104 UNREAD_PARAMETERS + * Not all sent parameters were read. + * Not all sent parameters were read; read '%s' parameter(s) but was sent '%s'. + +> -1105 PARAM_EMPTY + * A parameter was empty. + * Parameter '%s' was empty. + +> -1106 PARAM_NOT_REQUIRED + * A parameter was sent when not required. + * Parameter '%s' sent when not required. + +> -1108 BAD_ASSET + * Invalid asset. + +> -1109 BAD_ACCOUNT + * Invalid account. + +> -1110 BAD_INSTRUMENT_TYPE + * Invalid symbolType. + +> -1111 BAD_PRECISION + * Precision is over the maximum defined for this asset. + +> -1112 NO_DEPTH + * No orders on book for symbol. + +> -1113 WITHDRAW_NOT_NEGATIVE + * Withdrawal amount must be negative. + +> -1114 TIF_NOT_REQUIRED + * TimeInForce parameter sent when not required. + +> -1115 INVALID_TIF + * Invalid timeInForce. + +> -1116 INVALID_ORDER_TYPE + * Invalid orderType. + +> -1117 INVALID_SIDE + * Invalid side. + +> -1118 EMPTY_NEW_CL_ORD_ID + * New client order ID was empty. + +> -1119 EMPTY_ORG_CL_ORD_ID + * Original client order ID was empty. + +> -1120 BAD_INTERVAL + * Invalid interval. + +> -1121 BAD_SYMBOL + * Invalid symbol. + +> -1125 INVALID_LISTEN_KEY + * This listenKey does not exist. + +> -1127 MORE_THAN_XX_HOURS + * Lookup interval is too big. + * More than %s hours between startTime and endTime. + +> -1128 OPTIONAL_PARAMS_BAD_COMBO + * Combination of optional parameters invalid. + +> -1130 INVALID_PARAMETER + * Invalid data sent for a parameter. + * Data sent for parameter '%s' is not valid. + +> -1136 INVALID_NEW_ORDER_RESP_TYPE + * Invalid newOrderRespType. + + +## 20xx - Processing Issues + +> -2010 NEW_ORDER_REJECTED + * NEW_ORDER_REJECTED + +> -2011 CANCEL_REJECTED + * CANCEL_REJECTED + +> -2013 NO_SUCH_ORDER + * Order does not exist. + +> -2014 BAD_API_KEY_FMT + * API-key format invalid. + +> -2015 REJECTED_MBX_KEY + * Invalid API-key, IP, or permissions for action. + +> -2016 NO_TRADING_WINDOW + * No trading window could be found for the symbol. Try ticker/24hrs instead. + +> -2018 BALANCE_NOT_SUFFICIENT + * Balance is insufficient. + +> -2019 MARGIN_NOT_SUFFICIEN + * Margin is insufficient. + +> -2020 UNABLE_TO_FILL + * Unable to fill. + +> -2021 ORDER_WOULD_IMMEDIATELY_TRIGGER + * Order would immediately trigger. + +> -2022 REDUCE_ONLY_REJECT + * ReduceOnly Order is rejected. + +> -2023 USER_IN_LIQUIDATION + * User in liquidation mode now. + +> -2024 POSITION_NOT_SUFFICIENT + * Position is not sufficient. + +> -2025 MAX_OPEN_ORDER_EXCEEDED + * Reach max open order limit. + +> -2026 REDUCE_ONLY_ORDER_TYPE_NOT_SUPPORTED + * This OrderType is not supported when reduceOnly. + +> -2027 MAX_LEVERAGE_RATIO + * Exceeded the maximum allowable position at current leverage. + + +> -2028 MIN_LEVERAGE_RATIO + * Leverage is smaller than permitted: insufficient margin balance. + + +## 40xx - Filters and other Issues +> -4000 INVALID_ORDER_STATUS + * Invalid order status. + +> -4001 PRICE_LESS_THAN_ZERO + * Price less than 0. + +> -4002 PRICE_GREATER_THAN_MAX_PRICE + * Price greater than max price. + +> -4003 QTY_LESS_THAN_ZERO + * Quantity less than zero. + +> -4004 QTY_LESS_THAN_MIN_QTY + * Quantity less than min quantity. + +> -4005 QTY_GREATER_THAN_MAX_QTY + * Quantity greater than max quantity. + +> -4006 STOP_PRICE_LESS_THAN_ZERO + * Stop price less than zero. + +> -4007 STOP_PRICE_GREATER_THAN_MAX_PRICE + * Stop price greater than max price. + +> -4008 TICK_SIZE_LESS_THAN_ZERO + * Tick size less than zero. + +> -4009 MAX_PRICE_LESS_THAN_MIN_PRICE + * Max price less than min price. + +> -4010 MAX_QTY_LESS_THAN_MIN_QTY + * Max qty less than min qty. + +> -4011 STEP_SIZE_LESS_THAN_ZERO + * Step size less than zero. + +> -4012 MAX_NUM_ORDERS_LESS_THAN_ZERO + * Max mum orders less than zero. + +> -4013 PRICE_LESS_THAN_MIN_PRICE + * Price less than min price. + +> -4014 PRICE_NOT_INCREASED_BY_TICK_SIZE + * Price not increased by tick size. + +> -4015 INVALID_CL_ORD_ID_LEN + * Client order id is not valid. + * Client order id length should not be more than 36 chars + +> -4016 PRICE_HIGHTER_THAN_MULTIPLIER_UP + * Price is higher than mark price multiplier cap. + +> -4017 MULTIPLIER_UP_LESS_THAN_ZERO + * Multiplier up less than zero. + +> -4018 MULTIPLIER_DOWN_LESS_THAN_ZERO + * Multiplier down less than zero. + +> -4019 COMPOSITE_SCALE_OVERFLOW + * Composite scale too large. + +> -4020 TARGET_STRATEGY_INVALID + * Target strategy invalid for orderType '%s',reduceOnly '%b'. + +> -4021 INVALID_DEPTH_LIMIT + * Invalid depth limit. + * '%s' is not valid depth limit. + +> -4022 WRONG_MARKET_STATUS + * market status sent is not valid. + +> -4023 QTY_NOT_INCREASED_BY_STEP_SIZE + * Qty not increased by step size. + +> -4024 PRICE_LOWER_THAN_MULTIPLIER_DOWN + * Price is lower than mark price multiplier floor. + +> -4025 MULTIPLIER_DECIMAL_LESS_THAN_ZERO + * Multiplier decimal less than zero. + +> -4026 COMMISSION_INVALID + * Commission invalid. + * `%s` less than zero. + * `%s` absolute value greater than `%s` + +> -4027 INVALID_ACCOUNT_TYPE + * Invalid account type. + +> -4028 INVALID_LEVERAGE + * Invalid leverage + * Leverage `%s` is not valid + * Leverage `%s` already exist with `%s` + +> -4029 INVALID_TICK_SIZE_PRECISION + * Tick size precision is invalid. + +> -4030 INVALID_STEP_SIZE_PRECISION + * Step size precision is invalid. + +> -4031 INVALID_WORKING_TYPE + * Invalid parameter working type + * Invalid parameter working type: `%s` + +> -4032 EXCEED_MAX_CANCEL_ORDER_SIZE + * Exceed maximum cancel order size. + * Invalid parameter working type: `%s` + +> -4033 INSURANCE_ACCOUNT_NOT_FOUND + * Insurance account not found. + +> -4044 INVALID_BALANCE_TYPE + * Balance Type is invalid. + +> -4045 MAX_STOP_ORDER_EXCEEDED + * Reach max stop order limit. + +> -4046 NO_NEED_TO_CHANGE_MARGIN_TYPE + * No need to change margin type. + +> -4047 THERE_EXISTS_OPEN_ORDERS + * Margin type cannot be changed if there exists open orders. + +> -4048 THERE_EXISTS_QUANTITY + * Margin type cannot be changed if there exists position. + +> -4049 ADD_ISOLATED_MARGIN_REJECT + * Add margin only support for isolated position. + +> -4050 CROSS_BALANCE_INSUFFICIENT + * Cross balance insufficient. + +> -4051 ISOLATED_BALANCE_INSUFFICIENT + * Isolated balance insufficient. + +> -4052 NO_NEED_TO_CHANGE_AUTO_ADD_MARGIN + * No need to change auto add margin. + +> -4053 AUTO_ADD_CROSSED_MARGIN_REJECT + * Auto add margin only support for isolated position. + +> -4054 ADD_ISOLATED_MARGIN_NO_POSITION_REJECT + * Cannot add position margin: position is 0. + +> -4055 AMOUNT_MUST_BE_POSITIVE + * Amount must be positive. + +> -4056 INVALID_API_KEY_TYPE + * Invalid api key type. + +> -4057 INVALID_RSA_PUBLIC_KEY + * Invalid api public key + +> -4058 MAX_PRICE_TOO_LARGE + * maxPrice and priceDecimal too large,please check. + +> -4059 NO_NEED_TO_CHANGE_POSITION_SIDE + * No need to change position side. + +> -4060 INVALID_POSITION_SIDE + * Invalid position side. + +> -4061 POSITION_SIDE_NOT_MATCH + * Order's position side does not match user's setting. + +> -4062 REDUCE_ONLY_CONFLICT + * Invalid or improper reduceOnly value. + +> -4063 INVALID_OPTIONS_REQUEST_TYPE + * Invalid options request type + +> -4064 INVALID_OPTIONS_TIME_FRAME + * Invalid options time frame + +> -4065 INVALID_OPTIONS_AMOUNT + * Invalid options amount + +> -4066 INVALID_OPTIONS_EVENT_TYPE + * Invalid options event type + +> -4067 POSITION_SIDE_CHANGE_EXISTS_OPEN_ORDERS + * Position side cannot be changed if there exists open orders. + +> -4068 POSITION_SIDE_CHANGE_EXISTS_QUANTITY + * Position side cannot be changed if there exists position. + +> -4069 INVALID_OPTIONS_PREMIUM_FEE + * Invalid options premium fee + +> -4070 INVALID_CL_OPTIONS_ID_LEN + * Client options id is not valid. + * Client options id length should be less than 32 chars + +> -4071 INVALID_OPTIONS_DIRECTION + * Invalid options direction + +> -4072 OPTIONS_PREMIUM_NOT_UPDATE + * premium fee is not updated, reject order + +> -4073 OPTIONS_PREMIUM_INPUT_LESS_THAN_ZERO + * input premium fee is less than 0, reject order + +> -4074 OPTIONS_AMOUNT_BIGGER_THAN_UPPER + * Order amount is bigger than upper boundary or less than 0, reject order + +> -4075 OPTIONS_PREMIUM_OUTPUT_ZERO + * output premium fee is less than 0, reject order + +> -4076 OPTIONS_PREMIUM_TOO_DIFF + * original fee is too much higher than last fee + +> -4077 OPTIONS_PREMIUM_REACH_LIMIT + * place order amount has reached to limit, reject order + +> -4078 OPTIONS_COMMON_ERROR + * options internal error + +> -4079 INVALID_OPTIONS_ID + * invalid options id + * invalid options id: %s + * duplicate options id %d for user %d + +> -4080 OPTIONS_USER_NOT_FOUND + * user not found + * user not found with id: %s + +> -4081 OPTIONS_NOT_FOUND + * options not found + * options not found with id: %s + +> -4082 INVALID_BATCH_PLACE_ORDER_SIZE + * Invalid number of batch place orders. + * Invalid number of batch place orders: %s + +> -4083 PLACE_BATCH_ORDERS_FAIL + * Fail to place batch orders. + +> -4084 UPCOMING_METHOD + * Method is not allowed currently. Upcoming soon. + +> -4085 INVALID_NOTIONAL_LIMIT_COEF + * Invalid notional limit coefficient + +> -4086 INVALID_PRICE_SPREAD_THRESHOLD + * Invalid price spread threshold + +> -4087 REDUCE_ONLY_ORDER_PERMISSION + * User can only place reduce only order + +> -4088 NO_PLACE_ORDER_PERMISSION + * User can not place order currently + +> -4104 INVALID_CONTRACT_TYPE + * Invalid contract type + +> -4114 INVALID_CLIENT_TRAN_ID_LEN + * clientTranId is not valid + * Client tran id length should be less than 64 chars + +> -4115 DUPLICATED_CLIENT_TRAN_ID + * clientTranId is duplicated + * Client tran id should be unique within 7 days + +> -4118 REDUCE_ONLY_MARGIN_CHECK_FAILED + * ReduceOnly Order Failed. Please check your existing position and open orders + +> -4131 MARKET_ORDER_REJECT + * The counterparty's best price does not meet the PERCENT_PRICE filter limit + +> -4135 INVALID_ACTIVATION_PRICE + * Invalid activation price + +> -4137 QUANTITY_EXISTS_WITH_CLOSE_POSITION + * Quantity must be zero with closePosition equals true + +> -4138 REDUCE_ONLY_MUST_BE_TRUE + * Reduce only must be true with closePosition equals true + +> -4139 ORDER_TYPE_CANNOT_BE_MKT + * Order type can not be market if it's unable to cancel + +> -4140 INVALID_OPENING_POSITION_STATUS + * Invalid symbol status for opening position + +> -4141 SYMBOL_ALREADY_CLOSED + * Symbol is closed + +> -4142 STRATEGY_INVALID_TRIGGER_PRICE + * REJECT: take profit or stop order will be triggered immediately + +> -4144 INVALID_PAIR + * Invalid pair + +> -4161 ISOLATED_LEVERAGE_REJECT_WITH_POSITION + * Leverage reduction is not supported in Isolated Margin Mode with open positions + +> -4164 MIN_NOTIONAL + * Order's notional must be no smaller than 5.0 (unless you choose reduce only) + * Order's notional must be no smaller than %s (unless you choose reduce only) + +> -4165 INVALID_TIME_INTERVAL + * Invalid time interval + * Maximum time interval is %s days + +> -4183 PRICE_HIGHTER_THAN_STOP_MULTIPLIER_UP + * Price is higher than stop price multiplier cap. + * Limit price can't be higher than %s. + +> -4184 PRICE_LOWER_THAN_STOP_MULTIPLIER_DOWN + * Price is lower than stop price multiplier floor. + * Limit price can't be lower than %s. \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..4183587 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +import "./src/index.tsx"; diff --git a/package.json b/package.json new file mode 100644 index 0000000..82a8705 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "ritmex-bot", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest", + "vitest": "^3.2.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "ccxt": "^4.5.5", + "dotenv": "^17.2.2", + "ink": "^6.3.1", + "react": "^19.1.1", + "ws": "^8.18.3" + } +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..85f6ea8 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,53 @@ +export interface TradingConfig { + symbol: string; + tradeAmount: number; + lossLimit: number; + trailingProfit: number; + trailingCallbackRate: number; + profitLockTriggerUsd: number; + profitLockOffsetUsd: number; + pollIntervalMs: number; + maxLogEntries: number; + klineInterval: string; +} + +function parseNumber(value: string | undefined, fallback: number): number { + if (!value) return fallback; + const next = Number(value); + return Number.isFinite(next) ? next : fallback; +} + +export const tradingConfig: TradingConfig = { + symbol: process.env.TRADE_SYMBOL ?? "BTCUSDT", + tradeAmount: parseNumber(process.env.TRADE_AMOUNT, 0.001), + lossLimit: parseNumber(process.env.LOSS_LIMIT, 0.03), + trailingProfit: parseNumber(process.env.TRAILING_PROFIT, 0.2), + trailingCallbackRate: parseNumber(process.env.TRAILING_CALLBACK_RATE, 0.2), + profitLockTriggerUsd: parseNumber(process.env.PROFIT_LOCK_TRIGGER_USD, 0.1), + profitLockOffsetUsd: parseNumber(process.env.PROFIT_LOCK_OFFSET_USD, 0.05), + pollIntervalMs: parseNumber(process.env.POLL_INTERVAL_MS, 500), + maxLogEntries: parseNumber(process.env.MAX_LOG_ENTRIES, 200), + klineInterval: process.env.KLINE_INTERVAL ?? "1m", +}; + +export interface MakerConfig { + symbol: string; + tradeAmount: number; + lossLimit: number; + priceChaseThreshold: number; + bidOffset: number; + askOffset: number; + refreshIntervalMs: number; + maxLogEntries: number; +} + +export const makerConfig: MakerConfig = { + symbol: process.env.TRADE_SYMBOL ?? "BTCUSDT", + tradeAmount: parseNumber(process.env.TRADE_AMOUNT, 0.001), + lossLimit: parseNumber(process.env.MAKER_LOSS_LIMIT, parseNumber(process.env.LOSS_LIMIT, 0.5)), + priceChaseThreshold: parseNumber(process.env.MAKER_PRICE_CHASE, 0.5), + bidOffset: parseNumber(process.env.MAKER_BID_OFFSET, 0), + askOffset: parseNumber(process.env.MAKER_ASK_OFFSET, 0), + refreshIntervalMs: parseNumber(process.env.MAKER_REFRESH_INTERVAL_MS, 1500), + maxLogEntries: parseNumber(process.env.MAKER_MAX_LOG_ENTRIES, 200), +}; diff --git a/src/core/maker-engine.ts b/src/core/maker-engine.ts new file mode 100644 index 0000000..e9c78e3 --- /dev/null +++ b/src/core/maker-engine.ts @@ -0,0 +1,333 @@ +import type { MakerConfig } from "../config"; +import type { ExchangeAdapter } from "../exchanges/adapter"; +import type { + AsterAccountSnapshot, + AsterDepth, + AsterOrder, + AsterTicker, +} from "../exchanges/types"; +import { toPrice1Decimal } from "../utils/math"; +import { createTradeLog, type TradeLogEntry } from "../state/trade-log"; +import { getPosition, type PositionSnapshot } from "../utils/strategy"; +import { + marketClose, + OrderLockMap, + OrderPendingMap, + OrderTimerMap, + placeOrder, + unlockOperating, +} from "./order-coordinator"; + +interface DesiredOrder { + side: "BUY" | "SELL"; + price: number; + amount: number; + reduceOnly: boolean; +} + +export interface MakerEngineSnapshot { + ready: boolean; + symbol: string; + topBid: number | null; + topAsk: number | null; + spread: number | null; + position: PositionSnapshot; + pnl: number; + accountUnrealized: number; + openOrders: AsterOrder[]; + desiredOrders: DesiredOrder[]; + tradeLog: TradeLogEntry[]; + lastUpdated: number | null; +} + +type MakerEvent = "update"; +type MakerListener = (snapshot: MakerEngineSnapshot) => void; + +const EPS = 1e-5; + +export class MakerEngine { + private accountSnapshot: AsterAccountSnapshot | null = null; + private depthSnapshot: AsterDepth | null = null; + private tickerSnapshot: AsterTicker | null = null; + private openOrders: AsterOrder[] = []; + + private readonly locks: OrderLockMap = {}; + private readonly timers: OrderTimerMap = {}; + private readonly pending: OrderPendingMap = {}; + + private readonly tradeLog: ReturnType; + private readonly listeners = new Map>(); + + private timer: ReturnType | null = null; + private processing = false; + private desiredOrders: DesiredOrder[] = []; + private accountUnrealized = 0; + + constructor(private readonly config: MakerConfig, private readonly exchange: ExchangeAdapter) { + this.tradeLog = createTradeLog(this.config.maxLogEntries); + this.bootstrap(); + } + + start(): void { + if (this.timer) return; + this.timer = setInterval(() => { + void this.tick(); + }, this.config.refreshIntervalMs); + } + + stop(): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + } + + on(event: MakerEvent, handler: MakerListener): void { + const handlers = this.listeners.get(event) ?? new Set(); + handlers.add(handler); + this.listeners.set(event, handlers); + } + + off(event: MakerEvent, handler: MakerListener): void { + const handlers = this.listeners.get(event); + if (!handlers) return; + handlers.delete(handler); + if (handlers.size === 0) { + this.listeners.delete(event); + } + } + + getSnapshot(): MakerEngineSnapshot { + return this.buildSnapshot(); + } + + private bootstrap(): void { + this.exchange.watchAccount((snapshot) => { + this.accountSnapshot = snapshot; + const totalUnrealized = Number(snapshot.totalUnrealizedProfit ?? "0"); + if (Number.isFinite(totalUnrealized)) { + this.accountUnrealized = totalUnrealized; + } + this.emitUpdate(); + }); + + this.exchange.watchOrders((orders) => { + this.syncLocksWithOrders(orders); + this.openOrders = Array.isArray(orders) ? orders.filter((order) => order.type !== "MARKET") : []; + this.emitUpdate(); + }); + + this.exchange.watchDepth(this.config.symbol, (depth) => { + this.depthSnapshot = depth; + this.emitUpdate(); + }); + + this.exchange.watchTicker(this.config.symbol, (ticker) => { + this.tickerSnapshot = ticker; + this.emitUpdate(); + }); + + // Maker strategy does not consume klines, but subscribe to keep parity with other modules + this.exchange.watchKlines(this.config.symbol, "1m", () => { + /* no-op */ + }); + } + + private syncLocksWithOrders(orders: AsterOrder[]): void { + Object.keys(this.pending).forEach((type) => { + const pendingId = this.pending[type]; + if (!pendingId) return; + const match = orders.find((order) => String(order.orderId) === pendingId); + if (!match || (match.status && match.status !== "NEW" && match.status !== "PARTIALLY_FILLED")) { + unlockOperating(this.locks, this.timers, this.pending, type); + } + }); + } + + private isReady(): boolean { + return Boolean(this.accountSnapshot && this.depthSnapshot); + } + + private async tick(): Promise { + if (this.processing) return; + this.processing = true; + try { + if (!this.isReady()) { + this.emitUpdate(); + return; + } + + const depth = this.depthSnapshot!; + const bidLevel = depth.bids?.[0]; + const askLevel = depth.asks?.[0]; + const topBid = bidLevel ? Number(bidLevel[0]) : undefined; + const topAsk = askLevel ? Number(askLevel[0]) : undefined; + if (!Number.isFinite(topBid) || !Number.isFinite(topAsk)) { + this.emitUpdate(); + return; + } + + const bidPrice = toPrice1Decimal(topBid! - this.config.bidOffset); + const askPrice = toPrice1Decimal(topAsk! + this.config.askOffset); + const position = getPosition(this.accountSnapshot, this.config.symbol); + const absPosition = Math.abs(position.positionAmt); + const desired: DesiredOrder[] = []; + + if (absPosition < EPS) { + desired.push({ side: "BUY", price: bidPrice, amount: this.config.tradeAmount, reduceOnly: false }); + desired.push({ side: "SELL", price: askPrice, amount: this.config.tradeAmount, reduceOnly: false }); + } else { + const closeSide: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY"; + const closePrice = closeSide === "SELL" ? askPrice : bidPrice; + desired.push({ side: closeSide, price: closePrice, amount: absPosition, reduceOnly: true }); + } + + this.desiredOrders = desired; + await this.syncOrders(desired); + await this.checkRisk(position, bidPrice, askPrice); + this.emitUpdate(); + } catch (error) { + this.tradeLog.push("error", `做市循环异常: ${String(error)}`); + this.emitUpdate(); + } finally { + this.processing = false; + } + } + + private async syncOrders(targets: DesiredOrder[]): Promise { + const tolerance = this.config.priceChaseThreshold; + const unmatched = new Set(targets.map((_, idx) => idx)); + const toCancel: AsterOrder[] = []; + + for (const order of this.openOrders) { + const price = Number(order.price); + if (!Number.isFinite(price)) { + toCancel.push(order); + continue; + } + const reduceOnly = order.reduceOnly === true; + const matchedIndex = targets.findIndex((target, index) => { + if (!unmatched.has(index)) return false; + if (target.side !== order.side) return false; + if (target.reduceOnly !== reduceOnly) return false; + return Math.abs(price - target.price) <= tolerance; + }); + if (matchedIndex >= 0) { + unmatched.delete(matchedIndex); + continue; + } + toCancel.push(order); + } + + for (const order of toCancel) { + try { + await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: order.orderId }); + this.tradeLog.push("order", `撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`); + } catch (error) { + this.tradeLog.push("error", `撤销订单失败: ${String(error)}`); + } + } + + for (const index of unmatched) { + const target = targets[index]; + if (target.amount < EPS) continue; + try { + await placeOrder( + this.exchange, + this.config.symbol, + this.openOrders, + this.locks, + this.timers, + this.pending, + target.side, + target.price, + target.amount, + (type, detail) => this.tradeLog.push(type, detail), + target.reduceOnly + ); + } catch (error) { + this.tradeLog.push("error", `挂单失败(${target.side} ${target.price}): ${String(error)}`); + } + } + } + + private async checkRisk(position: PositionSnapshot, bidPrice: number, askPrice: number): Promise { + const absPosition = Math.abs(position.positionAmt); + if (absPosition < EPS) return; + + const pnl = position.positionAmt > 0 + ? (bidPrice - position.entryPrice) * absPosition + : (position.entryPrice - askPrice) * absPosition; + + if (pnl < -this.config.lossLimit || position.unrealizedProfit < -this.config.lossLimit) { + this.tradeLog.push( + "stop", + `触发止损,方向=${position.positionAmt > 0 ? "多" : "空"} 当前亏损=${pnl.toFixed(4)} USDT` + ); + try { + await this.flushOrders(); + await marketClose( + this.exchange, + this.config.symbol, + this.openOrders, + this.locks, + this.timers, + this.pending, + position.positionAmt > 0 ? "SELL" : "BUY", + absPosition, + (type, detail) => this.tradeLog.push(type, detail) + ); + } catch (error) { + this.tradeLog.push("error", `止损平仓失败: ${String(error)}`); + } + } + } + + private async flushOrders(): Promise { + if (!this.openOrders.length) return; + for (const order of this.openOrders) { + try { + await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: order.orderId }); + } catch (error) { + this.tradeLog.push("error", `撤销订单失败: ${String(error)}`); + } + } + } + + private emitUpdate(): void { + const snapshot = this.buildSnapshot(); + const handlers = this.listeners.get("update"); + if (!handlers) return; + handlers.forEach((handler) => handler(snapshot)); + } + + private buildSnapshot(): MakerEngineSnapshot { + const position = getPosition(this.accountSnapshot, this.config.symbol); + const bid = this.depthSnapshot?.bids?.[0]?.[0]; + const ask = this.depthSnapshot?.asks?.[0]?.[0]; + const bidNum = Number(bid); + const askNum = Number(ask); + const spread = Number.isFinite(bidNum) && Number.isFinite(askNum) ? askNum - bidNum : null; + const priceForPnl = position.positionAmt > 0 ? bidNum : askNum; + const pnl = Number.isFinite(priceForPnl) + ? (position.positionAmt > 0 + ? (priceForPnl! - position.entryPrice) * Math.abs(position.positionAmt) + : (position.entryPrice - priceForPnl!) * Math.abs(position.positionAmt)) + : 0; + + return { + ready: this.isReady(), + symbol: this.config.symbol, + topBid: Number.isFinite(bidNum) ? bidNum : null, + topAsk: Number.isFinite(askNum) ? askNum : null, + spread, + position, + pnl, + accountUnrealized: this.accountUnrealized, + openOrders: this.openOrders, + desiredOrders: this.desiredOrders, + tradeLog: this.tradeLog.all(), + lastUpdated: Date.now(), + }; + } +} diff --git a/src/core/order-coordinator.ts b/src/core/order-coordinator.ts new file mode 100644 index 0000000..66ac1f8 --- /dev/null +++ b/src/core/order-coordinator.ts @@ -0,0 +1,268 @@ +import type { ExchangeAdapter } from "../exchanges/adapter"; +import type { AsterOrder, CreateOrderParams } from "../exchanges/types"; +import { toPrice1Decimal, toQty3Decimal } from "../utils/math"; + +export type OrderLockMap = Record; +export type OrderTimerMap = Record | null>; +export type OrderPendingMap = Record; +export type LogHandler = (type: string, detail: string) => void; + +export function isOperating(locks: OrderLockMap, type: string): boolean { + return Boolean(locks[type]); +} + +export function lockOperating( + locks: OrderLockMap, + timers: OrderTimerMap, + pendings: OrderPendingMap, + type: string, + log: LogHandler, + timeout = 3000 +): void { + locks[type] = true; + if (timers[type]) { + clearTimeout(timers[type]!); + } + timers[type] = setTimeout(() => { + locks[type] = false; + pendings[type] = null; + log("error", `${type} 操作超时自动解锁`); + }, timeout); +} + +export function unlockOperating( + locks: OrderLockMap, + timers: OrderTimerMap, + pendings: OrderPendingMap, + type: string +): void { + locks[type] = false; + pendings[type] = null; + if (timers[type]) { + clearTimeout(timers[type]!); + } + timers[type] = null; +} + +export async function deduplicateOrders( + adapter: ExchangeAdapter, + symbol: string, + openOrders: AsterOrder[], + locks: OrderLockMap, + timers: OrderTimerMap, + pendings: OrderPendingMap, + type: string, + side: string, + log: LogHandler +): Promise { + const sameTypeOrders = openOrders.filter((o) => o.type === type && o.side === side); + if (sameTypeOrders.length <= 1) return; + sameTypeOrders.sort((a, b) => { + const ta = b.updateTime || b.time || 0; + const tb = a.updateTime || a.time || 0; + return ta - tb; + }); + const toCancel = sameTypeOrders.slice(1); + const orderIdList = toCancel.map((o) => o.orderId); + if (!orderIdList.length) return; + try { + lockOperating(locks, timers, pendings, type, log); + await adapter.cancelOrders({ symbol, orderIdList }); + log("order", `去重撤销重复 ${type} 单: ${orderIdList.join(",")}`); + } catch (err) { + log("error", `去重撤单失败: ${String(err)}`); + } finally { + unlockOperating(locks, timers, pendings, type); + } +} + +export async function placeOrder( + adapter: ExchangeAdapter, + symbol: string, + openOrders: AsterOrder[], + locks: OrderLockMap, + timers: OrderTimerMap, + pendings: OrderPendingMap, + side: "BUY" | "SELL", + price: number, + amount: number, + log: LogHandler, + reduceOnly = false +): Promise { + const type = "LIMIT"; + if (isOperating(locks, type)) return; + const params: CreateOrderParams = { + symbol, + side, + type, + quantity: toQty3Decimal(amount), + price: toPrice1Decimal(price), + timeInForce: "GTX", + }; + if (reduceOnly) params.reduceOnly = "true"; + await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log); + lockOperating(locks, timers, pendings, type, log); + try { + const order = await adapter.createOrder(params); + pendings[type] = String(order.orderId); + log("order", `挂限价单: ${side} @ ${params.price} 数量 ${params.quantity} reduceOnly=${reduceOnly}`); + return order; + } catch (err) { + unlockOperating(locks, timers, pendings, type); + throw err; + } +} + +export async function placeMarketOrder( + adapter: ExchangeAdapter, + symbol: string, + openOrders: AsterOrder[], + locks: OrderLockMap, + timers: OrderTimerMap, + pendings: OrderPendingMap, + side: "BUY" | "SELL", + amount: number, + log: LogHandler, + reduceOnly = false +): Promise { + const type = "MARKET"; + if (isOperating(locks, type)) return; + const params: CreateOrderParams = { + symbol, + side, + type, + quantity: toQty3Decimal(amount), + }; + if (reduceOnly) params.reduceOnly = "true"; + await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log); + lockOperating(locks, timers, pendings, type, log); + try { + const order = await adapter.createOrder(params); + pendings[type] = String(order.orderId); + log("order", `市价单: ${side} 数量 ${params.quantity} reduceOnly=${reduceOnly}`); + return order; + } catch (err) { + unlockOperating(locks, timers, pendings, type); + throw err; + } +} + +export async function placeStopLossOrder( + adapter: ExchangeAdapter, + symbol: string, + openOrders: AsterOrder[], + locks: OrderLockMap, + timers: OrderTimerMap, + pendings: OrderPendingMap, + side: "BUY" | "SELL", + stopPrice: number, + quantity: number, + lastPrice: number | null, + log: LogHandler +): Promise { + const type = "STOP_MARKET"; + if (isOperating(locks, type)) return; + if (lastPrice != null) { + if (side === "SELL" && stopPrice >= lastPrice) { + log("error", `止损价 ${stopPrice} 高于或等于当前价 ${lastPrice},取消挂单`); + return; + } + if (side === "BUY" && stopPrice <= lastPrice) { + log("error", `止损价 ${stopPrice} 低于或等于当前价 ${lastPrice},取消挂单`); + return; + } + } + const params: CreateOrderParams = { + symbol, + side, + type, + stopPrice: toPrice1Decimal(stopPrice), + closePosition: "true", + timeInForce: "GTC", + quantity: toQty3Decimal(quantity), + }; + await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log); + lockOperating(locks, timers, pendings, type, log); + try { + const order = await adapter.createOrder(params); + pendings[type] = String(order.orderId); + log("stop", `挂止损单: ${side} STOP_MARKET @ ${params.stopPrice}`); + return order; + } catch (err) { + unlockOperating(locks, timers, pendings, type); + throw err; + } +} + +export async function placeTrailingStopOrder( + adapter: ExchangeAdapter, + symbol: string, + openOrders: AsterOrder[], + locks: OrderLockMap, + timers: OrderTimerMap, + pendings: OrderPendingMap, + side: "BUY" | "SELL", + activationPrice: number, + quantity: number, + callbackRate: number, + log: LogHandler +): Promise { + const type = "TRAILING_STOP_MARKET"; + if (isOperating(locks, type)) return; + const params: CreateOrderParams = { + symbol, + side, + type, + quantity: toQty3Decimal(quantity), + reduceOnly: "true", + activationPrice: toPrice1Decimal(activationPrice), + callbackRate, + timeInForce: "GTC", + }; + await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log); + lockOperating(locks, timers, pendings, type, log); + try { + const order = await adapter.createOrder(params); + pendings[type] = String(order.orderId); + log( + "order", + `挂动态止盈单: ${side} activation=${params.activationPrice} callbackRate=${callbackRate}` + ); + return order; + } catch (err) { + unlockOperating(locks, timers, pendings, type); + throw err; + } +} + +export async function marketClose( + adapter: ExchangeAdapter, + symbol: string, + openOrders: AsterOrder[], + locks: OrderLockMap, + timers: OrderTimerMap, + pendings: OrderPendingMap, + side: "BUY" | "SELL", + quantity: number, + log: LogHandler +): Promise { + const type = "MARKET"; + if (isOperating(locks, type)) return; + const params: CreateOrderParams = { + symbol, + side, + type, + quantity: toQty3Decimal(quantity), + reduceOnly: "true", + }; + await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log); + lockOperating(locks, timers, pendings, type, log); + try { + const order = await adapter.createOrder(params); + pendings[type] = String(order.orderId); + log("close", `市价平仓: ${side}`); + } catch (err) { + unlockOperating(locks, timers, pendings, type); + throw err; + } +} diff --git a/src/core/trend-engine.ts b/src/core/trend-engine.ts new file mode 100644 index 0000000..04f52ae --- /dev/null +++ b/src/core/trend-engine.ts @@ -0,0 +1,434 @@ +import type { TradingConfig } from "../config"; +import type { ExchangeAdapter } from "../exchanges/adapter"; +import type { + AsterAccountSnapshot, + AsterOrder, + AsterTicker, + AsterDepth, + AsterKline, +} from "../exchanges/types"; +import { + calcStopLossPrice, + calcTrailingActivationPrice, + getPosition, + getSMA, + type PositionSnapshot, +} from "../utils/strategy"; +import { + marketClose, + OrderLockMap, + OrderPendingMap, + OrderTimerMap, + placeMarketOrder, + placeStopLossOrder, + placeTrailingStopOrder, + unlockOperating, +} from "./order-coordinator"; +import { toPrice1Decimal } from "../utils/math"; +import { createTradeLog, type TradeLogEntry } from "../state/trade-log"; + +export interface TrendEngineSnapshot { + ready: boolean; + symbol: string; + lastPrice: number | null; + sma30: number | null; + trend: "做多" | "做空" | "无信号"; + position: PositionSnapshot; + pnl: number; + unrealized: number; + totalProfit: number; + totalTrades: number; + tradeLog: TradeLogEntry[]; + openOrders: AsterOrder[]; + depth: AsterDepth | null; + ticker: AsterTicker | null; + lastUpdated: number | null; + lastOpenSignal: OpenOrderPlan; +} + +export interface OpenOrderPlan { + side: "BUY" | "SELL" | null; + price: number | null; +} + +type TrendEngineEvent = "update"; + +type TrendEngineListener = (snapshot: TrendEngineSnapshot) => void; + +export class TrendEngine { + private accountSnapshot: AsterAccountSnapshot | null = null; + private openOrders: AsterOrder[] = []; + private depthSnapshot: AsterDepth | null = null; + private tickerSnapshot: AsterTicker | null = null; + private klineSnapshot: AsterKline[] = []; + + private readonly locks: OrderLockMap = {}; + private readonly timers: OrderTimerMap = {}; + private readonly pending: OrderPendingMap = {}; + + private readonly tradeLog: ReturnType; + + private timer: ReturnType | null = null; + private processing = false; + private lastPrice: number | null = null; + private lastSma30: number | null = null; + private totalProfit = 0; + private totalTrades = 0; + private lastOpenPlan: OpenOrderPlan = { side: null, price: null }; + + private readonly listeners = new Map>(); + + constructor(private readonly config: TradingConfig, private readonly exchange: ExchangeAdapter) { + this.tradeLog = createTradeLog(this.config.maxLogEntries); + this.bootstrap(); + } + + start(): void { + if (this.timer) return; + this.timer = setInterval(() => { + void this.tick(); + }, this.config.pollIntervalMs); + } + + stop(): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + } + + on(event: TrendEngineEvent, handler: TrendEngineListener): void { + const handlers = this.listeners.get(event) ?? new Set(); + handlers.add(handler); + this.listeners.set(event, handlers); + } + + off(event: TrendEngineEvent, handler: TrendEngineListener): void { + const handlers = this.listeners.get(event); + if (!handlers) return; + handlers.delete(handler); + if (handlers.size === 0) { + this.listeners.delete(event); + } + } + + getSnapshot(): TrendEngineSnapshot { + return this.buildSnapshot(); + } + + private bootstrap(): void { + this.exchange.watchAccount((snapshot) => { + this.accountSnapshot = snapshot; + this.emitUpdate(); + }); + this.exchange.watchOrders((orders) => { + this.synchronizeLocks(orders); + this.openOrders = Array.isArray(orders) + ? orders.filter((order) => order.type !== "MARKET") + : []; + this.emitUpdate(); + }); + this.exchange.watchDepth(this.config.symbol, (depth) => { + this.depthSnapshot = depth; + this.emitUpdate(); + }); + this.exchange.watchTicker(this.config.symbol, (ticker) => { + this.tickerSnapshot = ticker; + this.emitUpdate(); + }); + this.exchange.watchKlines(this.config.symbol, this.config.klineInterval, (klines) => { + this.klineSnapshot = klines; + this.emitUpdate(); + }); + } + + private synchronizeLocks(orders: AsterOrder[]): void { + Object.keys(this.pending).forEach((type) => { + const pendingId = this.pending[type]; + if (!pendingId) return; + const match = orders.find((order) => String(order.orderId) === pendingId); + if (!match || (match.status && match.status !== "NEW")) { + unlockOperating(this.locks, this.timers, this.pending, type); + } + }); + } + + private isReady(): boolean { + return Boolean( + this.accountSnapshot && + this.tickerSnapshot && + this.depthSnapshot && + this.klineSnapshot.length >= 30 + ); + } + + private async tick(): Promise { + if (this.processing) return; + this.processing = true; + try { + if (!this.isReady()) { + this.emitUpdate(); + return; + } + const sma30 = getSMA(this.klineSnapshot, 30); + if (sma30 == null) { + return; + } + const ticker = this.tickerSnapshot!; + const price = Number(ticker.lastPrice); + const position = getPosition(this.accountSnapshot, this.config.symbol); + + if (Math.abs(position.positionAmt) < 1e-5) { + await this.handleOpenPosition(price, sma30); + } else { + const result = await this.handlePositionManagement(position, price); + if (result.closed) { + this.totalTrades += 1; + this.totalProfit += result.pnl; + } + } + + this.lastSma30 = sma30; + this.lastPrice = price; + this.emitUpdate(); + } catch (error) { + this.tradeLog.push("error", `策略循环异常: ${String(error)}`); + this.emitUpdate(); + } finally { + this.processing = false; + } + } + + private async handleOpenPosition(currentPrice: number, currentSma: number): Promise { + if (this.lastPrice == null) { + this.lastPrice = currentPrice; + return; + } + if (this.openOrders.length > 0) { + try { + await this.exchange.cancelAllOrders({ symbol: this.config.symbol }); + } catch (err) { + this.tradeLog.push("error", `撤销挂单失败: ${String(err)}`); + } + } + if (this.lastPrice > currentSma && currentPrice < currentSma) { + await this.submitMarketOrder("SELL", currentPrice, "下穿SMA30,市价开空"); + } else if (this.lastPrice < currentSma && currentPrice > currentSma) { + await this.submitMarketOrder("BUY", currentPrice, "上穿SMA30,市价开多"); + } + } + + private async submitMarketOrder(side: "BUY" | "SELL", price: number, reason: string): Promise { + try { + await placeMarketOrder( + this.exchange, + this.config.symbol, + this.openOrders, + this.locks, + this.timers, + this.pending, + side, + this.config.tradeAmount, + (type, detail) => this.tradeLog.push(type, detail) + ); + this.tradeLog.push("open", `${reason}: ${side} @ ${price}`); + this.lastOpenPlan = { side, price }; + } catch (err) { + this.tradeLog.push("error", `市价下单失败: ${String(err)}`); + } + } + + private async handlePositionManagement( + position: PositionSnapshot, + price: number + ): Promise<{ closed: boolean; pnl: number }> { + const direction = position.positionAmt > 0 ? "long" : "short"; + const pnl = + (direction === "long" + ? price - position.entryPrice + : position.entryPrice - price) * Math.abs(position.positionAmt); + const stopSide = direction === "long" ? "SELL" : "BUY"; + const stopPrice = calcStopLossPrice( + position.entryPrice, + Math.abs(position.positionAmt), + direction, + this.config.lossLimit + ); + const activationPrice = calcTrailingActivationPrice( + position.entryPrice, + Math.abs(position.positionAmt), + direction, + this.config.trailingProfit + ); + + const currentStop = this.openOrders.find( + (o) => o.type === "STOP_MARKET" && o.side === stopSide + ); + const currentTrailing = this.openOrders.find( + (o) => o.type === "TRAILING_STOP_MARKET" && o.side === stopSide + ); + + const profitLockStopPrice = direction === "long" + ? toPrice1Decimal( + position.entryPrice + this.config.profitLockOffsetUsd / Math.abs(position.positionAmt) + ) + : toPrice1Decimal( + position.entryPrice - this.config.profitLockOffsetUsd / Math.abs(position.positionAmt) + ); + + if (pnl > this.config.profitLockTriggerUsd || position.unrealizedProfit > this.config.profitLockTriggerUsd) { + if (!currentStop) { + await this.tryPlaceStopLoss(stopSide, profitLockStopPrice, price); + } else { + const existingPrice = Number(currentStop.stopPrice); + if (Math.abs(existingPrice - profitLockStopPrice) > 0.01) { + await this.tryReplaceStop(stopSide, currentStop, profitLockStopPrice, price); + } + } + } + + if (!currentStop) { + await this.tryPlaceStopLoss(stopSide, toPrice1Decimal(stopPrice), price); + } + + if (!currentTrailing) { + await this.tryPlaceTrailingStop( + stopSide, + toPrice1Decimal(activationPrice), + Math.abs(position.positionAmt) + ); + } + + if (pnl < -this.config.lossLimit || position.unrealizedProfit < -this.config.lossLimit) { + try { + if (this.openOrders.length > 0) { + const orderIdList = this.openOrders.map((order) => order.orderId); + await this.exchange.cancelOrders({ symbol: this.config.symbol, orderIdList }); + } + await marketClose( + this.exchange, + this.config.symbol, + this.openOrders, + this.locks, + this.timers, + this.pending, + direction === "long" ? "SELL" : "BUY", + this.config.tradeAmount, + (type, detail) => this.tradeLog.push(type, detail) + ); + this.tradeLog.push("close", `止损平仓: ${direction === "long" ? "SELL" : "BUY"}`); + } catch (err) { + this.tradeLog.push("error", `止损平仓失败: ${String(err)}`); + } + return { closed: true, pnl }; + } + + return { closed: false, pnl }; + } + + private async tryPlaceStopLoss( + side: "BUY" | "SELL", + stopPrice: number, + lastPrice: number + ): Promise { + try { + await placeStopLossOrder( + this.exchange, + this.config.symbol, + this.openOrders, + this.locks, + this.timers, + this.pending, + side, + stopPrice, + this.config.tradeAmount, + lastPrice, + (type, detail) => this.tradeLog.push(type, detail) + ); + } catch (err) { + this.tradeLog.push("error", `挂止损单失败: ${String(err)}`); + } + } + + private async tryReplaceStop( + side: "BUY" | "SELL", + currentOrder: AsterOrder, + nextStopPrice: number, + lastPrice: number + ): Promise { + try { + await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: currentOrder.orderId }); + } catch (err) { + this.tradeLog.push("error", `取消原止损单失败: ${String(err)}`); + } + await this.tryPlaceStopLoss(side, nextStopPrice, lastPrice); + this.tradeLog.push("stop", `移动止损到 ${nextStopPrice}`); + } + + private async tryPlaceTrailingStop( + side: "BUY" | "SELL", + activationPrice: number, + quantity: number + ): Promise { + try { + await placeTrailingStopOrder( + this.exchange, + this.config.symbol, + this.openOrders, + this.locks, + this.timers, + this.pending, + side, + activationPrice, + quantity, + this.config.trailingCallbackRate, + (type, detail) => this.tradeLog.push(type, detail) + ); + } catch (err) { + this.tradeLog.push("error", `挂动态止盈失败: ${String(err)}`); + } + } + + private emitUpdate(): void { + const snapshot = this.buildSnapshot(); + const handlers = this.listeners.get("update"); + if (!handlers) return; + handlers.forEach((handler) => handler(snapshot)); + } + + private buildSnapshot(): TrendEngineSnapshot { + const position = getPosition(this.accountSnapshot, this.config.symbol); + const price = this.tickerSnapshot ? Number(this.tickerSnapshot.lastPrice) : null; + const sma30 = this.lastSma30; + const trend = price == null || sma30 == null + ? "无信号" + : price > sma30 + ? "做多" + : price < sma30 + ? "做空" + : "无信号"; + const pnl = price != null && position + ? (position.positionAmt > 0 + ? (price - position.entryPrice) * Math.abs(position.positionAmt) + : (position.entryPrice - price) * Math.abs(position.positionAmt)) + : 0; + return { + ready: this.isReady(), + symbol: this.config.symbol, + lastPrice: price, + sma30, + trend, + position, + pnl, + unrealized: position.unrealizedProfit, + totalProfit: this.totalProfit, + totalTrades: this.totalTrades, + tradeLog: this.tradeLog.all(), + openOrders: this.openOrders, + depth: this.depthSnapshot, + ticker: this.tickerSnapshot, + lastUpdated: Date.now(), + lastOpenSignal: this.lastOpenPlan, + }; + } +} diff --git a/src/exchanges/adapter.ts b/src/exchanges/adapter.ts new file mode 100644 index 0000000..d05fcf0 --- /dev/null +++ b/src/exchanges/adapter.ts @@ -0,0 +1,41 @@ +import type { + AsterAccountSnapshot, + AsterOrder, + AsterDepth, + AsterTicker, + AsterKline, + CreateOrderParams, +} from "./types"; + +export interface AccountListener { + (snapshot: AsterAccountSnapshot): void; +} + +export interface OrderListener { + (orders: AsterOrder[]): void; +} + +export interface DepthListener { + (depth: AsterDepth): void; +} + +export interface TickerListener { + (ticker: AsterTicker): void; +} + +export interface KlineListener { + (klines: AsterKline[]): void; +} + +export interface ExchangeAdapter { + readonly id: string; + watchAccount(cb: AccountListener): void; + watchOrders(cb: OrderListener): void; + watchDepth(symbol: string, cb: DepthListener): void; + watchTicker(symbol: string, cb: TickerListener): void; + watchKlines(symbol: string, interval: string, cb: KlineListener): void; + createOrder(params: CreateOrderParams): Promise; + cancelOrder(params: { symbol: string; orderId: number | string }): Promise; + cancelOrders(params: { symbol: string; orderIdList: Array }): Promise; + cancelAllOrders(params: { symbol: string }): Promise; +} diff --git a/src/exchanges/aster-adapter.ts b/src/exchanges/aster-adapter.ts new file mode 100644 index 0000000..13d4d82 --- /dev/null +++ b/src/exchanges/aster-adapter.ts @@ -0,0 +1,90 @@ +import type { + AccountListener, + DepthListener, + ExchangeAdapter, + KlineListener, + OrderListener, + TickerListener, +} from "./adapter"; +import type { AsterOrder, CreateOrderParams, AsterDepth, AsterTicker, AsterKline } from "./types"; +import { AsterGateway } from "./aster/client"; + +export interface AsterCredentials { + apiKey?: string; + apiSecret?: string; + symbol?: string; +} + +export class AsterExchangeAdapter implements ExchangeAdapter { + readonly id = "aster"; + private readonly gateway: AsterGateway; + private readonly symbol: string; + private initPromise: Promise | null = null; + + constructor(credentials: AsterCredentials = {}) { + this.gateway = new AsterGateway({ apiKey: credentials.apiKey, apiSecret: credentials.apiSecret }); + this.symbol = (credentials.symbol ?? process.env.TRADE_SYMBOL ?? "BTCUSDT").toUpperCase(); + } + + private ensureInitialized(): Promise { + if (!this.initPromise) { + this.initPromise = this.gateway.ensureInitialized(this.symbol); + } + return this.initPromise; + } + + watchAccount(cb: AccountListener): void { + void this.ensureInitialized(); + this.gateway.onAccount((snapshot) => { + cb(snapshot); + }); + } + + watchOrders(cb: OrderListener): void { + void this.ensureInitialized(); + this.gateway.onOrders((orders) => { + cb(orders); + }); + } + + watchDepth(symbol: string, cb: DepthListener): void { + void this.ensureInitialized(); + this.gateway.onDepth(symbol, (depth: AsterDepth) => { + cb(depth); + }); + } + + watchTicker(symbol: string, cb: TickerListener): void { + void this.ensureInitialized(); + this.gateway.onTicker(symbol, (ticker: AsterTicker) => { + cb(ticker); + }); + } + + watchKlines(symbol: string, interval: string, cb: KlineListener): void { + void this.ensureInitialized(); + this.gateway.onKlines(symbol, interval, (klines: AsterKline[]) => { + cb(klines); + }); + } + + async createOrder(params: CreateOrderParams): Promise { + await this.ensureInitialized(); + return this.gateway.createOrder(params); + } + + async cancelOrder(params: { symbol: string; orderId: number | string }): Promise { + await this.ensureInitialized(); + await this.gateway.cancelOrder({ symbol: params.symbol, orderId: Number(params.orderId) }); + } + + async cancelOrders(params: { symbol: string; orderIdList: Array }): Promise { + await this.ensureInitialized(); + await this.gateway.cancelOrders({ symbol: params.symbol, orderIdList: params.orderIdList }); + } + + async cancelAllOrders(params: { symbol: string }): Promise { + await this.ensureInitialized(); + await this.gateway.cancelAllOrders(params); + } +} diff --git a/src/exchanges/aster/client.ts b/src/exchanges/aster/client.ts new file mode 100644 index 0000000..20ae5f2 --- /dev/null +++ b/src/exchanges/aster/client.ts @@ -0,0 +1,850 @@ +import crypto from "crypto"; +import { setInterval, clearInterval, setTimeout, clearTimeout } from "timers"; +import type { + AsterAccountSnapshot, + AsterDepth, + AsterKline, + AsterOrder, + AsterTicker, + CreateOrderParams, +} from "../types"; + +const REST_BASE = "https://fapi.asterdex.com"; +const WS_PUBLIC_URL = "wss://fstream.asterdex.com/ws"; +const WS_LISTEN_KEY_URL = "wss://fstream.asterdex.com/ws/"; + +const FINAL_ORDER_STATUSES = new Set(["FILLED", "CANCELED", "REJECTED", "EXPIRED"]); +const DEFAULT_DEPTH_LEVEL = 20; +const DEFAULT_DEPTH_SPEED = "100ms"; +const DEFAULT_KLINE_LIMIT = 120; +const KLINE_REFRESH_INTERVAL_MS = 60_000; +const LISTEN_KEY_KEEPALIVE_MS = 30 * 60 * 1000; +const RECONNECT_DELAY_MS = 2000; + +function requireEnv(value: string | undefined, key: string): string { + if (!value) { + throw new Error(`Missing required environment variable ${key}`); + } + return value; +} + +function toDepth(streamSymbol: string, data: any): AsterDepth { + return { + eventType: data.e, + eventTime: data.E, + tradeTime: data.T, + symbol: streamSymbol, + lastUpdateId: data.u, + bids: (data.b ?? []).map(([price, qty]: [string, string]) => [price, qty]), + asks: (data.a ?? []).map(([price, qty]: [string, string]) => [price, qty]), + }; +} + +function toTicker(data: any): AsterTicker { + return { + eventType: data.e, + eventTime: data.E, + symbol: data.s, + lastPrice: data.c, + openPrice: data.o, + highPrice: data.h, + lowPrice: data.l, + volume: data.q ?? data.v ?? "0", + quoteVolume: data.Q ?? data.V ?? "0", + priceChange: data.p, + priceChangePercent: data.P, + weightedAvgPrice: data.w, + lastQty: data.l ?? data.L, + openTime: data.O, + closeTime: data.C, + firstId: data.F, + lastId: data.L, + count: data.n, + }; +} + +function toKline(data: any): AsterKline { + return { + eventType: data.e, + eventTime: data.E, + symbol: data.s, + interval: data.k.i, + openTime: data.k.t, + closeTime: data.k.T, + firstTradeId: data.k.f, + lastTradeId: data.k.L, + open: data.k.o, + high: data.k.h, + low: data.k.l, + close: data.k.c, + volume: data.k.v, + numberOfTrades: data.k.n, + quoteAssetVolume: data.k.q, + takerBuyBaseAssetVolume: data.k.V, + takerBuyQuoteAssetVolume: data.k.Q, + isClosed: Boolean(data.k.x), + }; +} + +function fromRestKline(entry: any[], interval: string, symbol: string): AsterKline { + return { + eventType: undefined, + eventTime: undefined, + symbol, + interval, + openTime: entry[0], + open: entry[1], + high: entry[2], + low: entry[3], + close: entry[4], + volume: entry[5], + closeTime: entry[6], + quoteAssetVolume: entry[7], + numberOfTrades: entry[8], + takerBuyBaseAssetVolume: entry[9], + takerBuyQuoteAssetVolume: entry[10], + isClosed: Boolean(entry[11]), + } as AsterKline; +} + +function toOrderFromRest(raw: any): AsterOrder { + return { + avgPrice: raw.avgPrice ?? "0", + clientOrderId: raw.clientOrderId ?? "", + cumQuote: raw.cumQuote ?? "0", + executedQty: raw.executedQty ?? "0", + orderId: raw.orderId, + origQty: raw.origQty ?? raw.quantity ?? "0", + origType: raw.origType ?? raw.type ?? "", + price: raw.price ?? "0", + reduceOnly: Boolean(raw.reduceOnly), + side: raw.side ?? "", + positionSide: raw.positionSide ?? "BOTH", + status: raw.status ?? "NEW", + stopPrice: raw.stopPrice ?? raw.triggerPrice ?? "0", + closePosition: Boolean(raw.closePosition), + symbol: raw.symbol ?? "", + time: raw.time ?? raw.updateTime ?? Date.now(), + timeInForce: raw.timeInForce ?? "GTC", + type: raw.type ?? "LIMIT", + activatePrice: raw.activatePrice, + priceRate: raw.priceRate, + updateTime: raw.updateTime ?? Date.now(), + workingType: raw.workingType ?? "CONTRACT_PRICE", + priceProtect: Boolean(raw.priceProtect), + }; +} + +function toOrderFromEvent(event: any): AsterOrder { + return { + avgPrice: event.ap ?? "0", + clientOrderId: event.c ?? "", + cumQuote: event.z ?? "0", + executedQty: event.z ?? "0", + orderId: event.i, + origQty: event.q ?? "0", + origType: event.ot ?? event.o ?? "", + price: event.p ?? "0", + reduceOnly: Boolean(event.R), + side: event.S, + positionSide: event.ps ?? "BOTH", + status: event.X, + stopPrice: event.sp ?? "0", + closePosition: Boolean(event.cp), + symbol: event.s, + time: event.T ?? Date.now(), + timeInForce: event.f ?? "GTC", + type: event.o ?? "LIMIT", + activatePrice: event.AP, + priceRate: event.cr, + updateTime: event.T ?? Date.now(), + workingType: event.wt ?? "CONTRACT_PRICE", + priceProtect: Boolean(event.PP), + }; +} + +function deepCloneAccount(snapshot: AsterAccountSnapshot | null): AsterAccountSnapshot | null { + return snapshot ? JSON.parse(JSON.stringify(snapshot)) : null; +} + +class SimpleEvent { + private readonly listeners = new Set<(payload: T) => void>(); + + add(listener: (payload: T) => void): void { + this.listeners.add(listener); + } + + remove(listener: (payload: T) => void): void { + this.listeners.delete(listener); + } + + emit(payload: T): void { + for (const listener of Array.from(this.listeners)) { + try { + listener(payload); + } catch (error) { + console.error("[SimpleEvent] listener failure", error); + } + } + } + + listenerCount(): number { + return this.listeners.size; + } +} + +export interface ListenKeyResponse { + listenKey: string; +} + +export class AsterRestClient { + private readonly apiKey: string; + private readonly apiSecret: string; + + constructor(options: { apiKey?: string; apiSecret?: string } = {}) { + this.apiKey = requireEnv(options.apiKey ?? process.env.ASTER_API_KEY, "ASTER_API_KEY"); + this.apiSecret = requireEnv(options.apiSecret ?? process.env.ASTER_API_SECRET, "ASTER_API_SECRET"); + } + + async getAccount(): Promise { + return this.signedRequest({ path: "/fapi/v2/account", method: "GET", params: {} }); + } + + async getOpenOrders(symbol?: string): Promise { + const params: Record = {}; + if (symbol) params.symbol = symbol; + const raw = await this.signedRequest({ path: "/fapi/v1/openOrders", method: "GET", params }); + return raw.map(toOrderFromRest); + } + + async createOrder(params: CreateOrderParams): Promise { + const payload: Record = { ...params }; + const response = await this.signedRequest({ path: "/fapi/v1/order", method: "POST", params: payload }); + return toOrderFromRest(response); + } + + async cancelOrder(params: { symbol: string; orderId?: number; origClientOrderId?: string }): Promise { + const response = await this.signedRequest({ path: "/fapi/v1/order", method: "DELETE", params }); + return toOrderFromRest(response); + } + + async cancelOrders(params: { symbol: string; orderIdList?: Array; origClientOrderIdList?: string[] }): Promise { + const payload: Record = { symbol: params.symbol }; + if (params.orderIdList) payload.orderIdList = JSON.stringify(params.orderIdList.map((id) => Number(id))); + if (params.origClientOrderIdList) payload.origClientOrderIdList = JSON.stringify(params.origClientOrderIdList); + const response = await this.signedRequest({ path: "/fapi/v1/batchOrders", method: "DELETE", params: payload }); + return response.map(toOrderFromRest); + } + + async cancelAllOrders(params: { symbol: string }): Promise { + await this.signedRequest({ path: "/fapi/v1/allOpenOrders", method: "DELETE", params }); + } + + async getKlines(symbol: string, interval: string, limit = DEFAULT_KLINE_LIMIT): Promise { + const upper = symbol.toUpperCase(); + const url = `${REST_BASE}/fapi/v1/continuousKlines?pair=${upper}&contractType=PERPETUAL&interval=${encodeURIComponent(interval)}&limit=${limit}`; + const response = await fetch(url); + if (!response.ok) { + const text = await response.text(); + throw new Error(`HTTP ${response.status} ${text}`); + } + const payload = (await response.json()) as any[]; + return payload.map((entry) => fromRestKline(entry, interval, upper)); + } + + async getListenKey(): Promise { + const response = await this.signedRequest({ path: "/fapi/v1/listenKey", method: "POST", params: {} }); + return response.listenKey; + } + + async keepAliveListenKey(listenKey: string): Promise { + await this.signedRequest({ path: "/fapi/v1/listenKey", method: "PUT", params: { listenKey } }); + } + + async closeListenKey(listenKey: string): Promise { + await this.signedRequest({ path: "/fapi/v1/listenKey", method: "DELETE", params: { listenKey } }); + } + + private async signedRequest({ path, method, params }: { path: string; method: string; params: Record }): Promise { + const timestamp = Date.now(); + const payload = { ...params, timestamp, recvWindow: 5000 }; + const query = this.serialize(payload); + const signature = crypto.createHmac("sha256", this.apiSecret).update(query).digest("hex"); + const url = `${REST_BASE}${path}?${query}&signature=${signature}`; + const init: RequestInit = { + method, + headers: { + "X-MBX-APIKEY": this.apiKey, + "Content-Type": "application/x-www-form-urlencoded", + }, + }; + const response = await fetch(url, init); + if (!response.ok) { + const text = await response.text(); + throw new Error(`HTTP ${response.status} ${text}`); + } + return (await response.json()) as T; + } + + private serialize(params: Record): string { + return Object.keys(params) + .sort() + .map((key) => `${key}=${encodeURIComponent(String(params[key]))}`) + .join("&"); + } +} + +type DepthHandler = (depth: AsterDepth) => void; +type TickerHandler = (ticker: AsterTicker) => void; +type KlineHandler = (kline: AsterKline) => void; + +type StreamKind = "depth" | "ticker" | "kline"; + +interface StreamState { + stream: string; + kind: StreamKind; + symbol: string; + interval?: string; +} + +export class AsterPublicStreams { + private ws: WebSocket | null = null; + private reconnectTimeout: ReturnType | null = null; + private readonly streams = new Map(); + private readonly depthHandlers = new Map>(); + private readonly tickerHandlers = new Map>(); + private readonly klineHandlers = new Map>(); + private nextRequestId = 1; + + subscribeDepth(symbol: string, handler: DepthHandler): void { + const upper = symbol.toUpperCase(); + const stream = `${upper.toLowerCase()}@depth${DEFAULT_DEPTH_LEVEL}@${DEFAULT_DEPTH_SPEED}`; + this.addHandler(this.depthHandlers, upper, handler); + this.registerStream(stream, { stream, kind: "depth", symbol: upper }); + } + + subscribeTicker(symbol: string, handler: TickerHandler): void { + const upper = symbol.toUpperCase(); + const stream = `${upper.toLowerCase()}@miniTicker`; + this.addHandler(this.tickerHandlers, upper, handler); + this.registerStream(stream, { stream, kind: "ticker", symbol: upper }); + } + + subscribeKline(symbol: string, interval: string, handler: KlineHandler): void { + const upper = symbol.toUpperCase(); + const stream = `${upper.toLowerCase()}@kline_${interval}`; + this.addHandler(this.klineHandlers, `${upper}:${interval}`, handler); + this.registerStream(stream, { stream, kind: "kline", symbol: upper, interval }); + } + + private addHandler(map: Map>, key: string, handler: T): void { + let set = map.get(key); + if (!set) { + set = new Set(); + map.set(key, set); + } + set.add(handler); + this.ensureConnection(); + } + + private registerStream(stream: string, state: StreamState): void { + if (!this.streams.has(stream)) { + this.streams.set(stream, state); + this.send({ method: "SUBSCRIBE", params: [stream], id: this.nextRequestId++ }); + } + } + + private ensureConnection(): void { + if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { + return; + } + this.connect(); + } + + private connect(): void { + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + this.reconnectTimeout = null; + } + this.ws = new WebSocket(WS_PUBLIC_URL); + this.ws.onopen = () => { + const streams = Array.from(this.streams.keys()); + if (streams.length) { + this.send({ method: "SUBSCRIBE", params: streams, id: this.nextRequestId++ }); + } + }; + this.ws.onmessage = (event) => { + const payload = typeof event.data === "string" ? JSON.parse(event.data) : event.data; + if (!payload) return; + if (payload.result !== undefined) return; // subscription ack + const data = payload.data ?? payload; + if (!data.e) return; + switch (data.e) { + case "depthUpdate": + this.dispatchDepth(data); + break; + case "24hrMiniTicker": + this.dispatchTicker(data); + break; + case "kline": + this.dispatchKline(data); + break; + default: + break; + } + }; + this.ws.onclose = () => { + this.scheduleReconnect(); + }; + this.ws.onerror = () => { + this.ws?.close(); + }; + } + + private scheduleReconnect(): void { + if (this.reconnectTimeout) return; + this.reconnectTimeout = setTimeout(() => { + this.connect(); + }, RECONNECT_DELAY_MS); + } + + private send(message: Record): void { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(message)); + } + } + + private dispatchDepth(data: any): void { + const symbol = String(data.s ?? "").toUpperCase(); + const handlers = this.depthHandlers.get(symbol); + if (!handlers || !handlers.size) return; + const depth = toDepth(symbol, data); + handlers.forEach((handler) => handler(depth)); + } + + private dispatchTicker(data: any): void { + const symbol = String(data.s ?? "").toUpperCase(); + const handlers = this.tickerHandlers.get(symbol); + if (!handlers || !handlers.size) return; + const ticker = toTicker(data); + handlers.forEach((handler) => handler(ticker)); + } + + private dispatchKline(data: any): void { + const symbol = String(data.s ?? "").toUpperCase(); + const interval = data.k?.i ?? ""; + const key = `${symbol}:${interval}`; + const handlers = this.klineHandlers.get(key); + if (!handlers || !handlers.size) return; + const kline = toKline(data); + handlers.forEach((handler) => handler(kline)); + } +} + +interface AccountUpdatePayload { + B: Array<{ a: string; wb: string; cw: string; bc: string; wbBalance?: string; } & Record>; + P: Array<{ s: string; pa: string; ep: string; cr: string; up: string; mt: string; iw?: string; ps: string; pc?: string; } & Record>; +} + +interface OrderUpdatePayload extends Record {} + +export class AsterUserStream { + private readonly rest: AsterRestClient; + private listenKey: string | null = null; + private ws: WebSocket | null = null; + private keepAliveTimer: ReturnType | null = null; + private reconnectTimeout: ReturnType | null = null; + private readonly accountEvent = new SimpleEvent<{ eventTime: number; payload: AccountUpdatePayload }>(); + private readonly orderEvent = new SimpleEvent<{ eventTime: number; payload: OrderUpdatePayload }>(); + private isRunning = false; + + constructor(rest: AsterRestClient) { + this.rest = rest; + } + + onAccount(listener: (payload: { eventTime: number; payload: AccountUpdatePayload }) => void): void { + this.accountEvent.add(listener); + } + + onOrder(listener: (payload: { eventTime: number; payload: OrderUpdatePayload }) => void): void { + this.orderEvent.add(listener); + } + + async start(): Promise { + if (this.isRunning) return; + this.isRunning = true; + await this.ensureListenKey(); + this.openSocket(); + this.scheduleKeepAlive(); + } + + stop(): void { + this.isRunning = false; + if (this.keepAliveTimer) { + clearInterval(this.keepAliveTimer); + this.keepAliveTimer = null; + } + if (this.ws) { + this.ws.close(); + this.ws = null; + } + if (this.listenKey) { + void this.rest.closeListenKey(this.listenKey).catch(() => undefined); + this.listenKey = null; + } + } + + private async ensureListenKey(): Promise { + if (this.listenKey) return; + this.listenKey = await this.rest.getListenKey(); + } + + private scheduleKeepAlive(): void { + if (this.keepAliveTimer) return; + this.keepAliveTimer = setInterval(() => { + if (!this.listenKey) return; + void this.rest.keepAliveListenKey(this.listenKey).catch((error) => { + console.error("[AsterUserStream] keepAlive error", error); + }); + }, LISTEN_KEY_KEEPALIVE_MS / 2); + } + + private openSocket(): void { + if (!this.listenKey) return; + if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { + return; + } + const url = `${WS_LISTEN_KEY_URL}${this.listenKey}`; + this.ws = new WebSocket(url); + this.ws.onopen = () => { + // no-op + }; + this.ws.onmessage = (event) => { + const payload = typeof event.data === "string" ? JSON.parse(event.data) : event.data; + if (!payload) return; + if (payload === "ping") { + this.ws?.send("pong"); + return; + } + switch (payload.e) { + case "ACCOUNT_UPDATE": + this.accountEvent.emit({ eventTime: payload.E, payload: payload.a }); + break; + case "ORDER_TRADE_UPDATE": + this.orderEvent.emit({ eventTime: payload.E, payload: payload.o }); + break; + case "listenKeyExpired": + this.handleListenKeyExpired(); + break; + default: + break; + } + }; + this.ws.onclose = () => { + this.scheduleReconnect(); + }; + this.ws.onerror = () => { + this.ws?.close(); + }; + } + + private async handleListenKeyExpired(): Promise { + this.listenKey = null; + await this.ensureListenKey(); + this.openSocket(); + } + + private scheduleReconnect(): void { + if (!this.isRunning) return; + if (this.reconnectTimeout) return; + this.reconnectTimeout = setTimeout(() => { + this.reconnectTimeout = null; + this.openSocket(); + }, RECONNECT_DELAY_MS); + } +} + +function updateAccountSnapshot(snapshot: AsterAccountSnapshot | null, event: { eventTime: number; payload: AccountUpdatePayload }): AsterAccountSnapshot | null { + if (!snapshot) return snapshot; + const next = deepCloneAccount(snapshot); + if (!next) return snapshot; + next.updateTime = event.eventTime; + const balances = event.payload.B ?? []; + for (const balance of balances) { + const asset = balance.a; + let existing = next.assets.find((item) => item.asset === asset); + if (!existing) { + existing = { + asset, + walletBalance: "0", + unrealizedProfit: "0", + marginBalance: "0", + maintMargin: "0", + initialMargin: "0", + positionInitialMargin: "0", + openOrderInitialMargin: "0", + crossWalletBalance: "0", + crossUnPnl: "0", + availableBalance: "0", + maxWithdrawAmount: "0", + marginAvailable: true, + updateTime: event.eventTime, + } as any; + next.assets.push(existing); + } + if (balance.wb !== undefined) existing.walletBalance = balance.wb; + if (balance.cw !== undefined) existing.crossWalletBalance = balance.cw; + if (balance.bc !== undefined) existing.availableBalance = balance.bc; + existing.updateTime = event.eventTime; + } + + const positions = event.payload.P ?? []; + const unrealizedTotals = positions.reduce((acc, item) => acc + parseFloat(item.up ?? "0"), 0); + next.totalUnrealizedProfit = unrealizedTotals.toFixed(8); + + for (const position of positions) { + const symbol = position.s; + let existing = next.positions.find((item) => item.symbol === symbol && item.positionSide === position.ps); + if (!existing) { + existing = { + symbol, + positionAmt: "0", + entryPrice: "0", + unrealizedProfit: "0", + positionSide: position.ps, + updateTime: event.eventTime, + initialMargin: "0", + maintMargin: "0", + positionInitialMargin: "0", + openOrderInitialMargin: "0", + leverage: "", + isolated: position.mt === "isolated", + maxNotional: "0", + } as any; + next.positions.push(existing); + } + existing.positionAmt = position.pa ?? existing.positionAmt; + existing.entryPrice = position.ep ?? existing.entryPrice; + existing.unrealizedProfit = position.up ?? existing.unrealizedProfit; + existing.updateTime = event.eventTime; + } + return next; +} + +function mergeOrderSnapshot(map: Map, order: AsterOrder): void { + if (FINAL_ORDER_STATUSES.has(order.status)) { + map.delete(order.orderId); + } else { + map.set(order.orderId, order); + } +} + +export class AsterGateway { + private readonly rest: AsterRestClient; + private readonly publicStreams: AsterPublicStreams; + private readonly userStream: AsterUserStream; + + private accountSnapshot: AsterAccountSnapshot | null = null; + private readonly openOrders = new Map(); + + private readonly accountEvent = new SimpleEvent(); + private readonly ordersEvent = new SimpleEvent(); + private readonly depthEvents = new Map>(); + private readonly tickerEvents = new Map>(); + private readonly klineEvents = new Map>(); + + private readonly klineStores = new Map(); + private readonly klineRefreshTimers = new Map>(); + private readonly klineInitialFetches = new Map>(); + private initialized = false; + private initializing: Promise | null = null; + + constructor(options: { apiKey?: string; apiSecret?: string } = {}) { + this.rest = new AsterRestClient(options); + this.publicStreams = new AsterPublicStreams(); + this.userStream = new AsterUserStream(this.rest); + this.userStream.onAccount((event) => { + const updated = updateAccountSnapshot(this.accountSnapshot, event); + if (updated) { + this.accountSnapshot = updated; + this.accountEvent.emit(updated); + } + }); + this.userStream.onOrder((event) => { + const order = toOrderFromEvent(event.payload); + mergeOrderSnapshot(this.openOrders, order); + this.ordersEvent.emit(Array.from(this.openOrders.values())); + }); + } + + async ensureInitialized(symbol: string): Promise { + if (this.initialized) return; + if (this.initializing) return this.initializing; + this.initializing = (async () => { + this.accountSnapshot = await this.rest.getAccount(); + const orders = await this.rest.getOpenOrders(); + this.openOrders.clear(); + orders.forEach((order) => mergeOrderSnapshot(this.openOrders, order)); + this.initialized = true; + await this.userStream.start(); + this.accountEvent.emit(this.accountSnapshot!); + this.ordersEvent.emit(Array.from(this.openOrders.values())); + })().catch((error) => { + this.initializing = null; + throw error; + }); + return this.initializing; + } + + onAccount(listener: (snapshot: AsterAccountSnapshot) => void): void { + this.accountEvent.add(listener); + if (this.accountSnapshot) listener(this.accountSnapshot); + } + + onOrders(listener: (orders: AsterOrder[]) => void): void { + this.ordersEvent.add(listener); + listener(Array.from(this.openOrders.values())); + } + + onDepth(symbol: string, listener: (depth: AsterDepth) => void): void { + const upper = symbol.toUpperCase(); + let event = this.depthEvents.get(upper); + if (!event) { + event = new SimpleEvent(); + this.depthEvents.set(upper, event); + this.publicStreams.subscribeDepth(upper, (depth) => { + event?.emit(depth); + }); + } + event.add(listener); + } + + onTicker(symbol: string, listener: (ticker: AsterTicker) => void): void { + const upper = symbol.toUpperCase(); + let event = this.tickerEvents.get(upper); + if (!event) { + event = new SimpleEvent(); + this.tickerEvents.set(upper, event); + this.publicStreams.subscribeTicker(upper, (ticker) => { + event?.emit(ticker); + }); + } + event.add(listener); + } + + onKlines(symbol: string, interval: string, listener: (klines: AsterKline[]) => void): void { + const upper = symbol.toUpperCase(); + const key = `${upper}:${interval}`; + let event = this.klineEvents.get(key); + if (!event) { + event = new SimpleEvent(); + this.klineEvents.set(key, event); + this.publicStreams.subscribeKline(symbol, interval, (kline) => { + const storeKey = `${upper}:${interval}`; + let store = this.klineStores.get(storeKey); + if (!store) { + store = []; + this.klineStores.set(storeKey, store); + } + const index = store.findIndex((item) => item.openTime === kline.openTime); + if (index >= 0) { + store[index] = kline; + } else { + store.push(kline); + store.sort((a, b) => a.openTime - b.openTime); + if (store.length > DEFAULT_KLINE_LIMIT) { + store.shift(); + } + } + event?.emit([...store]); + }); + void this.ensureKlineSeed(upper, interval); + } + event.add(listener); + const existing = this.klineStores.get(key); + if (existing && existing.length) { + listener([...existing]); + } else { + void this.ensureKlineSeed(upper, interval); + } + } + + private ensureKlineSeed(symbol: string, interval: string): Promise { + const key = `${symbol}:${interval}`; + const existing = this.klineInitialFetches.get(key); + if (existing) return existing; + const task = (async () => { + try { + const klines = await this.rest.getKlines(symbol, interval, DEFAULT_KLINE_LIMIT); + klines.sort((a, b) => a.openTime - b.openTime); + this.klineStores.set(key, klines); + const event = this.klineEvents.get(key); + if (event) { + event.emit([...klines]); + } + } catch (error) { + console.error("[AsterGateway] seed klines failed", error); + } finally { + this.startKlineRefresh(symbol, interval); + } + })(); + this.klineInitialFetches.set(key, task); + return task; + } + + private startKlineRefresh(symbol: string, interval: string): void { + const key = `${symbol}:${interval}`; + if (this.klineRefreshTimers.has(key)) return; + const timer = setInterval(async () => { + try { + const klines = await this.rest.getKlines(symbol, interval, DEFAULT_KLINE_LIMIT); + klines.sort((a, b) => a.openTime - b.openTime); + this.klineStores.set(key, klines); + const event = this.klineEvents.get(key); + if (event) { + event.emit([...klines]); + } + } catch (error) { + console.error("[AsterGateway] refresh klines failed", error); + } + }, KLINE_REFRESH_INTERVAL_MS); + this.klineRefreshTimers.set(key, timer); + } + + getAccountSnapshot(): AsterAccountSnapshot | null { + return this.accountSnapshot; + } + + getOpenOrdersSnapshot(): AsterOrder[] { + return Array.from(this.openOrders.values()); + } + + async createOrder(params: CreateOrderParams): Promise { + const order = await this.rest.createOrder(params); + mergeOrderSnapshot(this.openOrders, order); + this.ordersEvent.emit(Array.from(this.openOrders.values())); + return order; + } + + async cancelOrder(params: { symbol: string; orderId?: number; origClientOrderId?: string }): Promise { + const result = await this.rest.cancelOrder(params); + mergeOrderSnapshot(this.openOrders, result); + this.ordersEvent.emit(Array.from(this.openOrders.values())); + } + + async cancelOrders(params: { symbol: string; orderIdList?: Array; origClientOrderIdList?: string[] }): Promise { + const results = await this.rest.cancelOrders(params); + results.forEach((order) => mergeOrderSnapshot(this.openOrders, order)); + this.ordersEvent.emit(Array.from(this.openOrders.values())); + } + + async cancelAllOrders(params: { symbol: string }): Promise { + await this.rest.cancelAllOrders(params); + for (const order of Array.from(this.openOrders.values())) { + if (order.symbol === params.symbol) { + this.openOrders.delete(order.orderId); + } + } + this.ordersEvent.emit(Array.from(this.openOrders.values())); + } +} diff --git a/src/exchanges/types.ts b/src/exchanges/types.ts new file mode 100644 index 0000000..3a92ae6 --- /dev/null +++ b/src/exchanges/types.ts @@ -0,0 +1,113 @@ +export type StringBoolean = "true" | "false"; + +export type OrderSide = "BUY" | "SELL"; +export type OrderType = + | "LIMIT" + | "MARKET" + | "STOP_MARKET" + | "TRAILING_STOP_MARKET"; +export type PositionSide = "BOTH" | "LONG" | "SHORT"; +export type TimeInForce = "GTC" | "IOC" | "FOK" | "GTX"; + +export interface CreateOrderParams { + symbol: string; + side: OrderSide; + type: OrderType; + quantity?: number; + price?: number; + stopPrice?: number; + activationPrice?: number; + callbackRate?: number; + timeInForce?: TimeInForce; + reduceOnly?: StringBoolean; + closePosition?: StringBoolean; +} + +export interface AsterAccountPosition { + symbol: string; + positionAmt: string; + entryPrice: string; + unrealizedProfit: string; + positionSide: PositionSide; + updateTime: number; +} + +export interface AsterAccountAsset { + asset: string; + walletBalance: string; + availableBalance: string; + updateTime: number; +} + +export interface AsterAccountSnapshot { + canTrade: boolean; + canDeposit: boolean; + canWithdraw: boolean; + updateTime: number; + totalWalletBalance: string; + totalUnrealizedProfit: string; + positions: AsterAccountPosition[]; + assets: AsterAccountAsset[]; +} + +export interface AsterDepthLevel extends Array { + 0: string; // price + 1: string; // quantity +} + +export interface AsterDepth { + lastUpdateId: number; + bids: AsterDepthLevel[]; + asks: AsterDepthLevel[]; + eventTime?: number; +} + +export interface AsterTicker { + symbol: string; + lastPrice: string; + openPrice: string; + highPrice: string; + lowPrice: string; + volume: string; + quoteVolume: string; + eventTime?: number; +} + +export interface AsterKline { + eventType?: string; + eventTime?: number; + symbol?: string; + interval?: string; + openTime: number; + open: string; + high: string; + low: string; + close: string; + volume: string; + closeTime: number; + firstTradeId?: number; + lastTradeId?: number; + quoteAssetVolume?: string; + numberOfTrades: number; + takerBuyBaseAssetVolume?: string; + takerBuyQuoteAssetVolume?: string; + isClosed?: boolean; +} + +export interface AsterOrder { + orderId: number; + clientOrderId: string; + symbol: string; + side: OrderSide; + type: OrderType; + status: string; + price: string; + origQty: string; + executedQty: string; + stopPrice: string; + time: number; + updateTime: number; + reduceOnly: boolean; + closePosition: boolean; + workingType?: string; +} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..7679907 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,6 @@ +import "dotenv/config"; +import React from "react"; +import { render } from "ink"; +import { App } from "./ui/App"; + +render(); diff --git a/src/state/trade-log.ts b/src/state/trade-log.ts new file mode 100644 index 0000000..2ef187c --- /dev/null +++ b/src/state/trade-log.ts @@ -0,0 +1,19 @@ +export interface TradeLogEntry { + time: string; + type: string; + detail: string; +} + +export function createTradeLog(maxEntries: number) { + const entries: TradeLogEntry[] = []; + function push(type: string, detail: string) { + entries.push({ time: new Date().toLocaleString(), type, detail }); + if (entries.length > maxEntries) { + entries.shift(); + } + } + function all() { + return entries; + } + return { push, all }; +} diff --git a/src/ui/App.tsx b/src/ui/App.tsx new file mode 100644 index 0000000..fc5eff1 --- /dev/null +++ b/src/ui/App.tsx @@ -0,0 +1,75 @@ +import React, { useState } from "react"; +import { Box, Text, useInput } from "ink"; +import { TrendApp } from "./TrendApp"; +import { MakerApp } from "./MakerApp"; + +interface StrategyOption { + id: "trend" | "maker"; + label: string; + description: string; + component: React.ComponentType<{ onExit: () => void }>; +} + +const STRATEGIES: StrategyOption[] = [ + { + id: "trend", + label: "趋势跟随策略 (SMA30)", + description: "监控均线信号,自动进出场并维护止损/止盈", + component: TrendApp, + }, + { + id: "maker", + label: "做市刷单策略", + description: "双边挂单提供流动性,自动追价与风控止损", + component: MakerApp, + }, +]; + +const inputSupported = Boolean(process.stdin && (process.stdin as any).isTTY); + +export function App() { + const [cursor, setCursor] = useState(0); + const [selected, setSelected] = useState(null); + + useInput( + (input, key) => { + if (selected) return; + if (key.upArrow) { + setCursor((prev) => (prev - 1 + STRATEGIES.length) % STRATEGIES.length); + } else if (key.downArrow) { + setCursor((prev) => (prev + 1) % STRATEGIES.length); + } else if (key.return) { + const strategy = STRATEGIES[cursor]; + if (strategy) { + setSelected(strategy); + } + } + }, + { isActive: inputSupported && !selected } + ); + + if (selected) { + const Selected = selected.component; + return setSelected(null)} />; + } + + return ( + + 请选择要运行的策略 + 使用 ↑/↓ 选择,回车开始,Ctrl+C 退出。 + + {STRATEGIES.map((strategy, index) => { + const active = index === cursor; + return ( + + + {active ? "➤" : " "} {strategy.label} + + {strategy.description} + + ); + })} + + + ); +} diff --git a/src/ui/MakerApp.tsx b/src/ui/MakerApp.tsx new file mode 100644 index 0000000..09eae7c --- /dev/null +++ b/src/ui/MakerApp.tsx @@ -0,0 +1,177 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Box, Text, useInput } from "ink"; +import { makerConfig } from "../config"; +import { AsterExchangeAdapter } from "../exchanges/aster-adapter"; +import { MakerEngine, type MakerEngineSnapshot } from "../core/maker-engine"; +import { DataTable, type TableColumn } from "./components/DataTable"; +import { formatNumber } from "../utils/format"; + +interface MakerAppProps { + onExit: () => void; +} + +const inputSupported = Boolean(process.stdin && (process.stdin as any).isTTY); + +export function MakerApp({ onExit }: MakerAppProps) { + const [snapshot, setSnapshot] = useState(null); + const [error, setError] = useState(null); + const engineRef = useRef(null); + + useInput( + (input, key) => { + if (key.escape) { + engineRef.current?.stop(); + onExit(); + } + }, + { isActive: inputSupported } + ); + + useEffect(() => { + const apiKey = process.env.ASTER_API_KEY; + const apiSecret = process.env.ASTER_API_SECRET; + if (!apiKey || !apiSecret) { + setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量")); + return; + } + try { + const adapter = new AsterExchangeAdapter({ + apiKey, + apiSecret, + symbol: makerConfig.symbol, + }); + const engine = new MakerEngine(makerConfig, adapter); + engineRef.current = engine; + setSnapshot(engine.getSnapshot()); + const handler = (next: MakerEngineSnapshot) => { + setSnapshot({ ...next, tradeLog: [...next.tradeLog] }); + }; + engine.on("update", handler); + engine.start(); + return () => { + engine.off("update", handler); + engine.stop(); + }; + } catch (err) { + console.error(err); + setError(err instanceof Error ? err : new Error(String(err))); + } + }, []); + + if (error) { + return ( + + 启动失败: {error.message} + 请检查环境变量和网络连通性。 + + ); + } + + if (!snapshot) { + return ( + + 正在初始化做市策略… + + ); + } + + const topBid = snapshot.topBid; + const topAsk = snapshot.topAsk; + const spreadDisplay = snapshot.spread != null ? `${snapshot.spread.toFixed(4)} USDT` : "-"; + const hasPosition = Math.abs(snapshot.position.positionAmt) > 1e-5; + const openOrderRows = snapshot.openOrders.map((order) => ({ + id: order.orderId, + side: order.side, + price: order.price, + qty: order.origQty, + filled: order.executedQty, + reduceOnly: order.reduceOnly ? "yes" : "no", + status: order.status, + })); + const openOrderColumns: TableColumn[] = [ + { key: "id", header: "ID", align: "right", minWidth: 6 }, + { key: "side", header: "Side", minWidth: 4 }, + { key: "price", header: "Price", align: "right", minWidth: 10 }, + { key: "qty", header: "Qty", align: "right", minWidth: 8 }, + { key: "filled", header: "Filled", align: "right", minWidth: 8 }, + { key: "reduceOnly", header: "RO", minWidth: 4 }, + { key: "status", header: "Status", minWidth: 10 }, + ]; + + const desiredRows = snapshot.desiredOrders.map((order, index) => ({ + index: index + 1, + side: order.side, + price: order.price, + amount: order.amount, + reduceOnly: order.reduceOnly ? "yes" : "no", + })); + const desiredColumns: TableColumn[] = [ + { key: "index", header: "#", align: "right", minWidth: 2 }, + { key: "side", header: "Side", minWidth: 4 }, + { key: "price", header: "Price", align: "right", minWidth: 10 }, + { key: "amount", header: "Qty", align: "right", minWidth: 8 }, + { key: "reduceOnly", header: "RO", minWidth: 4 }, + ]; + + const lastLogs = snapshot.tradeLog.slice(-10); + + return ( + + + Maker Strategy Dashboard + + 交易对: {snapshot.symbol} | 买一价: {formatNumber(topBid, 2)} | 卖一价: {formatNumber(topAsk, 2)} | 点差: {spreadDisplay} + + 状态: {snapshot.ready ? "实时运行" : "等待市场数据"} | 按 Esc 返回策略选择 + + + + + 持仓 + {hasPosition ? ( + <> + + 方向: {snapshot.position.positionAmt > 0 ? "多" : "空"} | 数量: {formatNumber(Math.abs(snapshot.position.positionAmt), 4)} | 开仓价: {formatNumber(snapshot.position.entryPrice, 2)} + + + 浮动盈亏: {formatNumber(snapshot.pnl, 4)} USDT | 账户未实现盈亏: {formatNumber(snapshot.accountUnrealized, 4)} USDT + + + ) : ( + 当前无持仓 + )} + + + 目标挂单 + {desiredRows.length > 0 ? ( + + ) : ( + 暂无目标挂单 + )} + + + + + 当前挂单 + {openOrderRows.length > 0 ? ( + + ) : ( + 暂无挂单 + )} + + + + 最近事件 + {lastLogs.length > 0 ? ( + lastLogs.map((item, index) => ( + + [{item.time}] [{item.type}] {item.detail} + + )) + ) : ( + 暂无日志 + )} + + + ); +} diff --git a/src/ui/TrendApp.tsx b/src/ui/TrendApp.tsx new file mode 100644 index 0000000..83f1267 --- /dev/null +++ b/src/ui/TrendApp.tsx @@ -0,0 +1,164 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Box, Text, useInput } from "ink"; +import { tradingConfig } from "../config"; +import { AsterExchangeAdapter } from "../exchanges/aster-adapter"; +import { TrendEngine, type TrendEngineSnapshot } from "../core/trend-engine"; +import { formatNumber } from "../utils/format"; +import { DataTable, type TableColumn } from "./components/DataTable"; + +const READY_MESSAGE = "正在等待交易所推送数据…"; + +interface TrendAppProps { + onExit: () => void; +} + +const inputSupported = Boolean(process.stdin && (process.stdin as any).isTTY); + +export function TrendApp({ onExit }: TrendAppProps) { + const [snapshot, setSnapshot] = useState(null); + const [error, setError] = useState(null); + const engineRef = useRef(null); + + useInput( + (input, key) => { + if (key.escape) { + engineRef.current?.stop(); + onExit(); + } + }, + { isActive: inputSupported } + ); + + useEffect(() => { + const apiKey = process.env.ASTER_API_KEY; + const apiSecret = process.env.ASTER_API_SECRET; + if (!apiKey || !apiSecret) { + setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量")); + return; + } + try { + const adapter = new AsterExchangeAdapter({ + apiKey, + apiSecret, + symbol: tradingConfig.symbol, + }); + const engine = new TrendEngine(tradingConfig, adapter); + engineRef.current = engine; + setSnapshot(engine.getSnapshot()); + const handler = (next: TrendEngineSnapshot) => { + setSnapshot({ ...next, tradeLog: [...next.tradeLog] }); + }; + engine.on("update", handler); + engine.start(); + return () => { + engine.off("update", handler); + engine.stop(); + }; + } catch (err) { + console.error(err); + setError(err instanceof Error ? err : new Error(String(err))); + } + }, []); + + if (error) { + return ( + + 启动失败: {error.message} + 请检查环境变量和网络连通性。 + + ); + } + + if (!snapshot) { + return ( + + 正在初始化趋势策略… + + ); + } + + const { position, tradeLog, openOrders, trend, ready, lastPrice, sma30 } = snapshot; + const hasPosition = Math.abs(position.positionAmt) > 1e-5; + const lastLogs = tradeLog.slice(-10); + const orderRows = openOrders.slice(0, 8).map((order) => ({ + id: order.orderId, + side: order.side, + type: order.type, + price: order.price, + qty: order.origQty, + filled: order.executedQty, + status: order.status, + })); + const orderColumns: TableColumn[] = [ + { key: "id", header: "ID", align: "right", minWidth: 6 }, + { key: "side", header: "Side", minWidth: 4 }, + { key: "type", header: "Type", minWidth: 10 }, + { key: "price", header: "Price", align: "right", minWidth: 10 }, + { key: "qty", header: "Qty", align: "right", minWidth: 8 }, + { key: "filled", header: "Filled", align: "right", minWidth: 8 }, + { key: "status", header: "Status", minWidth: 10 }, + ]; + + return ( + + + Trend Strategy Dashboard + + 交易对: {snapshot.symbol} | 最近价格: {formatNumber(lastPrice, 2)} | SMA30: {formatNumber(sma30, 2)} | 趋势: {trend} + + 状态: {ready ? "实时运行" : READY_MESSAGE} | 按 Esc 返回策略选择 + + + + + 持仓 + {hasPosition ? ( + <> + + 方向: {position.positionAmt > 0 ? "多" : "空"} | 数量: {formatNumber(Math.abs(position.positionAmt), 4)} | 开仓价: {formatNumber(position.entryPrice, 2)} + + + 浮动盈亏: {formatNumber(snapshot.pnl, 4)} USDT | 账户未实现盈亏: {formatNumber(snapshot.unrealized, 4)} USDT + + + ) : ( + 当前无持仓 + )} + + + 绩效 + + 累计交易次数: {snapshot.totalTrades} | 累计收益: {formatNumber(snapshot.totalProfit, 4)} USDT + + {snapshot.lastOpenSignal.side ? ( + + 最近开仓信号: {snapshot.lastOpenSignal.side} @ {formatNumber(snapshot.lastOpenSignal.price, 2)} + + ) : null} + + + + + 当前挂单 + {orderRows.length > 0 ? ( + + ) : ( + 暂无挂单 + )} + + + + 最近交易与事件 + {lastLogs.length > 0 ? ( + lastLogs.map((item, index) => ( + + [{item.time}] [{item.type}] {item.detail} + + )) + ) : ( + 暂无日志 + )} + + + ); +} diff --git a/src/ui/components/DataTable.tsx b/src/ui/components/DataTable.tsx new file mode 100644 index 0000000..9e5e82a --- /dev/null +++ b/src/ui/components/DataTable.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { Box, Text } from "ink"; + +type Align = "left" | "right"; + +export interface TableColumn { + key: string; + header: string; + align?: Align; + minWidth?: number; +} + +export interface DataTableProps> { + columns: TableColumn[]; + rows: Row[]; +} + +function formatCell(value: unknown): string { + if (value == null) return ""; + if (typeof value === "number") { + if (Number.isInteger(value)) return value.toString(); + return value.toFixed(4).replace(/\.0+$/, ".0"); + } + return String(value); +} + +function pad(text: string, width: number, align: Align): string { + if (text.length >= width) return text; + const padding = " ".repeat(width - text.length); + return align === "right" ? padding + text : text + padding; +} + +export function DataTable>({ columns, rows }: DataTableProps) { + const widths = columns.map((col) => { + const headerLength = col.header.length; + const minWidth = col.minWidth ?? 0; + const contentLength = rows.reduce((max, row) => { + const cell = formatCell(row[col.key]); + return Math.max(max, cell.length); + }, 0); + return Math.max(headerLength, contentLength, minWidth); + }); + + return ( + + + {columns + .map((col, index) => pad(col.header, widths[index], col.align ?? "left")) + .join(" ")} + + {rows.map((row, rowIndex) => ( + + {columns + .map((col, index) => { + const align = col.align ?? "left"; + const cell = formatCell(row[col.key]); + return pad(cell, widths[index], align); + }) + .join(" ")} + + ))} + + ); +} diff --git a/src/utils/format.ts b/src/utils/format.ts new file mode 100644 index 0000000..d246225 --- /dev/null +++ b/src/utils/format.ts @@ -0,0 +1,8 @@ +export function formatNumber(value: number | null | undefined, digits = 4, fallback = "-"): string { + if (value == null || Number.isNaN(value)) return fallback; + return Number(value).toFixed(digits); +} + +export function formatTrendLabel(trend: "做多" | "做空" | "无信号"): string { + return trend; +} diff --git a/src/utils/math.ts b/src/utils/math.ts new file mode 100644 index 0000000..0aa0d85 --- /dev/null +++ b/src/utils/math.ts @@ -0,0 +1,11 @@ +export function toPrice1Decimal(price: number): number { + return Math.floor(price * 10) / 10; +} + +export function toQty3Decimal(qty: number): number { + return Math.floor(qty * 1000) / 1000; +} + +export function isNearlyZero(value: number, epsilon = 1e-5): boolean { + return Math.abs(value) < epsilon; +} diff --git a/src/utils/strategy.ts b/src/utils/strategy.ts new file mode 100644 index 0000000..505e3a2 --- /dev/null +++ b/src/utils/strategy.ts @@ -0,0 +1,43 @@ +import { AsterAccountSnapshot, AsterKline } from "../exchanges/types"; + +export interface PositionSnapshot { + positionAmt: number; + entryPrice: number; + unrealizedProfit: number; +} + +export function getPosition(snapshot: AsterAccountSnapshot | null, symbol: string): PositionSnapshot { + if (!snapshot) { + return { positionAmt: 0, entryPrice: 0, unrealizedProfit: 0 }; + } + const pos = snapshot.positions?.find((p) => p.symbol === symbol); + if (!pos) { + return { positionAmt: 0, entryPrice: 0, unrealizedProfit: 0 }; + } + return { + positionAmt: Number(pos.positionAmt), + entryPrice: Number(pos.entryPrice), + unrealizedProfit: Number(pos.unrealizedProfit), + }; +} + +export function getSMA(values: AsterKline[], length: number): number | null { + if (!values || values.length < length) return null; + const closes = values.slice(-length).map((k) => Number(k.close)); + const sum = closes.reduce((acc, current) => acc + current, 0); + return sum / closes.length; +} + +export function calcStopLossPrice(entryPrice: number, qty: number, side: "long" | "short", loss: number): number { + if (side === "long") { + return entryPrice - loss / qty; + } + return entryPrice + loss / Math.abs(qty); +} + +export function calcTrailingActivationPrice(entryPrice: number, qty: number, side: "long" | "short", profit: number): number { + if (side === "long") { + return entryPrice + profit / qty; + } + return entryPrice - profit / Math.abs(qty); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}