12 Commits
Author SHA1 Message Date
discountry 792351ab8a Add Liquidity Maker strategy and related configurations
- Introduced a new `LiquidityMakerConfig` interface and corresponding configuration settings in `config.ts`.
- Updated CLI argument handling to include the new "liquidity-maker" strategy option.
- Implemented the `LiquidityMakerEngine` class to manage the liquidity making strategy, including order handling and risk management.
- Added a new `LiquidityMakerApp` component for user interaction and display of strategy status.
- Enhanced internationalization support with translations for the liquidity maker strategy.
- Updated the main application to integrate the new liquidity maker strategy into the existing framework.
2026-01-14 00:56:29 +08:00
discountry 4915dc574e Implement precision error handling in MakerPointsEngine
- Added a new `isPrecisionError` function to identify precision-related errors in the error utility module.
- Updated the MakerPointsEngine to handle precision errors by logging warnings and synchronizing precision when such errors occur during order processing and stop-loss execution.
- Enhanced the `syncPrecision` method to allow forced synchronization, improving the handling of precision-related issues.
2026-01-13 20:23:18 +08:00
discountry 9866e8068f Clarify instructions in Maker Points guide regarding the proxy wallet private key format and environment variable setup. Emphasize that the private key should be copied as is, without the '0x' prefix, to enhance user understanding and security practices. 2026-01-12 18:03:26 +08:00
discountry 099af3ce01 Update Maker Points guide to clarify proxy wallet private key format and environment variable instructions. Specify that the private key should generally not include the '0x' prefix, enhancing user understanding and security practices. 2026-01-12 18:02:05 +08:00
discountry 445e634aa1 Refactor Telegram notification handling and remove unused functions
- Removed deprecated functions for masking sensitive data and previewing text, streamlining the Telegram notification process.
- Simplified logging by eliminating unnecessary console outputs related to notification configuration and sending.
- Updated the `TelegramNotifier` class to enhance clarity and maintainability, focusing on essential notification functionality.
2026-01-12 12:39:37 +08:00
discountry 4bb1fee995 Refactor Telegram notification handling in MakerPointsEngine
- Introduced a dedicated `notify` method to streamline notification sending and improve logging for Telegram notifications.
- Added a new environment variable check for enabling debug logging of Telegram notifications.
- Enhanced logging to include detailed information about notification attempts, including masked sensitive data for security.
- Updated various notification calls to utilize the new `notify` method, ensuring consistent logging and functionality.
2026-01-12 12:23:24 +08:00
discountry aad14395e0 Enhance Telegram notification functionality
- Introduced functions to mask sensitive information and preview notification text for improved logging and security.
- Added detailed logging for notification sending process, including configuration details and response handling.
- Implemented checks to prevent sending notifications when bot token or chat ID is missing, with appropriate warnings logged.
2026-01-12 12:12:14 +08:00
discountry 598f2a0eb6 Add token expiry and Telegram notification features
- Introduced `STANDX_TOKEN_EXPIRY` configuration to manage token expiration, including handling logic for active, expired, and silent states.
- Implemented Telegram notifications for key events such as order filled, position opened/closed, stop loss triggered, and token expiration.
- Updated Maker Points engine to integrate token expiry checks and notification sending, enhancing user awareness of trading conditions.
- Enhanced documentation to include details on configuring token expiry and Telegram notifications for improved user guidance.
2026-01-10 12:44:52 +08:00
discountry cb1cef6f1b Revise Maker Points guide to provide a comprehensive step-by-step tutorial for new users. Update installation instructions for Bun, enhance clarity on obtaining StandX login credentials, and improve environment variable configuration details. Add safety tips and common troubleshooting questions to support user onboarding. 2026-01-09 01:11:10 +08:00
discountry fd034d493f Enhance README with language setting instructions, updated referral links, and additional documentation for StandX and Nado exchanges. Clarify environment variable setup and improve formatting for better readability. 2026-01-07 23:39:40 +08:00
discountry 2551670874 Update README and Maker Points guide to clarify the export of StandX login credentials, specifying token and proxy wallet private key for enhanced user security. 2026-01-07 02:46:38 +08:00
discountry 3ec7e9b8d6 Update Maker Points guide to include details on exporting the generated proxy wallet private key along with the token, enhancing security instructions for users. 2026-01-06 21:49:28 +08:00
17 changed files with 2485 additions and 78 deletions
+9
View File
@@ -16,6 +16,10 @@ STANDX_SYMBOL=BTC-USD
# STANDX_SESSION_ID= # STANDX_SESSION_ID=
# Optional: request signing key (ed25519 private key, hex or base64) # Optional: request signing key (ed25519 private key, hex or base64)
# STANDX_REQUEST_PRIVATE_KEY= # STANDX_REQUEST_PRIVATE_KEY=
# Token expiry timestamp (Unix seconds). When expired: cancels all orders, stops new orders.
# If position exists, continues close/stop-loss logic. If no position/orders, enters silent mode.
# Example: STANDX_TOKEN_EXPIRY=1737092800
# STANDX_TOKEN_EXPIRY=
# Core trading symbol and sizing # Core trading symbol and sizing
TRADE_SYMBOL=BTCUSDT # Trading pair symbol TRADE_SYMBOL=BTCUSDT # Trading pair symbol
@@ -154,3 +158,8 @@ NADO_MIN_SIZE_POLICY=adjust
# NADO_ARCHIVE_URL=https://archive.prod.nado.xyz/v1 # NADO_ARCHIVE_URL=https://archive.prod.nado.xyz/v1
# NADO_TRIGGER_URL=https://trigger.prod.nado.xyz/v1 # NADO_TRIGGER_URL=https://trigger.prod.nado.xyz/v1
# NADO_DEBUG=false # NADO_DEBUG=false
# Telegram notification configuration
# TELEGRAM_BOT_TOKEN= # Telegram bot token from @BotFather
# TELEGRAM_CHAT_ID= # Chat ID to receive notifications
# TELEGRAM_ACCOUNT_LABEL= # Account label to distinguish multiple bot instances (e.g., "Account-A")
+1 -1
View File
@@ -120,7 +120,7 @@ curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh
1. 打开 https://standx.ritmex.one/ 1. 打开 https://standx.ritmex.one/
2. 连接钱包 2. 连接钱包
3. 点击“登录” 3. 点击“登录”
4. 导出登录信息里面会包含 token和代理钱包私钥,代理钱包仅用于交易签名,有效保证资产钱包安全 4. 导出登录信息里面会包含 token(STANDX_TOKEN) 和代理钱包私钥(STANDX_REQUEST_PRIVATE_KEY),代理钱包仅用于交易签名,有效保证资产钱包安全
请妥善保存,不要分享给他人 请妥善保存,不要分享给他人
+29 -11
View File
@@ -1,7 +1,11 @@
# ritmex-bot # ritmex-bot
**Language Setting**: Set `LANG=en` in your `.env` file to display the CLI interface in English.
A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend engine, a Guardian stop sentinel, and two market-making modes. It offers instant restarts, realtime market data, structured logging, and an Ink-based CLI dashboard. A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend engine, a Guardian stop sentinel, and two market-making modes. It offers instant restarts, realtime market data, structured logging, and an Ink-based CLI dashboard.
If you'd like to support this project and get fee discounts, please consider using these referral links:
* [Lighter referral link](https://app.lighter.xyz/?referral=111909FA) * [Lighter referral link](https://app.lighter.xyz/?referral=111909FA)
* [Aster referral link](https://www.asterdex.com/en/referral/4665f3) * [Aster referral link](https://www.asterdex.com/en/referral/4665f3)
* [StandX referral link](https://standx.com/referral?code=xingxingjun) * [StandX referral link](https://standx.com/referral?code=xingxingjun)
@@ -11,11 +15,11 @@ A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend en
* [Backpack referral link](https://backpack.exchange/join/ritmex) * [Backpack referral link](https://backpack.exchange/join/ritmex)
* [edgex referral link](https://pro.edgex.exchange/referral/BULL) * [edgex referral link](https://pro.edgex.exchange/referral/BULL)
* [Paradex referral link](https://paradex.io/ref/xingxingjun) * [Paradex referral link](https://paradex.io/ref/xingxingjun)
* [Apex referral link](https://join.omni.apex.exchange/RITHMEX) * [Apex referral link](https://join.omni.apex.exchange/SEA)
## Documentation Map ## Documentation Map
- [中文 README](README.md)
- [Beginner-friendly Quick Start](simple-readme.md) - [Beginner-friendly Quick Start](simple-readme.md)
- [Grid Trading Strategy Guide](grid-trading.md)
## Highlights ## Highlights
- **Live data & risk sync** via websockets with REST fallbacks and full reconciliation on restart. - **Live data & risk sync** via websockets with REST fallbacks and full reconciliation on restart.
@@ -35,7 +39,7 @@ A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend en
| Paradex | StarkEx perpetuals | `PARADEX_PRIVATE_KEY`, `PARADEX_WALLET_ADDRESS` | Toggle `PARADEX_SANDBOX=true` for the testnet | | Paradex | StarkEx perpetuals | `PARADEX_PRIVATE_KEY`, `PARADEX_WALLET_ADDRESS` | Toggle `PARADEX_SANDBOX=true` for the testnet |
## Requirements ## Requirements
- Bun 1.2 (both `bun` and `bunx` on PATH) - Bun >= 1.2 (both `bun` and `bunx` on PATH)
- macOS, Linux, or Windows via WSL (native Windows works but WSL is recommended) - macOS, Linux, or Windows via WSL (native Windows works but WSL is recommended)
- Node.js is optional unless your tooling requires it - Node.js is optional unless your tooling requires it
@@ -96,6 +100,7 @@ The script installs Bun, project dependencies, collects Aster API credentials, g
> ``` > ```
## Exchange Setup Guides ## Exchange Setup Guides
### Aster ### Aster
1. Keep `EXCHANGE=aster` (default value). 1. Keep `EXCHANGE=aster` (default value).
2. Supply `ASTER_API_KEY` and `ASTER_API_SECRET`. 2. Supply `ASTER_API_KEY` and `ASTER_API_SECRET`.
@@ -103,11 +108,24 @@ The script installs Bun, project dependencies, collects Aster API credentials, g
4. The bootstrap script auto-populates these variables; manual installs must maintain them. 4. The bootstrap script auto-populates these variables; manual installs must maintain them.
### StandX ### StandX
* [StandX Maker Points Strategy Guide](docs/standx/maker-points-guide.md)
The strategy requires a StandX login token to place orders.
How to obtain the token:
1. Open https://standx.ritmex.one/
2. Connect your wallet
3. Click "Login"
4. Export your login credentials, which will include the token (`STANDX_TOKEN`) and proxy wallet private key (`STANDX_REQUEST_PRIVATE_KEY`). The proxy wallet is only used for trade signatures, keeping your main wallet secure.
Please keep these credentials safe and do not share them with anyone.
1. Set `EXCHANGE=standx`. 1. Set `EXCHANGE=standx`.
2. Provide `STANDX_TOKEN` (JWT token for perps API). 2. Provide `STANDX_TOKEN` (JWT token for perps API).
3. Set `STANDX_SYMBOL` (defaults to `BTC-USD`) and align `PRICE_TICK` / `QTY_STEP`. 3. Provide `STANDX_REQUEST_PRIVATE_KEY` (proxy wallet private key).
4. Optional: `STANDX_BASE_URL`, `STANDX_WS_URL`, or `STANDX_SESSION_ID` for custom endpoints. 4. Set `STANDX_SYMBOL` (defaults to `BTC-USD`) and align `PRICE_TICK` / `QTY_STEP`.
5. Optional: `STANDX_REQUEST_PRIVATE_KEY` if the API requires body signatures. 5. Optional: `STANDX_BASE_URL`, `STANDX_WS_URL`, or `STANDX_SESSION_ID` for custom endpoints.
### GRVT ### GRVT
1. Set `EXCHANGE=grvt` inside `.env`. 1. Set `EXCHANGE=grvt` inside `.env`.
@@ -117,25 +135,25 @@ The script installs Bun, project dependencies, collects Aster API credentials, g
### Lighter ### Lighter
1. Set `EXCHANGE=lighter`. 1. Set `EXCHANGE=lighter`.
2. Provide `LIGHTER_ACCOUNT_INDEX` and `LIGHTER_API_PRIVATE_KEY` (40-byte hex private key). 2. Provide `LIGHTER_ACCOUNT_INDEX` and `LIGHTER_API_PRIVATE_KEY` (40-byte hex private key). `LIGHTER_ACCOUNT_INDEX` is your account index, which you can find by opening DevTools (F12) on the official website and observing API requests. `LIGHTER_API_PRIVATE_KEY` is your API private key.
3. Switch `LIGHTER_ENV` to `mainnet`, `staging`, or `dev` when necessary; override `LIGHTER_BASE_URL` if endpoints differ. 3. Switch `LIGHTER_ENV` to `mainnet`, `staging`, or `dev` when necessary; override `LIGHTER_BASE_URL` if endpoints differ.
4. `LIGHTER_SYMBOL` defaults to `BTCUSDT`; override price/size decimals when markets differ. 4. `LIGHTER_SYMBOL` defaults to `BTCUSDT`; override price/size decimals when markets differ.
### Backpack ### Backpack
1. Set `EXCHANGE=backpack`. 1. Set `EXCHANGE=backpack`.
2. Populate `BACKPACK_API_KEY`, `BACKPACK_API_SECRET`, and `BACKPACK_PASSWORD`; add `BACKPACK_SUBACCOUNT` if you trade from a subaccount. 2. Populate `BACKPACK_API_KEY`, `BACKPACK_API_SECRET`, and `BACKPACK_PASSWORD`; add `BACKPACK_SUBACCOUNT` if you trade from a subaccount (defaults to main account ID).
3. Toggle `BACKPACK_SANDBOX=true` for the sandbox environment and verify `BACKPACK_SYMBOL` matches the contract (defaults to `BTC_USD_PERP`). 3. Toggle `BACKPACK_SANDBOX=true` for the sandbox environment and verify `BACKPACK_SYMBOL` matches the contract (defaults to `BTC_USD_PERP`).
4. Enable `BACKPACK_DEBUG=true` for verbose adapter logging. 4. Enable `BACKPACK_DEBUG=true` for verbose adapter logging.
### Paradex ### Paradex
1. Set `EXCHANGE=paradex`. 1. Set `EXCHANGE=paradex`.
2. Provide `PARADEX_PRIVATE_KEY` (EVM private key) and `PARADEX_WALLET_ADDRESS`. 2. Provide `PARADEX_PRIVATE_KEY` (EVM private key) and `PARADEX_WALLET_ADDRESS`. Note: These are your EVM wallet address and private key. It is recommended to create a brand new wallet and avoid storing unrelated assets in it.
3. The adapter connects to mainnet by default; enable `PARADEX_SANDBOX=true` and adjust `PARADEX_SYMBOL` for testnet usage. 3. The adapter connects to mainnet by default; enable `PARADEX_SANDBOX=true` and adjust `PARADEX_SYMBOL` for testnet usage.
4. Advanced tuning: use `PARADEX_USE_PRO`, `PARADEX_RECONNECT_DELAY_MS`, or debug flags as needed. 4. Advanced tuning: use `PARADEX_USE_PRO`, `PARADEX_RECONNECT_DELAY_MS`, or debug flags as needed.
### Nado ### Nado
1. Set `EXCHANGE=nado`. 1. Set `EXCHANGE=nado`.
2. On the Nado web app, open DevTools switch to the `Application` tab `Local Storage`, locate `nado.userSettings`, then grab the `privateKey` field from its JSON value and paste it into `.env` as `NADO_SIGNER_PRIVATE_KEY`. 2. On the Nado web app (trading interface), open DevTools (F12) -> switch to the `Application` tab -> `Local Storage`, locate `nado.userSettings`, then grab the `privateKey` field from its JSON value and paste it into `.env` as `NADO_SIGNER_PRIVATE_KEY`.
3. Provide `NADO_SUBACCOUNT_OWNER` (or `NADO_EVM_ADDRESS`). 3. Provide `NADO_SUBACCOUNT_OWNER` (or `NADO_EVM_ADDRESS`).
4. Select network via `NADO_ENV=inkMainnet` (mainnet) or `inkTestnet` (testnet). 4. Select network via `NADO_ENV=inkMainnet` (mainnet) or `inkTestnet` (testnet).
5. Set `NADO_SYMBOL` using Nado product symbols like `BTC-PERP` (it also accepts `BTCUSDT0` and maps it to `BTC-PERP`). 5. Set `NADO_SYMBOL` using Nado product symbols like `BTC-PERP` (it also accepts `BTCUSDT0` and maps it to `BTC-PERP`).
@@ -187,7 +205,7 @@ bun x vitest --watch
``` ```
## Troubleshooting ## Troubleshooting
- Keep at least 50100 USDT in the account before deploying a live strategy. - Keep at least 50-100 USDT in the account before deploying a live strategy.
- Configure leverage on the exchange manually (~50x is recommended); the bot will not change it. - Configure leverage on the exchange manually (~50x is recommended); the bot will not change it.
- Ensure your server or workstation clock is in sync to avoid signature errors. - Ensure your server or workstation clock is in sync to avoid signature errors.
- Accounts must run in one-way position mode. - Accounts must run in one-way position mode.
+298 -55
View File
@@ -1,112 +1,355 @@
# StandX 做市积分策略使用教程(新手版) # StandX 做市积分策略使用教程(超详细新手版)
本教程用于帮助你快速上手 StandX 做市积分策略(Maker Points)。无需交易经验,照着步骤做即可。 本教程**手把手** 教你如何运行 StandX 做市积分策略。每一步都有详细说明,按顺序操作即可。
--- ---
## 1. 准备工作 ## 第一步:安装 Bun(运行环境)
### 1.1 安装 Bun 本项目需要 Bun 才能运行。
本项目使用 Bun 运行。
如果你还没有安装 Bun,请先参考官方文档完成安装 ### macOS / Linux 用户
- https://bun.sh/ 打开终端,复制粘贴以下命令后按回车:
安装完成后,进入项目目录执行:
```bash ```bash
curl -fsSL https://bun.sh/install | bash
```
### Windows 用户:
打开 PowerShell,复制粘贴以下命令后按回车:
```powershell
powershell -c "irm bun.sh/install.ps1 | iex"
```
安装完成后,**关闭终端,重新打开一个新的终端窗口**,然后输入:
```bash
bun -v
```
如果显示版本号(如 `1.2.x`),说明安装成功。
---
## 第二步:下载项目并安装依赖
```bash
git clone https://github.com/discountry/ritmex-bot.git
cd ritmex-bot
bun install bun install
``` ```
### 1.2 获取 StandX 登录凭证(Token ---
策略需要 StandX 的登录 token 才能下单。
获取方式: ## 第三步:获取 StandX 登录凭证(最重要的一步)
1. 打开 https://standx.ritmex.one/
2. 连接钱包
3. 点击“登录”
4. 导出登录信息(里面会包含 token)
请妥善保存,不要分享给他人。 > ⚠️ **这一步是 90% 新手卡住的地方,请仔细阅读!**
>
> ⚠️ **这一步是 90% 新手卡住的地方,请仔细阅读!**
>
> ⚠️ **这一步是 90% 新手卡住的地方,请仔细阅读!**
策略需要两样东西才能帮你下单:
1. **TOKEN**(登录令牌)
2. **代理钱包私钥**(用于签名交易)
### 获取步骤(图文说明):
#### 3.1 打开专用登录页面
在浏览器打开这个网址:
```
https://standx.ritmex.one/
```
> **注意:不是 standx.com!是 standx.ritmex.one**
>
> **注意:不是 standx.com!是 standx.ritmex.one**
>
> **注意:不是 standx.com!是 standx.ritmex.one**
#### 3.2 连接你的钱包
点击页面上的 **"连接钱包"** 按钮,使用 MetaMask 或其他钱包连接。
#### 3.3 点击登录
连接钱包后,点击 **"登录"** 按钮。钱包会弹出签名请求,确认签名。
#### 3.4 导出登录信息(关键!)
登录成功后,页面上会出现 **"导出登录信息"** 按钮,**点击它**
你会看到类似这样的内容:
```
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx...(很长一串)
代理钱包私钥: 1234567890abcdef...64 位十六进制字符)
```
> 🔴 **请把这两个值复制保存下来!**
>
> 🔴 **请把这两个值复制保存下来!**
>
> 🔴 **请把这两个值复制保存下来!**
### 什么是代理钱包?
- 代理钱包是系统 **自动为你生成** 的一个临时钱包
-**只用于签名交易**,不存放你的资金
- 你的资产仍然在你自己的钱包里,非常安全
- **你不需要手动创建**,登录时系统会自动生成
--- ---
## 2. 配置环境变量 ## 第四步:配置环境变量
在项目根目录新建 `.env` 文件(或修改已有 `.env`),填入以下配置: 在项目根目录创建一个 `.env` 文件(如果已存在就修改它)。
### 4.1 创建/编辑 .env 文件
**macOS / Linux**
```bash
nano .env
```
**Windows**
用记事本打开项目文件夹,新建一个文本文件,命名为 `.env`(注意前面有个点)
### 4.2 填入以下内容
> ⚠️ **请务必把下面的示例值替换成你自己的!**
>
> ⚠️ **请务必把下面的示例值替换成你自己的!**
>
> ⚠️ **请务必把下面的示例值替换成你自己的!**
```bash ```bash
# ===== 交易所设置 =====
EXCHANGE=standx EXCHANGE=standx
STANDX_REQUEST_PRIVATE_KEY=你的代理钱包私钥
STANDX_TOKEN=你的token # ===== 你的登录凭证(第三步获取的) =====
# 把下面的 "你的TOKEN" 替换成你导出的 Token(很长一串以 eyJ 开头的)
STANDX_TOKEN=你的TOKEN
# 把下面的 "你的私钥" 替换成页面中标注为 STANDX_REQUEST_PRIVATE_KEY 的那一段内容(按页面原样粘贴即可)
STANDX_REQUEST_PRIVATE_KEY=你的 STANDX_REQUEST_PRIVATE_KEY
# ===== 交易品种 =====
STANDX_SYMBOL=BTC-USD STANDX_SYMBOL=BTC-USD
# ===== 策略参数(新手直接用默认值就行) =====
MAKER_POINTS_ORDER_AMOUNT=0.01 MAKER_POINTS_ORDER_AMOUNT=0.01
MAKER_POINTS_CLOSE_THRESHOLD=0.1 MAKER_POINTS_CLOSE_THRESHOLD=0.1
MAKER_POINTS_STOP_LOSS_USD=0 MAKER_POINTS_STOP_LOSS_USD=0
MAKER_POINTS_MIN_REPRICE_BPS=3 MAKER_POINTS_MIN_REPRICE_BPS=3
# ===== 挂单档位开关 =====
MAKER_POINTS_BAND_0_10=true MAKER_POINTS_BAND_0_10=true
MAKER_POINTS_BAND_10_30=true MAKER_POINTS_BAND_10_30=true
MAKER_POINTS_BAND_30_100=true MAKER_POINTS_BAND_30_100=true
# ===== Token 过期时间配置(可选) =====
# 配置 Token 过期时间后,策略会在 Token 过期前提醒你,过期后自动进入安全模式
# 格式1:时间戳(毫秒),例如:1735689600000
# 格式2:时间戳(秒),例如:1735689600(会自动转换为毫秒)
# 格式3:ISO 日期字符串,例如:2025-01-01T00:00:00Z
# 如果不配置此项,策略不会检查 Token 过期时间
# STANDX_TOKEN_EXPIRY=1735689600000
# ===== Telegram 通知配置(可选) =====
# 配置后,策略会通过 Telegram 发送重要通知(订单成交、开仓、平仓、止损、Token过期等)
# 如何获取 Bot Token:在 Telegram 搜索 @BotFather,发送 /newbot 创建机器人,获取 Token
# 如何获取 Chat ID:在 Telegram 搜索 @userinfobot,发送任意消息即可看到你的 Chat ID
# TELEGRAM_BOT_TOKEN=你的BotToken
# TELEGRAM_CHAT_ID=你的ChatID
# TELEGRAM_ACCOUNT_LABEL=我的账户(可选,用于区分多个账户的通知)
``` ```
### 配置说明(新手可直接照抄) ### 正确填写示例
- `STANDX_TOKEN`: 登录后导出的 token(必须)
- `STANDX_SYMBOL`: 交易对(默认 `BTC-USD` 假设你导出的信息是:
- `MAKER_POINTS_ORDER_AMOUNT`: 每一笔挂单数量 - Token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ`
- `MAKER_POINTS_CLOSE_THRESHOLD`: 持仓达到该数值进入平仓模式(0 表示不自动平仓) - 代理钱包私钥: `abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890`
- `MAKER_POINTS_STOP_LOSS_USD`: 亏损超过该值时市价平仓(0 表示关闭)
- `MAKER_POINTS_MIN_REPRICE_BPS`: 盘口变动达到多少 bps 才会重算并撤单(默认 3) 那么你的 `.env` 应该这样写:
- `MAKER_POINTS_BAND_0_10` / `10_30` / `30_100`: 三个档位开关
```bash
EXCHANGE=standx
STANDX_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
STANDX_REQUEST_PRIVATE_KEY=abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
STANDX_SYMBOL=BTC-USD
MAKER_POINTS_ORDER_AMOUNT=0.01
MAKER_POINTS_CLOSE_THRESHOLD=0.1
MAKER_POINTS_STOP_LOSS_USD=0
MAKER_POINTS_MIN_REPRICE_BPS=3
MAKER_POINTS_BAND_0_10=true
MAKER_POINTS_BAND_10_30=true
MAKER_POINTS_BAND_30_100=true
# STANDX_TOKEN_EXPIRY=1735689600000
# TELEGRAM_BOT_TOKEN=你的BotToken
# TELEGRAM_CHAT_ID=你的ChatID
```
> 🔴 **不要加引号!不要加空格!直接粘贴值!**
>
> 🔴 **不要加引号!不要加空格!直接粘贴值!**
>
> 🔴 **不要加引号!不要加空格!直接粘贴值!**
--- ---
## 3. 启动策略 ## 第五步:启动策略
### 3.1 普通启动 ### 普通启动(看实时仪表盘)
```bash ```bash
bun run index.ts --strategy maker-points --exchange standx bun run index.ts --strategy maker-points --exchange standx
``` ```
### 3.2 使用 PM2 后台运行(推荐) ### 后台运行(推荐长期挂机
```bash ```bash
bun run pm2:start:maker-points bun run pm2:start:maker-points
``` ```
PM2 会自动重启策略,适合长期运行。 ---
## 配置参数说明
| 参数 | 含义 | 新手建议 |
|------|------|----------|
| `STANDX_TOKEN` | 登录令牌 | 必填,从第三步获取 |
| `STANDX_REQUEST_PRIVATE_KEY` | 代理钱包私钥 | 必填,从第三步获取 |
| `STANDX_SYMBOL` | 交易品种 | 默认 `BTC-USD` |
| `MAKER_POINTS_ORDER_AMOUNT` | 每笔挂单数量 | 建议 `0.01` 起步 |
| `MAKER_POINTS_CLOSE_THRESHOLD` | 持仓达到多少开始平仓 | 设为 `0` 表示不自动平仓 |
| `MAKER_POINTS_STOP_LOSS_USD` | 亏损多少美元强制平仓 | 设为 `0` 表示关闭止损 |
| `MAKER_POINTS_BAND_*` | 三个挂单档位的开关 | 全部 `true` 即可 |
| `STANDX_TOKEN_EXPIRY` | Token 过期时间 | 可选,格式见下方说明 |
| `TELEGRAM_BOT_TOKEN` | Telegram 机器人 Token | 可选,用于接收通知 |
| `TELEGRAM_CHAT_ID` | Telegram 聊天 ID | 可选,配合 Bot Token 使用 |
| `TELEGRAM_ACCOUNT_LABEL` | Telegram 通知账户标签 | 可选,用于区分多个账户 |
### Token 过期时间配置详解
`STANDX_TOKEN_EXPIRY` 用于设置 Token 的过期时间。配置后,策略会:
1. **Token 过期前 1 小时**:在日志中提醒你 Token 即将过期
2. **Token 过期后**
- 如果有持仓:进入**平仓模式**,只允许平仓和止损,不再开新仓
- 如果无持仓但有挂单:**自动取消所有挂单**
- 如果无持仓无挂单:进入**静默模式**,只接收数据,不下单
**支持的格式:**
- **时间戳(毫秒)**`1735689600000`
- **时间戳(秒)**`1735689600`(会自动转换为毫秒)
- **ISO 日期字符串**`2025-01-01T00:00:00Z``2025-01-01 00:00:00`
**如何获取 Token 过期时间?**
登录 standx.ritmex.one 时,系统会返回 Token 的有效期。你可以在导出登录信息时查看,或者根据登录时设置的过期时间计算。
**示例:**
```bash
# 方式1:使用时间戳(毫秒)
STANDX_TOKEN_EXPIRY=1735689600000
# 方式2:使用时间戳(秒)
STANDX_TOKEN_EXPIRY=1735689600
# 方式3:使用 ISO 日期字符串
STANDX_TOKEN_EXPIRY=2025-01-01T00:00:00Z
```
> 💡 **提示**:如果不配置此项,策略不会检查 Token 过期时间,但建议配置以便及时收到提醒。
### Telegram 通知配置详解
配置 Telegram 通知后,策略会在以下情况发送通知:
- 📝 **订单成交**:挂单被成交时
- 📈 **开仓**:持仓从 0 变为非 0 时
- 📉 **平仓**:持仓从非 0 变为 0 时
- 🛑 **止损触发**:触发止损平仓时
-**Token 过期**Token 过期时
**配置步骤:**
1. **创建 Telegram 机器人**
- 在 Telegram 搜索 `@BotFather`
- 发送 `/newbot` 命令
- 按提示设置机器人名称和用户名
- 获取 Bot Token(格式类似:`123456789:ABCdefGHIjklMNOpqrsTUVwxyz`
2. **获取你的 Chat ID**
- 在 Telegram 搜索 `@userinfobot`
- 发送任意消息
- 机器人会返回你的 Chat ID(一串数字,例如:`123456789`
3. **配置环境变量**
```bash
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
TELEGRAM_CHAT_ID=123456789
TELEGRAM_ACCOUNT_LABEL=我的账户(可选)
```
4. **测试通知**
- 启动策略后,如果配置正确,会在 Token 过期或重要事件时收到通知
- 如果收不到通知,检查 Bot Token 和 Chat ID 是否正确
> 💡 **提示**`TELEGRAM_ACCOUNT_LABEL` 是可选的,如果你有多个账户在运行策略,可以用这个标签区分不同账户的通知。
--- ---
## 4. 运行后你会看到什么 ## 常见问题
启动后会显示仪表盘,包含: ### Q:报错说 Token 无效怎么办?
- 当前交易对与盘口
- 当前仓位与浮动盈亏 重新去 https://standx.ritmex.one/ 登录,重新导出 Token。Token 可能过期了。
- 目标挂单列表
- 已挂单列表 ### Q:代理钱包私钥从哪来的?
- Binance 深度失衡状态
登录 standx.ritmex.one 后点击"导出登录信息"就能看到。
**你不需要自己创建钱包,系统会自动生成!**
**你不需要自己创建钱包,系统会自动生成!**
**你不需要自己创建钱包,系统会自动生成!**
### Q.env 文件放在哪?
放在项目根目录,就是 `ritmex-bot` 文件夹里,和 `package.json` 同一个目录。
### Q:为什么策略没有下单?
1. 检查账户里有没有足够的保证金
2. 检查 TOKEN 和私钥是否正确填写
3. 检查 .env 文件是否保存成功
### Q:担心平掉我手动开的仓位?
把 `MAKER_POINTS_CLOSE_THRESHOLD` 设为 `0` 或者设置成一个比你持仓大的数字。
### Q:如何知道 Token 什么时候过期?
配置 `STANDX_TOKEN_EXPIRY` 环境变量,策略会在 Token 过期前 1 小时提醒你。Token 过期后,如果有持仓会进入平仓模式,只允许平仓和止损。
### QTelegram 通知收不到怎么办?
1. 检查 `TELEGRAM_BOT_TOKEN` 和 `TELEGRAM_CHAT_ID` 是否正确填写
2. 确保没有在 Bot Token 和 Chat ID 前后加引号或空格
3. 在 Telegram 中先给机器人发送一条消息(任意内容),然后再启动策略
4. 检查网络连接是否正常
--- ---
## 5. 新手常见问题 ## 安全提示
### Q1:为什么会撤单重挂? 1. **绝对不要把 TOKEN 和私钥分享给任何人!**
策略只有在盘口变化超过 `MAKER_POINTS_MIN_REPRICE_BPS` 或 Binance 深度状态变化时才会重算挂单。 2. **绝对不要把 TOKEN 和私钥分享给任何人!**
3. **绝对不要把 TOKEN 和私钥分享给任何人!**
### Q2:担心平掉手动持仓怎么办? 代理钱包只用于签名,你的资产始终在你自己的主钱包里。但如果泄露了 TOKEN,别人可以用你的账户交易。
`MAKER_POINTS_CLOSE_THRESHOLD` 设为 0 或者设成大于你的持仓即可。
### Q3:我只想挂某个档位?
把不需要的档位开关设为 `false` 即可。
--- ---
## 6. 注意事项 ## 还是不会?
1. 请确保钱包里有足够的保证金/资金。 把你的报错信息截图发到 Telegram 群里,会有人帮你:
2. 不要泄露 `STANDX_TOKEN`
3. 初次使用请先用小仓位测试。
--- Telegram 群:https://t.me/+4fdo0quY87o4Mjhh
如果你需要我帮你定制参数或排查问题,直接把日志贴给我即可。
+6 -2
View File
@@ -1,4 +1,4 @@
export type StrategyId = "trend" | "guardian" | "maker" | "maker-points" | "offset-maker" | "basis" | "grid"; export type StrategyId = "trend" | "guardian" | "maker" | "maker-points" | "offset-maker" | "liquidity-maker" | "basis" | "grid";
export interface CliOptions { export interface CliOptions {
strategy?: StrategyId; strategy?: StrategyId;
@@ -13,6 +13,7 @@ const STRATEGY_VALUES = new Set<StrategyId>([
"maker", "maker",
"maker-points", "maker-points",
"offset-maker", "offset-maker",
"liquidity-maker",
"basis", "basis",
"grid", "grid",
]); ]);
@@ -72,6 +73,8 @@ function assignStrategy(options: CliOptions, raw: string): void {
options.strategy = "offset-maker"; options.strategy = "offset-maker";
} else if (normalized === "makerpoints" || normalized === "maker-points" || normalized === "maker_points") { } else if (normalized === "makerpoints" || normalized === "maker-points" || normalized === "maker_points") {
options.strategy = "maker-points"; options.strategy = "maker-points";
} else if (normalized === "liquidity" || normalized === "liquiditymaker" || normalized === "liquidity-maker" || normalized === "liquidity_maker") {
options.strategy = "liquidity-maker";
} }
} }
@@ -95,10 +98,11 @@ function assignExchange(options: CliOptions, raw: string): void {
export function printCliHelp(): void { export function printCliHelp(): void {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`Usage: bun run index.ts [--strategy <trend|guardian|maker|maker-points|offset-maker|basis|grid>] [--exchange <aster|grvt|lighter|backpack|paradex|nado|standx>] [--silent]\n\n` + console.log(`Usage: bun run index.ts [--strategy <trend|guardian|maker|maker-points|offset-maker|liquidity-maker|basis|grid>] [--exchange <aster|grvt|lighter|backpack|paradex|nado|standx>] [--silent]\n\n` +
`Options:\n` + `Options:\n` +
` --strategy, -s Automatically start the specified strategy without the interactive menu.\n` + ` --strategy, -s Automatically start the specified strategy without the interactive menu.\n` +
` Aliases: offset, offset-maker for the offset maker engine.\n` + ` Aliases: offset, offset-maker for the offset maker engine.\n` +
` Aliases: liquidity, liquidity-maker for the liquidity maker engine.\n` +
` --exchange, -e Choose exchange. Overrides EXCHANGE/TRADE_EXCHANGE environment variables.\n` + ` --exchange, -e Choose exchange. Overrides EXCHANGE/TRADE_EXCHANGE environment variables.\n` +
` --silent, -q Reduce console output. When used with --strategy, runs in silent daemon mode.\n` + ` --silent, -q Reduce console output. When used with --strategy, runs in silent daemon mode.\n` +
` --help, -h Show this help message.\n`); ` --help, -h Show this help message.\n`);
+17 -1
View File
@@ -1,9 +1,10 @@
import { basisConfig, gridConfig, isBasisStrategyEnabled, makerConfig, makerPointsConfig, tradingConfig } from "../config"; import { basisConfig, gridConfig, isBasisStrategyEnabled, liquidityMakerConfig, makerConfig, makerPointsConfig, tradingConfig } from "../config";
import { getExchangeDisplayName, resolveExchangeId } from "../exchanges/create-adapter"; import { getExchangeDisplayName, resolveExchangeId } from "../exchanges/create-adapter";
import type { ExchangeAdapter } from "../exchanges/adapter"; import type { ExchangeAdapter } from "../exchanges/adapter";
import { buildAdapterFromEnv } from "../exchanges/resolve-from-env"; import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
import { MakerEngine, type MakerEngineSnapshot } from "../strategy/maker-engine"; import { MakerEngine, type MakerEngineSnapshot } from "../strategy/maker-engine";
import { OffsetMakerEngine, type OffsetMakerEngineSnapshot } from "../strategy/offset-maker-engine"; import { OffsetMakerEngine, type OffsetMakerEngineSnapshot } from "../strategy/offset-maker-engine";
import { LiquidityMakerEngine, type LiquidityMakerEngineSnapshot } from "../strategy/liquidity-maker-engine";
import { MakerPointsEngine, type MakerPointsSnapshot } from "../strategy/maker-points-engine"; import { MakerPointsEngine, type MakerPointsSnapshot } from "../strategy/maker-points-engine";
import { TrendEngine, type TrendEngineSnapshot } from "../strategy/trend-engine"; import { TrendEngine, type TrendEngineSnapshot } from "../strategy/trend-engine";
import { GuardianEngine, type GuardianEngineSnapshot } from "../strategy/guardian-engine"; import { GuardianEngine, type GuardianEngineSnapshot } from "../strategy/guardian-engine";
@@ -24,6 +25,7 @@ export const STRATEGY_LABELS: Record<StrategyId, string> = {
maker: "Maker", maker: "Maker",
"maker-points": "Maker Points", "maker-points": "Maker Points",
"offset-maker": "Offset Maker", "offset-maker": "Offset Maker",
"liquidity-maker": "Liquidity Maker",
basis: "Basis Arbitrage", basis: "Basis Arbitrage",
grid: "Grid", grid: "Grid",
}; };
@@ -106,6 +108,19 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
offUpdate: (emitter) => engine.off("update", emitter), offUpdate: (emitter) => engine.off("update", emitter),
}); });
}, },
"liquidity-maker": async (opts) => {
const config = liquidityMakerConfig;
const adapter = createAdapterOrThrow(config.symbol);
const engine = new LiquidityMakerEngine(config, adapter);
await runEngine({
engine,
strategy: "liquidity-maker",
silent: opts.silent,
getSnapshot: () => engine.getSnapshot(),
onUpdate: (emitter) => engine.on("update", emitter),
offUpdate: (emitter) => engine.off("update", emitter),
});
},
basis: async (opts) => { basis: async (opts) => {
if (!isBasisStrategyEnabled()) { if (!isBasisStrategyEnabled()) {
throw new Error("Basis arbitrage strategy is disabled. Set ENABLE_BASIS_STRATEGY=true to enable it."); throw new Error("Basis arbitrage strategy is disabled. Set ENABLE_BASIS_STRATEGY=true to enable it.");
@@ -156,6 +171,7 @@ async function runEngine<
| MakerEngineSnapshot | MakerEngineSnapshot
| MakerPointsSnapshot | MakerPointsSnapshot
| OffsetMakerEngineSnapshot | OffsetMakerEngineSnapshot
| LiquidityMakerEngineSnapshot
| BasisArbSnapshot | BasisArbSnapshot
| GridEngineSnapshot | GridEngineSnapshot
>( >(
+72
View File
@@ -6,6 +6,45 @@
import { resolveExchangeId, type SupportedExchangeId } from "./exchanges/create-adapter"; import { resolveExchangeId, type SupportedExchangeId } from "./exchanges/create-adapter";
import { language, type Language } from "./i18n"; import { language, type Language } from "./i18n";
export interface StandxTokenConfig {
expiryTimestamp: number | null;
}
function parseTimestamp(value: string | undefined): number | null {
if (!value || !value.trim()) return null;
const trimmed = value.trim();
const asNumber = Number(trimmed);
if (Number.isFinite(asNumber) && asNumber > 0) {
return asNumber < 1e12 ? asNumber * 1000 : asNumber;
}
const asDate = Date.parse(trimmed);
if (Number.isFinite(asDate) && asDate > 0) {
return asDate;
}
return null;
}
export const standxTokenConfig: StandxTokenConfig = {
expiryTimestamp: parseTimestamp(process.env.STANDX_TOKEN_EXPIRY),
};
export function isStandxTokenExpired(): boolean {
const expiry = standxTokenConfig.expiryTimestamp;
if (expiry == null) return false;
return Date.now() >= expiry;
}
export function getStandxTokenExpiryInfo(): { expired: boolean; expiryTimestamp: number | null; remainingMs: number | null } {
const expiry = standxTokenConfig.expiryTimestamp;
if (expiry == null) {
return { expired: false, expiryTimestamp: null, remainingMs: null };
}
const now = Date.now();
const expired = now >= expiry;
const remainingMs = expired ? 0 : expiry - now;
return { expired, expiryTimestamp: expiry, remainingMs };
}
export interface TradingConfig { export interface TradingConfig {
symbol: string; symbol: string;
tradeAmount: number; tradeAmount: number;
@@ -255,6 +294,39 @@ export const gridConfig: GridConfig = {
gridConfig.maxPositionSize = resolveGridMaxPosition(gridConfig.orderSize, gridConfig.gridLevels); gridConfig.maxPositionSize = resolveGridMaxPosition(gridConfig.orderSize, gridConfig.gridLevels);
export interface LiquidityMakerConfig {
symbol: string;
tradeAmount: number;
lossLimit: number;
bidOffset: number;
askOffset: number;
refreshIntervalMs: number;
maxLogEntries: number;
maxCloseSlippagePct: number;
priceTick: number;
/** 平仓挂单距成交价的档位数,默认1档 */
closeTickOffset: number;
/** 偏移判断阈值倍数,当一侧深度超出另一侧此倍数时取消薄端订单,默认2 */
depthImbalanceRatio: number;
}
export const liquidityMakerConfig: LiquidityMakerConfig = {
symbol: resolveSymbolFromEnv(),
tradeAmount: parseNumber(process.env.TRADE_AMOUNT, 0.001),
lossLimit: parseNumber(process.env.LIQUIDITY_MAKER_LOSS_LIMIT, parseNumber(process.env.MAKER_LOSS_LIMIT, parseNumber(process.env.LOSS_LIMIT, 0.03))),
bidOffset: parseNumber(process.env.LIQUIDITY_MAKER_BID_OFFSET, parseNumber(process.env.MAKER_BID_OFFSET, 0)),
askOffset: parseNumber(process.env.LIQUIDITY_MAKER_ASK_OFFSET, parseNumber(process.env.MAKER_ASK_OFFSET, 0)),
refreshIntervalMs: parseNumber(process.env.LIQUIDITY_MAKER_REFRESH_INTERVAL_MS, parseNumber(process.env.MAKER_REFRESH_INTERVAL_MS, 500)),
maxLogEntries: parseNumber(process.env.LIQUIDITY_MAKER_MAX_LOG_ENTRIES, parseNumber(process.env.MAKER_MAX_LOG_ENTRIES, 200)),
maxCloseSlippagePct: parseNumber(
process.env.LIQUIDITY_MAKER_MAX_CLOSE_SLIPPAGE_PCT ?? process.env.MAKER_MAX_CLOSE_SLIPPAGE_PCT ?? process.env.MAX_CLOSE_SLIPPAGE_PCT,
0.05
),
priceTick: parseNumber(process.env.LIQUIDITY_MAKER_PRICE_TICK ?? process.env.MAKER_PRICE_TICK ?? process.env.PRICE_TICK, 0.1),
closeTickOffset: Math.max(1, Math.floor(parseNumber(process.env.LIQUIDITY_MAKER_CLOSE_TICK_OFFSET, 1))),
depthImbalanceRatio: Math.max(1.1, parseNumber(process.env.LIQUIDITY_MAKER_DEPTH_IMBALANCE_RATIO, 2)),
};
export function isBasisStrategyEnabled(): boolean { export function isBasisStrategyEnabled(): boolean {
const raw = process.env.ENABLE_BASIS_STRATEGY; const raw = process.env.ENABLE_BASIS_STRATEGY;
if (!raw) return false; if (!raw) return false;
+9
View File
@@ -55,6 +55,15 @@ const translations: Record<string, TranslationEntry> = {
zh: "监控期货与现货盘口差价,辅助发现套利机会", zh: "监控期货与现货盘口差价,辅助发现套利机会",
en: "Monitors futures/spot spread to surface arbitrage windows.", en: "Monitors futures/spot spread to surface arbitrage windows.",
}, },
"app.strategy.liquidityMaker.label": { zh: "流动性做市商", en: "Liquidity Maker" },
"app.strategy.liquidityMaker.desc": {
zh: "成交后在更优价位挂单平仓,更敏感的深度偏移判断",
en: "Places close orders at better prices after fills, with sensitive depth imbalance detection.",
},
"liquidityMaker.title": { zh: "流动性做市商 (Liquidity Maker)", en: "Liquidity Maker" },
"liquidityMaker.initializing": { zh: "流动性做市商初始化中...", en: "Initializing Liquidity Maker..." },
"liquidityMaker.lastFill": { zh: "最近成交: {info}", en: "Last fill: {info}" },
"liquidityMaker.noFill": { zh: "无", en: "None" },
"app.integrity.warning": { "app.integrity.warning": {
zh: "警告: 版权校验失败,当前版本可能被篡改。", zh: "警告: 版权校验失败,当前版本可能被篡改。",
en: "Warning: Copyright integrity check failed; build may be tampered.", en: "Warning: Copyright integrity check failed; build may be tampered.",
+12
View File
@@ -0,0 +1,12 @@
export type {
NotificationLevel,
TradeNotification,
NotificationSender,
NotificationConfig,
} from "./types";
export {
TelegramNotifier,
createTelegramNotifier,
type TelegramConfig,
} from "./telegram";
+117
View File
@@ -0,0 +1,117 @@
import type { NotificationSender, TradeNotification, NotificationConfig } from "./types";
export interface TelegramConfig extends NotificationConfig {
botToken: string;
chatId: string;
}
const LEVEL_EMOJI: Record<string, string> = {
info: "️",
warn: "⚠️",
error: "🚨",
success: "✅",
};
const TYPE_EMOJI: Record<string, string> = {
order_filled: "📝",
position_opened: "📈",
position_closed: "📉",
stop_loss: "🛑",
token_expired: "⏰",
custom: "📢",
};
const LOG_PREFIX = "[Telegram]";
function formatNotificationMessage(notification: TradeNotification, accountLabel?: string): string {
const levelEmoji = LEVEL_EMOJI[notification.level] ?? "";
const typeEmoji = TYPE_EMOJI[notification.type] ?? "";
const timestamp = notification.timestamp ?? Date.now();
const time = new Date(timestamp).toISOString().replace("T", " ").substring(0, 19);
const label = notification.accountLabel ?? accountLabel ?? notification.symbol;
const lines: string[] = [
`${typeEmoji}${levelEmoji} [${label}] ${notification.title}`,
``,
`${notification.message}`,
];
if (notification.details && Object.keys(notification.details).length > 0) {
lines.push(``);
for (const [key, value] of Object.entries(notification.details)) {
if (value != null) {
lines.push(`${key}: ${value}`);
}
}
}
lines.push(``);
lines.push(`🕐 ${time} UTC`);
lines.push(`📊 ${notification.symbol}`);
return lines.join("\n");
}
export class TelegramNotifier implements NotificationSender {
private readonly config: TelegramConfig;
private readonly baseUrl: string;
private sendQueue: Promise<void> = Promise.resolve();
constructor(config: Partial<TelegramConfig> = {}) {
this.config = {
enabled: Boolean(config.botToken && config.chatId),
botToken: config.botToken ?? "",
chatId: config.chatId ?? "",
accountLabel: config.accountLabel,
};
this.baseUrl = `https://api.telegram.org/bot${this.config.botToken}`;
}
isEnabled(): boolean {
return this.config.enabled;
}
async send(notification: TradeNotification): Promise<void> {
if (!this.isEnabled()) {
return;
}
this.sendQueue = this.sendQueue
.then(() => this.doSend(notification))
.catch(() => {});
}
private async doSend(notification: TradeNotification): Promise<void> {
const text = formatNotificationMessage(notification, this.config.accountLabel);
const url = `${this.baseUrl}/sendMessage`;
try {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: this.config.chatId,
text,
}),
});
if (response.ok) {
console.info(`${LOG_PREFIX} Notification sent (status ${response.status}).`);
return;
}
const errorText = await response.text().catch(() => "unknown error");
console.error(`${LOG_PREFIX} Failed to send notification: ${response.status} ${errorText}`);
} catch (error) {
console.error(`${LOG_PREFIX} Failed to send notification: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
export function createTelegramNotifier(): TelegramNotifier {
return new TelegramNotifier({
botToken: process.env.TELEGRAM_BOT_TOKEN,
chatId: process.env.TELEGRAM_CHAT_ID,
accountLabel: process.env.TELEGRAM_ACCOUNT_LABEL,
});
}
+22
View File
@@ -0,0 +1,22 @@
export type NotificationLevel = "info" | "warn" | "error" | "success";
export interface TradeNotification {
type: "order_filled" | "position_opened" | "position_closed" | "stop_loss" | "token_expired" | "custom";
level: NotificationLevel;
symbol: string;
title: string;
message: string;
accountLabel?: string;
details?: Record<string, string | number | boolean | null>;
timestamp?: number;
}
export interface NotificationSender {
send(notification: TradeNotification): Promise<void>;
isEnabled(): boolean;
}
export interface NotificationConfig {
enabled: boolean;
accountLabel?: string;
}
File diff suppressed because it is too large Load Diff
+236 -7
View File
@@ -8,7 +8,7 @@ import type {
} from "../exchanges/types"; } from "../exchanges/types";
import { formatPriceToString } from "../utils/math"; import { formatPriceToString } from "../utils/math";
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log"; import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
import { extractMessage, isInsufficientBalanceError, isRateLimitError, isUnknownOrderError } from "../utils/errors"; import { extractMessage, isInsufficientBalanceError, isPrecisionError, isRateLimitError, isUnknownOrderError } from "../utils/errors";
import { isOrderActiveStatus } from "../utils/order-status"; import { isOrderActiveStatus } from "../utils/order-status";
import { getPosition, parseSymbolParts } from "../utils/strategy"; import { getPosition, parseSymbolParts } from "../utils/strategy";
import type { PositionSnapshot } from "../utils/strategy"; import type { PositionSnapshot } from "../utils/strategy";
@@ -29,6 +29,17 @@ import { SessionVolumeTracker } from "./common/session-volume";
import { BinanceDepthTracker, type BinanceDepthSnapshot } from "./common/binance-depth"; import { BinanceDepthTracker, type BinanceDepthSnapshot } from "./common/binance-depth";
import { buildBpsTargets } from "./maker-points-logic"; import { buildBpsTargets } from "./maker-points-logic";
import { t } from "../i18n"; import { t } from "../i18n";
import {
checkStandxTokenExpiry,
formatTokenExpiryMessage,
isTokenExpiryConfigured,
type TokenExpiryState,
} from "../utils/standx-token-expiry";
import {
createTelegramNotifier,
type NotificationSender,
type TradeNotification,
} from "../notifications";
interface DesiredOrder { interface DesiredOrder {
side: "BUY" | "SELL"; side: "BUY" | "SELL";
@@ -90,6 +101,7 @@ export class MakerPointsEngine {
private readonly sessionVolume = new SessionVolumeTracker(); private readonly sessionVolume = new SessionVolumeTracker();
private readonly rateLimit: RateLimitController; private readonly rateLimit: RateLimitController;
private readonly binanceDepth: BinanceDepthTracker; private readonly binanceDepth: BinanceDepthTracker;
private readonly notifier: NotificationSender;
private priceTick: number = 0.1; private priceTick: number = 0.1;
private qtyStep: number = 0.001; private qtyStep: number = 0.001;
@@ -128,11 +140,21 @@ export class MakerPointsEngine {
private insufficientBalanceNotified = false; private insufficientBalanceNotified = false;
private lastInsufficientMessage: string | null = null; private lastInsufficientMessage: string | null = null;
private tokenExpiryState: TokenExpiryState = "active";
private tokenExpiryLogged = false;
private tokenExpiryCancelDone = false;
private tokenExpiredCloseOnlyMode = false;
private tokenExpiryNotified = false;
private lastPositionAmt = 0;
private lastPositionSide: "LONG" | "SHORT" | "FLAT" = "FLAT";
constructor(private readonly config: MakerPointsConfig, private readonly exchange: ExchangeAdapter) { constructor(private readonly config: MakerPointsConfig, private readonly exchange: ExchangeAdapter) {
this.tradeLog = createTradeLog(this.config.maxLogEntries); this.tradeLog = createTradeLog(this.config.maxLogEntries);
this.rateLimit = new RateLimitController(this.config.refreshIntervalMs, (type, detail) => this.rateLimit = new RateLimitController(this.config.refreshIntervalMs, (type, detail) =>
this.tradeLog.push(type, detail) this.tradeLog.push(type, detail)
); );
this.notifier = createTelegramNotifier();
this.priceTick = Math.max(1e-9, this.config.priceTick); this.priceTick = Math.max(1e-9, this.config.priceTick);
this.qtyStep = Math.max(1e-9, this.config.qtyStep); this.qtyStep = Math.max(1e-9, this.config.qtyStep);
this.binanceDepth = new BinanceDepthTracker(resolveBinanceSymbol(this.config.symbol), { this.binanceDepth = new BinanceDepthTracker(resolveBinanceSymbol(this.config.symbol), {
@@ -201,6 +223,7 @@ export class MakerPointsEngine {
} }
const position = getPosition(snapshot, this.config.symbol); const position = getPosition(snapshot, this.config.symbol);
this.sessionVolume.update(position, this.getReferencePrice()); this.sessionVolume.update(position, this.getReferencePrice());
this.detectPositionChange(position);
this.feedStatus.account = true; this.feedStatus.account = true;
this.emitUpdate(); this.emitUpdate();
}, },
@@ -314,6 +337,14 @@ export class MakerPointsEngine {
return; return;
} }
const position = getPosition(this.accountSnapshot, this.config.symbol);
const absPosition = Math.abs(position.positionAmt);
if (await this.handleTokenExpiry(position, absPosition)) {
this.emitUpdate();
return;
}
const depth = this.depthSnapshot!; const depth = this.depthSnapshot!;
const { topBid, topAsk } = getTopPrices(depth); const { topBid, topAsk } = getTopPrices(depth);
if (topBid == null || topAsk == null) { if (topBid == null || topAsk == null) {
@@ -321,13 +352,12 @@ export class MakerPointsEngine {
return; return;
} }
const position = getPosition(this.accountSnapshot, this.config.symbol);
const absPosition = Math.abs(position.positionAmt);
const closeThreshold = Number(this.config.closeThreshold); const closeThreshold = Number(this.config.closeThreshold);
const closeOnly = const closeOnly =
Number.isFinite(closeThreshold) && this.tokenExpiredCloseOnlyMode ||
(Number.isFinite(closeThreshold) &&
closeThreshold > 0 && closeThreshold > 0 &&
absPosition >= closeThreshold - EPS; absPosition >= closeThreshold - EPS);
const prevCloseOnly = this.lastCloseOnly; const prevCloseOnly = this.lastCloseOnly;
if (closeOnly !== prevCloseOnly) { if (closeOnly !== prevCloseOnly) {
this.tradeLog.push("info", closeOnly ? "进入平仓模式,仅挂 reduce-only" : "退出平仓模式"); this.tradeLog.push("info", closeOnly ? "进入平仓模式,仅挂 reduce-only" : "退出平仓模式");
@@ -587,6 +617,10 @@ export class MakerPointsEngine {
this.registerInsufficientBalance(error); this.registerInsufficientBalance(error);
break; break;
} }
if (isPrecisionError(error)) {
this.tradeLog.push("warn", `检测到精度错误,重新同步: ${extractMessage(error)}`);
this.syncPrecision(true);
}
this.tradeLog.push( this.tradeLog.push(
"error", "error",
`挂单失败 ${target.side} @ ${target.price}: ${extractMessage(error)}` `挂单失败 ${target.side} @ ${target.price}: ${extractMessage(error)}`
@@ -615,6 +649,19 @@ export class MakerPointsEngine {
"stop", "stop",
`触发止损: 未实现亏损 ${position.unrealizedProfit.toFixed(4)} USDT` `触发止损: 未实现亏损 ${position.unrealizedProfit.toFixed(4)} USDT`
); );
this.notify({
type: "stop_loss",
level: "error",
symbol: this.config.symbol,
title: "止损触发",
message: `未实现亏损 ${position.unrealizedProfit.toFixed(4)} USDT,强制平仓`,
details: {
side: position.positionAmt > 0 ? "LONG" : "SHORT",
size: absPosition,
unrealizedPnl: position.unrealizedProfit,
lossLimit: -lossLimit,
},
});
try { try {
await this.flushOrders(); await this.flushOrders();
await marketClose( await marketClose(
@@ -633,6 +680,9 @@ export class MakerPointsEngine {
} catch (error) { } catch (error) {
if (isUnknownOrderError(error)) { if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "止损平仓时订单已不存在"); this.tradeLog.push("order", "止损平仓时订单已不存在");
} else if (isPrecisionError(error)) {
this.tradeLog.push("warn", `止损平仓精度错误,重新同步: ${extractMessage(error)}`);
this.syncPrecision(true);
} else { } else {
this.tradeLog.push("error", `止损平仓失败: ${extractMessage(error)}`); this.tradeLog.push("error", `止损平仓失败: ${extractMessage(error)}`);
} }
@@ -668,12 +718,13 @@ export class MakerPointsEngine {
} }
} }
private syncPrecision(): void { private syncPrecision(force = false): void {
if (this.precisionSync) return; if (this.precisionSync && !force) return;
const getPrecision = this.exchange.getPrecision?.bind(this.exchange); const getPrecision = this.exchange.getPrecision?.bind(this.exchange);
if (!getPrecision) return; if (!getPrecision) return;
this.precisionSync = getPrecision() this.precisionSync = getPrecision()
.then((precision) => { .then((precision) => {
this.precisionSync = null;
if (!precision) return; if (!precision) return;
let updated = false; let updated = false;
if (Number.isFinite(precision.priceTick) && precision.priceTick > 0) { if (Number.isFinite(precision.priceTick) && precision.priceTick > 0) {
@@ -759,6 +810,10 @@ export class MakerPointsEngine {
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot); return getMidOrLast(this.depthSnapshot, this.tickerSnapshot);
} }
private notify(notification: TradeNotification): void {
this.notifier.send(notification);
}
private logReadinessBlockers(): void { private logReadinessBlockers(): void {
if (!this.feedStatus.account && !this.readinessLogged.account) { if (!this.feedStatus.account && !this.readinessLogged.account) {
this.tradeLog.push("info", t("log.maker.waitAccount")); this.tradeLog.push("info", t("log.maker.waitAccount"));
@@ -828,6 +883,180 @@ export class MakerPointsEngine {
} }
return active; return active;
} }
private detectPositionChange(position: PositionSnapshot): void {
const currentAmt = position.positionAmt;
const currentSide: "LONG" | "SHORT" | "FLAT" =
currentAmt > EPS ? "LONG" : currentAmt < -EPS ? "SHORT" : "FLAT";
const prevAmt = this.lastPositionAmt;
const prevSide = this.lastPositionSide;
if (Math.abs(currentAmt - prevAmt) < EPS && currentSide === prevSide) {
return;
}
const absChange = Math.abs(currentAmt - prevAmt);
const reference = this.getReferencePrice() ?? 0;
if (prevSide === "FLAT" && currentSide !== "FLAT") {
this.notify({
type: "position_opened",
level: "info",
symbol: this.config.symbol,
title: "开仓",
message: `${currentSide === "LONG" ? "做多" : "做空"} ${Math.abs(currentAmt).toFixed(6)}`,
details: {
side: currentSide,
size: Math.abs(currentAmt),
price: reference > 0 ? reference : null,
},
});
} else if (currentSide === "FLAT" && prevSide !== "FLAT") {
const pnl = position.unrealizedProfit;
const closeType = this.tokenExpiredCloseOnlyMode ? "Token过期平仓" : "平仓";
this.notify({
type: "position_closed",
level: "success",
symbol: this.config.symbol,
title: closeType,
message: `已平仓 ${Math.abs(prevAmt).toFixed(6)} (${prevSide === "LONG" ? "多" : "空"})`,
details: {
prevSide,
closedSize: Math.abs(prevAmt),
pnl: Number.isFinite(pnl) ? pnl : null,
},
});
} else if (currentSide === prevSide && absChange > EPS) {
const isIncrease = Math.abs(currentAmt) > Math.abs(prevAmt);
if (isIncrease) {
this.notify({
type: "order_filled",
level: "info",
symbol: this.config.symbol,
title: "加仓",
message: `${currentSide === "LONG" ? "做多" : "做空"} +${absChange.toFixed(6)}${Math.abs(currentAmt).toFixed(6)}`,
details: {
side: currentSide,
added: absChange,
totalSize: Math.abs(currentAmt),
},
});
} else {
this.notify({
type: "order_filled",
level: "info",
symbol: this.config.symbol,
title: "减仓",
message: `${currentSide === "LONG" ? "多" : "空"} -${absChange.toFixed(6)}${Math.abs(currentAmt).toFixed(6)}`,
details: {
side: currentSide,
reduced: absChange,
totalSize: Math.abs(currentAmt),
},
});
}
} else if (currentSide !== prevSide && currentSide !== "FLAT" && prevSide !== "FLAT") {
this.notify({
type: "position_opened",
level: "info",
symbol: this.config.symbol,
title: "反向开仓",
message: `${prevSide === "LONG" ? "多→空" : "空→多"} ${Math.abs(currentAmt).toFixed(6)}`,
details: {
prevSide,
newSide: currentSide,
size: Math.abs(currentAmt),
},
});
}
this.lastPositionAmt = currentAmt;
this.lastPositionSide = currentSide;
}
private async handleTokenExpiry(position: PositionSnapshot, absPosition: number): Promise<boolean> {
if (!isTokenExpiryConfigured()) {
return false;
}
const expiryStatus = checkStandxTokenExpiry({
positionAmt: position.positionAmt,
openOrderCount: this.openOrders.length,
});
if (!expiryStatus.expired) {
if (this.tokenExpiryState !== "active") {
this.tokenExpiryState = "active";
this.tokenExpiryLogged = false;
this.tokenExpiryCancelDone = false;
this.tokenExpiredCloseOnlyMode = false;
this.tokenExpiryNotified = false;
}
return false;
}
const prevState = this.tokenExpiryState;
this.tokenExpiryState = expiryStatus.state;
if (!this.tokenExpiryLogged) {
const message = formatTokenExpiryMessage(expiryStatus);
if (message) {
this.tradeLog.push("warn", message);
}
this.tokenExpiryLogged = true;
}
if (!this.tokenExpiryNotified) {
this.notify({
type: "token_expired",
level: "warn",
symbol: this.config.symbol,
title: "Token 已过期",
message: expiryStatus.hasPosition
? "Token 已过期,进入平仓模式,不再开新仓"
: "Token 已过期,策略进入静默模式",
details: {
hasPosition: expiryStatus.hasPosition,
hasOpenOrders: expiryStatus.hasOpenOrders,
state: expiryStatus.state,
},
});
this.tokenExpiryNotified = true;
}
if (!this.tokenExpiryCancelDone && this.openOrders.length > 0) {
try {
await this.exchange.cancelAllOrders({ symbol: this.config.symbol });
this.tradeLog.push("order", "Token 过期,已撤销所有挂单");
this.openOrders = [];
this.tokenExpiryCancelDone = true;
} catch (error) {
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "Token 过期撤单时订单已不存在");
this.tokenExpiryCancelDone = true;
} else {
this.tradeLog.push("error", `Token 过期撤单失败: ${extractMessage(error)}`);
}
}
}
if (expiryStatus.state === "expired_with_position") {
if (!this.tokenExpiredCloseOnlyMode) {
this.tokenExpiredCloseOnlyMode = true;
this.tradeLog.push("info", "Token 过期,强制进入平仓模式,仅允许 reduce-only 订单");
}
return false;
}
if (expiryStatus.state === "silent") {
if (prevState !== "silent") {
this.tradeLog.push("info", "进入静默数据接收模式,不再进行任何交易操作");
}
return true;
}
return true;
}
} }
function resolveBinanceSymbol(symbol: string): string { function resolveBinanceSymbol(symbol: string): string {
+8 -1
View File
@@ -5,6 +5,7 @@ import { GuardianApp } from "./GuardianApp";
import { MakerApp } from "./MakerApp"; import { MakerApp } from "./MakerApp";
import { MakerPointsApp } from "./MakerPointsApp"; import { MakerPointsApp } from "./MakerPointsApp";
import { OffsetMakerApp } from "./OffsetMakerApp"; import { OffsetMakerApp } from "./OffsetMakerApp";
import { LiquidityMakerApp } from "./LiquidityMakerApp";
import { GridApp } from "./GridApp"; import { GridApp } from "./GridApp";
import { BasisApp } from "./BasisApp"; import { BasisApp } from "./BasisApp";
import { isBasisStrategyEnabled } from "../config"; import { isBasisStrategyEnabled } from "../config";
@@ -13,7 +14,7 @@ import { resolveExchangeId } from "../exchanges/create-adapter";
import { t } from "../i18n"; import { t } from "../i18n";
interface StrategyOption { interface StrategyOption {
id: "trend" | "guardian" | "maker" | "maker-points" | "offset-maker" | "basis" | "grid"; id: "trend" | "guardian" | "maker" | "maker-points" | "offset-maker" | "liquidity-maker" | "basis" | "grid";
label: string; label: string;
description: string; description: string;
component: React.ComponentType<{ onExit: () => void }>; component: React.ComponentType<{ onExit: () => void }>;
@@ -50,6 +51,12 @@ const BASE_STRATEGIES: StrategyOption[] = [
description: t("app.strategy.offset.desc"), description: t("app.strategy.offset.desc"),
component: OffsetMakerApp, component: OffsetMakerApp,
}, },
{
id: "liquidity-maker",
label: t("app.strategy.liquidityMaker.label"),
description: t("app.strategy.liquidityMaker.desc"),
component: LiquidityMakerApp,
},
]; ];
const inputSupported = Boolean(process.stdin && (process.stdin as any).isTTY); const inputSupported = Boolean(process.stdin && (process.stdin as any).isTTY);
+220
View File
@@ -0,0 +1,220 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Box, Text, useInput } from "ink";
import { liquidityMakerConfig } from "../config";
import { getExchangeDisplayName, resolveExchangeId } from "../exchanges/create-adapter";
import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
import { LiquidityMakerEngine, type LiquidityMakerEngineSnapshot } from "../strategy/liquidity-maker-engine";
import { DataTable, type TableColumn } from "./components/DataTable";
import { formatNumber } from "../utils/format";
import { t } from "../i18n";
interface LiquidityMakerAppProps {
onExit: () => void;
}
const inputSupported = Boolean(process.stdin && (process.stdin as any).isTTY);
export function LiquidityMakerApp({ onExit }: LiquidityMakerAppProps) {
const [snapshot, setSnapshot] = useState<LiquidityMakerEngineSnapshot | null>(null);
const [error, setError] = useState<Error | null>(null);
const engineRef = useRef<LiquidityMakerEngine | null>(null);
const exchangeId = useMemo(() => resolveExchangeId(), []);
const exchangeName = useMemo(() => getExchangeDisplayName(exchangeId), [exchangeId]);
useInput(
(input, key) => {
if (key.escape) {
engineRef.current?.stop();
onExit();
}
},
{ isActive: inputSupported }
);
useEffect(() => {
try {
const adapter = buildAdapterFromEnv({ exchangeId, symbol: liquidityMakerConfig.symbol });
const engine = new LiquidityMakerEngine(liquidityMakerConfig, adapter);
engineRef.current = engine;
setSnapshot(engine.getSnapshot());
const handler = (next: LiquidityMakerEngineSnapshot) => {
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)));
}
}, [exchangeId]);
if (error) {
return (
<Box flexDirection="column" padding={1}>
<Text color="red">{t("common.startFailed", { message: error.message })}</Text>
<Text color="gray">{t("common.checkEnv")}</Text>
</Box>
);
}
if (!snapshot) {
return (
<Box padding={1}>
<Text>{t("liquidityMaker.initializing")}</Text>
</Box>
);
}
const topBid = snapshot.topBid;
const topAsk = snapshot.topAsk;
const priceDigits = snapshot.priceDecimals ?? 2;
const spreadDigits = Math.max(priceDigits + 1, 4);
const spreadDisplay =
snapshot.spread != null ? `${formatNumber(snapshot.spread, spreadDigits)} USDT` : "-";
const hasPosition = Math.abs(snapshot.position.positionAmt) > 1e-5;
const sortedOrders = [...snapshot.openOrders].sort((a, b) =>
(Number(b.updateTime ?? 0) - Number(a.updateTime ?? 0)) || Number(b.orderId) - Number(a.orderId)
);
const openOrderRows = sortedOrders.slice(0, 8).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(-5);
const imbalanceLabel =
snapshot.depthImbalance === "balanced"
? t("offset.imbalance.balanced")
: snapshot.depthImbalance === "buy_dominant"
? t("offset.imbalance.buy")
: t("offset.imbalance.sell");
const readyStatus = snapshot.ready ? t("status.live") : t("status.waitingData");
// 显示最近成交信息
const lastFillInfo = snapshot.lastFill
? `${snapshot.lastFill.side} ${formatNumber(snapshot.lastFill.amount, 6)} @ ${formatNumber(snapshot.lastFill.price, priceDigits)}`
: t("liquidityMaker.noFill");
return (
<Box flexDirection="column" paddingX={1}>
<Box flexDirection="column" marginBottom={1}>
<Text color="cyanBright">{t("liquidityMaker.title")}</Text>
<Text>
{t("offset.headerLine", {
exchange: exchangeName,
symbol: snapshot.symbol,
bid: formatNumber(topBid, priceDigits),
ask: formatNumber(topAsk, priceDigits),
spread: spreadDisplay,
})}
</Text>
<Text>
{t("offset.depthLine", {
buy: formatNumber(snapshot.buyDepthSum10, 4),
sell: formatNumber(snapshot.sellDepthSum10, 4),
status: imbalanceLabel,
})}
</Text>
<Text color="gray">
{t("offset.strategyStatus", {
buyStatus: snapshot.skipBuySide ? t("common.disabled") : t("common.enabled"),
sellStatus: snapshot.skipSellSide ? t("common.disabled") : t("common.enabled"),
})}
</Text>
<Text color="gray">{t("liquidityMaker.lastFill", { info: lastFillInfo })}</Text>
<Text color="gray">{t("trend.statusLine", { status: readyStatus })}</Text>
</Box>
<Box flexDirection="row" marginBottom={1}>
<Box flexDirection="column" marginRight={4}>
<Text color="greenBright">{t("common.section.position")}</Text>
{hasPosition ? (
<>
<Text>
{t("maker.positionLine", {
direction:
snapshot.position.positionAmt > 0 ? t("common.direction.long") : t("common.direction.short"),
qty: formatNumber(Math.abs(snapshot.position.positionAmt), 4),
entry: formatNumber(snapshot.position.entryPrice, priceDigits),
})}
</Text>
<Text>
{t("maker.pnlLine", {
pnl: formatNumber(snapshot.pnl, 4),
accountPnl: formatNumber(snapshot.accountUnrealized, 4),
})}
</Text>
</>
) : (
<Text color="gray">{t("common.noPosition")}</Text>
)}
</Box>
<Box flexDirection="column">
<Text color="greenBright">{t("maker.targetOrders")}</Text>
{desiredRows.length > 0 ? (
<DataTable columns={desiredColumns} rows={desiredRows} />
) : (
<Text color="gray">{t("maker.noTargetOrders")}</Text>
)}
<Text>
{t("trend.volumeLine", { volume: formatNumber(snapshot.sessionVolume, 2) })}
</Text>
</Box>
</Box>
<Box flexDirection="column" marginBottom={1}>
<Text color="yellow">{t("common.section.orders")}</Text>
{openOrderRows.length > 0 ? (
<DataTable columns={openOrderColumns} rows={openOrderRows} />
) : (
<Text color="gray">{t("common.noOrders")}</Text>
)}
</Box>
<Box flexDirection="column">
<Text color="yellow">{t("common.section.recent")}</Text>
{lastLogs.length > 0 ? (
lastLogs.map((item, index) => (
<Text key={`${item.time}-${index}`}>
[{item.time}] [{item.type}] {item.detail}
</Text>
))
) : (
<Text color="gray">{t("common.noLogs")}</Text>
)}
</Box>
</Box>
);
}
+24
View File
@@ -61,3 +61,27 @@ export function isInsufficientBalanceError(error: unknown): boolean {
message.includes("NOT ENOUGH") message.includes("NOT ENOUGH")
); );
} }
export function isPrecisionError(error: unknown): boolean {
const message = extractMessage(error).toUpperCase();
return (
message.includes("PRECISION") ||
message.includes("TICK_SIZE") ||
message.includes("TICKSIZE") ||
message.includes("STEP_SIZE") ||
message.includes("STEPSIZE") ||
message.includes("LOT_SIZE") ||
message.includes("LOTSIZE") ||
message.includes("INVALID_QUANTITY") ||
message.includes("INVALID QUANTITY") ||
message.includes("QUANTITY_INVALID") ||
message.includes("INVALID_PRICE") ||
message.includes("INVALID PRICE") ||
message.includes("PRICE_INVALID") ||
message.includes("QTY_STEP") ||
message.includes("PRICE_TICK") ||
message.includes("DECIMAL") ||
message.includes("FILTER_FAILURE") ||
message.includes("NOTIONAL")
);
}
+92
View File
@@ -0,0 +1,92 @@
import { isStandxTokenExpired, getStandxTokenExpiryInfo, standxTokenConfig } from "../config";
export type TokenExpiryState = "active" | "expired" | "expired_with_position" | "silent";
export interface TokenExpiryStatus {
state: TokenExpiryState;
expired: boolean;
expiryTimestamp: number | null;
remainingMs: number | null;
hasPosition: boolean;
hasOpenOrders: boolean;
}
export interface TokenExpiryCheckParams {
positionAmt: number;
openOrderCount: number;
}
export function checkStandxTokenExpiry(params: TokenExpiryCheckParams): TokenExpiryStatus {
const info = getStandxTokenExpiryInfo();
const hasPosition = Math.abs(params.positionAmt) > 1e-8;
const hasOpenOrders = params.openOrderCount > 0;
if (!info.expired) {
return {
state: "active",
expired: false,
expiryTimestamp: info.expiryTimestamp,
remainingMs: info.remainingMs,
hasPosition,
hasOpenOrders,
};
}
if (hasPosition) {
return {
state: "expired_with_position",
expired: true,
expiryTimestamp: info.expiryTimestamp,
remainingMs: 0,
hasPosition: true,
hasOpenOrders,
};
}
if (!hasOpenOrders) {
return {
state: "silent",
expired: true,
expiryTimestamp: info.expiryTimestamp,
remainingMs: 0,
hasPosition: false,
hasOpenOrders: false,
};
}
return {
state: "expired",
expired: true,
expiryTimestamp: info.expiryTimestamp,
remainingMs: 0,
hasPosition,
hasOpenOrders,
};
}
export function formatTokenExpiryMessage(status: TokenExpiryStatus): string | null {
if (!status.expired) {
if (status.remainingMs != null && status.remainingMs < 3600_000) {
const mins = Math.ceil(status.remainingMs / 60_000);
return `StandX Token 将在 ${mins} 分钟后过期`;
}
return null;
}
switch (status.state) {
case "expired":
return "StandX Token 已过期,正在取消所有挂单";
case "expired_with_position":
return "StandX Token 已过期,仅保留平仓/止损逻辑";
case "silent":
return "StandX Token 已过期,进入静默数据接收模式";
default:
return null;
}
}
export function isTokenExpiryConfigured(): boolean {
return standxTokenConfig.expiryTimestamp != null;
}
export { isStandxTokenExpired, getStandxTokenExpiryInfo };