mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-19 02:14:35 +08:00
Compare commits
22 Commits
ai-grid
...
5ea9a3990e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ea9a3990e | ||
|
|
5b5199359c | ||
|
|
9dbc861cdf | ||
|
|
fcc0267a46 | ||
|
|
c9150e8273 | ||
|
|
fcaabea6cb | ||
|
|
b5716ff3cb | ||
|
|
2f54d1d4c0 | ||
|
|
0b448558ca | ||
|
|
84276f64ae | ||
|
|
5560df133e | ||
|
|
f43c63699b | ||
|
|
7b1edaa51f | ||
|
|
ed8ad63288 | ||
|
|
a7370efc2f | ||
|
|
5b384d126f | ||
|
|
1532b55d77 | ||
|
|
0e75b80d95 | ||
|
|
9c57134dfb | ||
|
|
7ce7361cef | ||
|
|
7a1643c56c | ||
|
|
7e96c5d0f2 |
46
README.md
46
README.md
@@ -1,9 +1,21 @@
|
|||||||
# NOFX - Agentic Trading OS
|
<h1 align="center">NOFX — Open Source AI Trading OS</h1>
|
||||||
|
|
||||||
[](https://golang.org/)
|
<p align="center">
|
||||||
[](https://reactjs.org/)
|
<strong>The infrastructure layer for AI-powered financial trading.</strong>
|
||||||
[](https://www.typescriptlang.org/)
|
</p>
|
||||||
[](LICENSE)
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||||
|
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||||
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
| CONTRIBUTOR AIRDROP PROGRAM |
|
| CONTRIBUTOR AIRDROP PROGRAM |
|
||||||
|:----------------------------------:|
|
|:----------------------------------:|
|
||||||
@@ -14,10 +26,6 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## AI-Powered Multi-Asset Trading Platform
|
|
||||||
|
|
||||||
**NOFX** is an open-source AI trading system that lets you run multiple AI models to trade automatically. Configure strategies through a web interface, monitor performance in real-time, and let AI agents compete to find the best trading approach.
|
|
||||||
|
|
||||||
### Supported Markets
|
### Supported Markets
|
||||||
|
|
||||||
| Market | Trading | Status |
|
| Market | Trading | Status |
|
||||||
@@ -488,6 +496,26 @@ All contributions are tracked on GitHub. When NOFX generates revenue, contributo
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
Thanks to all our sponsors!
|
||||||
|
|
||||||
|
<a href="https://github.com/pjl914335852-ux"><img src="https://github.com/pjl914335852-ux.png" width="60" height="60" style="border-radius:50%" alt="pjl914335852-ux" /></a>
|
||||||
|
<a href="https://github.com/cat9999aaa"><img src="https://github.com/cat9999aaa.png" width="60" height="60" style="border-radius:50%" alt="cat9999aaa" /></a>
|
||||||
|
<a href="https://github.com/1733055465"><img src="https://github.com/1733055465.png" width="60" height="60" style="border-radius:50%" alt="1733055465" /></a>
|
||||||
|
<a href="https://github.com/kolal2020"><img src="https://github.com/kolal2020.png" width="60" height="60" style="border-radius:50%" alt="kolal2020" /></a>
|
||||||
|
<a href="https://github.com/CyberFFarm"><img src="https://github.com/CyberFFarm.png" width="60" height="60" style="border-radius:50%" alt="CyberFFarm" /></a>
|
||||||
|
<a href="https://github.com/vip3001003"><img src="https://github.com/vip3001003.png" width="60" height="60" style="border-radius:50%" alt="vip3001003" /></a>
|
||||||
|
<a href="https://github.com/mrtluh"><img src="https://github.com/mrtluh.png" width="60" height="60" style="border-radius:50%" alt="mrtluh" /></a>
|
||||||
|
<a href="https://github.com/cpcp1117-source"><img src="https://github.com/cpcp1117-source.png" width="60" height="60" style="border-radius:50%" alt="cpcp1117-source" /></a>
|
||||||
|
<a href="https://github.com/match-007"><img src="https://github.com/match-007.png" width="60" height="60" style="border-radius:50%" alt="match-007" /></a>
|
||||||
|
<a href="https://github.com/leiwuhen1715"><img src="https://github.com/leiwuhen1715.png" width="60" height="60" style="border-radius:50%" alt="leiwuhen1715" /></a>
|
||||||
|
<a href="https://github.com/SHAOXIA1991"><img src="https://github.com/SHAOXIA1991.png" width="60" height="60" style="border-radius:50%" alt="SHAOXIA1991" /></a>
|
||||||
|
|
||||||
|
[Become a sponsor](https://github.com/sponsors/NoFxAiOS)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
# NOFX - AI トレーディングシステム
|
<h1 align="center">NOFX — オープンソース AI トレーディング OS</h1>
|
||||||
|
|
||||||
[](https://golang.org/)
|
<p align="center">
|
||||||
[](https://reactjs.org/)
|
<strong>AI 駆動金融取引のインフラストラクチャレイヤー</strong>
|
||||||
[](https://www.typescriptlang.org/)
|
</p>
|
||||||
[](LICENSE)
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||||
|
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||||
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
**言語:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [日本語](README.md)
|
**言語:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [日本語](README.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## AI 駆動の暗号通貨取引プラットフォーム
|
|
||||||
|
|
||||||
**NOFX** は、複数の AI モデルを使用して暗号通貨先物を自動取引できるオープンソースの AI 取引システムです。Web インターフェースで戦略を設定し、リアルタイムでパフォーマンスを監視し、AI エージェントを競わせて最適な取引アプローチを見つけます。
|
|
||||||
|
|
||||||
### コア機能
|
### コア機能
|
||||||
|
|
||||||
- **マルチ AI サポート**: DeepSeek、Qwen、GPT、Claude、Gemini、Grok、Kimi を実行 - いつでもモデルを切り替え可能
|
- **マルチ AI サポート**: DeepSeek、Qwen、GPT、Claude、Gemini、Grok、Kimi を実行 - いつでもモデルを切り替え可能
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
# NOFX - AI 트레이딩 시스템
|
<h1 align="center">NOFX — 오픈소스 AI 트레이딩 OS</h1>
|
||||||
|
|
||||||
[](https://golang.org/)
|
<p align="center">
|
||||||
[](https://reactjs.org/)
|
<strong>AI 기반 금융 거래를 위한 인프라 레이어</strong>
|
||||||
[](https://www.typescriptlang.org/)
|
</p>
|
||||||
[](LICENSE)
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||||
|
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||||
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
**언어:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [한국어](README.md)
|
**언어:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [한국어](README.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## AI 기반 암호화폐 거래 플랫폼
|
|
||||||
|
|
||||||
**NOFX**는 여러 AI 모델을 실행하여 암호화폐 선물을 자동으로 거래할 수 있는 오픈소스 AI 거래 시스템입니다. 웹 인터페이스를 통해 전략을 구성하고, 실시간으로 성과를 모니터링하며, AI 에이전트들이 최적의 거래 방식을 찾도록 경쟁시킵니다.
|
|
||||||
|
|
||||||
### 핵심 기능
|
### 핵심 기능
|
||||||
|
|
||||||
- **다중 AI 지원**: DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi 실행 - 언제든 모델 전환 가능
|
- **다중 AI 지원**: DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi 실행 - 언제든 모델 전환 가능
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
# NOFX - AI Торговая Система
|
<h1 align="center">NOFX — Open Source AI Торговая ОС</h1>
|
||||||
|
|
||||||
[](https://golang.org/)
|
<p align="center">
|
||||||
[](https://reactjs.org/)
|
<strong>Инфраструктурный слой для AI-powered финансовой торговли</strong>
|
||||||
[](https://www.typescriptlang.org/)
|
</p>
|
||||||
[](LICENSE)
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||||
|
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||||
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
**Языки:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [Русский](README.md)
|
**Языки:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [Русский](README.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Криптовалютная торговая платформа на базе ИИ
|
|
||||||
|
|
||||||
**NOFX** — это open-source AI торговая система, позволяющая запускать несколько AI моделей для автоматической торговли криптовалютными фьючерсами. Настраивайте стратегии через веб-интерфейс, отслеживайте эффективность в реальном времени и позвольте AI агентам конкурировать за лучший торговый подход.
|
|
||||||
|
|
||||||
### Основные функции
|
### Основные функции
|
||||||
|
|
||||||
- **Мульти-AI поддержка**: Запускайте DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi — переключайтесь между моделями в любое время
|
- **Мульти-AI поддержка**: Запускайте DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi — переключайтесь между моделями в любое время
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
# NOFX - AI Торгова Система
|
<h1 align="center">NOFX — Open Source AI Торгова ОС</h1>
|
||||||
|
|
||||||
[](https://golang.org/)
|
<p align="center">
|
||||||
[](https://reactjs.org/)
|
<strong>Інфраструктурний рівень для AI-powered фінансової торгівлі</strong>
|
||||||
[](https://www.typescriptlang.org/)
|
</p>
|
||||||
[](LICENSE)
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||||
|
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||||
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
**Мови:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [Українська](README.md)
|
**Мови:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [Українська](README.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Криптовалютна торгова платформа на базі ШІ
|
|
||||||
|
|
||||||
**NOFX** — це open-source AI торгова система, що дозволяє запускати кілька AI моделей для автоматичної торгівлі криптовалютними ф'ючерсами. Налаштовуйте стратегії через веб-інтерфейс, відстежуйте ефективність у реальному часі та дозвольте AI агентам конкурувати за найкращий торговий підхід.
|
|
||||||
|
|
||||||
### Основні функції
|
### Основні функції
|
||||||
|
|
||||||
- **Мульти-AI підтримка**: Запускайте DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi — перемикайтеся між моделями будь-коли
|
- **Мульти-AI підтримка**: Запускайте DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi — перемикайтеся між моделями будь-коли
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
# NOFX - Hệ Thống Giao Dịch AI
|
<h1 align="center">NOFX — Hệ Điều Hành Giao Dịch AI Mã Nguồn Mở</h1>
|
||||||
|
|
||||||
[](https://golang.org/)
|
<p align="center">
|
||||||
[](https://reactjs.org/)
|
<strong>Lớp cơ sở hạ tầng cho giao dịch tài chính AI-powered</strong>
|
||||||
[](https://www.typescriptlang.org/)
|
</p>
|
||||||
[](LICENSE)
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||||
|
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||||
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
**Ngôn ngữ:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [Tiếng Việt](README.md)
|
**Ngôn ngữ:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [Tiếng Việt](README.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Nền Tảng Giao Dịch Crypto Sử Dụng AI
|
|
||||||
|
|
||||||
**NOFX** là hệ thống giao dịch AI mã nguồn mở cho phép bạn chạy nhiều mô hình AI để tự động giao dịch hợp đồng tương lai crypto. Cấu hình chiến lược qua giao diện web, theo dõi hiệu suất theo thời gian thực, và để các AI agent cạnh tranh tìm ra phương pháp giao dịch tốt nhất.
|
|
||||||
|
|
||||||
### Tính Năng Chính
|
### Tính Năng Chính
|
||||||
|
|
||||||
- **Hỗ trợ Đa AI**: Chạy DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi - chuyển đổi mô hình bất cứ lúc nào
|
- **Hỗ trợ Đa AI**: Chạy DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi - chuyển đổi mô hình bất cứ lúc nào
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
# NOFX - AI 交易系统
|
<h1 align="center">NOFX — 开源 AI 交易操作系统</h1>
|
||||||
|
|
||||||
[](https://golang.org/)
|
<p align="center">
|
||||||
[](https://reactjs.org/)
|
<strong>AI 驱动金融交易的基础设施层</strong>
|
||||||
[](https://www.typescriptlang.org/)
|
</p>
|
||||||
[](LICENSE)
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||||
|
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||||
|
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||||
|
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||||
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
> **语言声明:** 本中文版本文档仅为方便海外华人社区阅读而提供,不代表本软件面向中国大陆、香港、澳门或台湾地区用户开放。如您位于上述地区,请勿使用本软件。
|
> **语言声明:** 本中文版本文档仅为方便海外华人社区阅读而提供,不代表本软件面向中国大陆、香港、澳门或台湾地区用户开放。如您位于上述地区,请勿使用本软件。
|
||||||
|
|
||||||
@@ -16,10 +28,6 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## AI 驱动的加密货币交易平台
|
|
||||||
|
|
||||||
**NOFX** 是一个开源的 AI 交易系统,让你可以运行多个 AI 模型自动交易加密货币期货。通过 Web 界面配置策略,实时监控表现,让多个 AI 代理竞争找出最佳交易方案。
|
|
||||||
|
|
||||||
### 核心功能
|
### 核心功能
|
||||||
|
|
||||||
- **多 AI 支持**: 运行 DeepSeek、通义千问、GPT、Claude、Gemini、Grok、Kimi - 随时切换模型
|
- **多 AI 支持**: 运行 DeepSeek、通义千问、GPT、Claude、Gemini、Grok、Kimi - 随时切换模型
|
||||||
|
|||||||
@@ -447,6 +447,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// 空列表是正常情况,直接返回
|
||||||
return e.filterExcludedCoins(coins), nil
|
return e.filterExcludedCoins(coins), nil
|
||||||
|
|
||||||
case "oi_top":
|
case "oi_top":
|
||||||
@@ -466,6 +467,27 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// 空列表是正常情况,直接返回
|
||||||
|
return e.filterExcludedCoins(coins), nil
|
||||||
|
|
||||||
|
case "oi_low":
|
||||||
|
// 持仓减少榜,适合做空
|
||||||
|
if !coinSource.UseOILow {
|
||||||
|
logger.Infof("⚠️ source_type is 'oi_low' but use_oi_low is false, falling back to static coins")
|
||||||
|
for _, symbol := range coinSource.StaticCoins {
|
||||||
|
symbol = market.Normalize(symbol)
|
||||||
|
candidates = append(candidates, CandidateCoin{
|
||||||
|
Symbol: symbol,
|
||||||
|
Sources: []string{"static"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return e.filterExcludedCoins(candidates), nil
|
||||||
|
}
|
||||||
|
coins, err := e.getOILowCoins(coinSource.OILowLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 空列表是正常情况,直接返回
|
||||||
return e.filterExcludedCoins(coins), nil
|
return e.filterExcludedCoins(coins), nil
|
||||||
|
|
||||||
case "mixed":
|
case "mixed":
|
||||||
@@ -491,6 +513,17 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if coinSource.UseOILow {
|
||||||
|
oiLowCoins, err := e.getOILowCoins(coinSource.OILowLimit)
|
||||||
|
if err != nil {
|
||||||
|
logger.Infof("⚠️ Failed to get OI Low: %v", err)
|
||||||
|
} else {
|
||||||
|
for _, coin := range oiLowCoins {
|
||||||
|
symbolSources[coin.Symbol] = append(symbolSources[coin.Symbol], "oi_low")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, symbol := range coinSource.StaticCoins {
|
for _, symbol := range coinSource.StaticCoins {
|
||||||
symbol = market.Normalize(symbol)
|
symbol = market.Normalize(symbol)
|
||||||
if _, exists := symbolSources[symbol]; !exists {
|
if _, exists := symbolSources[symbol]; !exists {
|
||||||
@@ -561,7 +594,7 @@ func (e *StrategyEngine) getAI500Coins(limit int) ([]CandidateCoin, error) {
|
|||||||
|
|
||||||
func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
|
func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
limit = 20
|
limit = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
positions, err := e.nofxosClient.GetOITopPositions()
|
positions, err := e.nofxosClient.GetOITopPositions()
|
||||||
@@ -583,6 +616,30 @@ func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
|
|||||||
return candidates, nil
|
return candidates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *StrategyEngine) getOILowCoins(limit int) ([]CandidateCoin, error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
positions, err := e.nofxosClient.GetOILowPositions()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidates []CandidateCoin
|
||||||
|
for i, pos := range positions {
|
||||||
|
if i >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
symbol := market.Normalize(pos.Symbol)
|
||||||
|
candidates = append(candidates, CandidateCoin{
|
||||||
|
Symbol: symbol,
|
||||||
|
Sources: []string{"oi_low"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return candidates, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// External & Quant Data
|
// External & Quant Data
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -1289,13 +1346,38 @@ func (e *StrategyEngine) formatPositionInfo(index int, pos PositionInfo, ctx *Co
|
|||||||
|
|
||||||
func (e *StrategyEngine) formatCoinSourceTag(sources []string) string {
|
func (e *StrategyEngine) formatCoinSourceTag(sources []string) string {
|
||||||
if len(sources) > 1 {
|
if len(sources) > 1 {
|
||||||
|
// 多信号源组合
|
||||||
|
hasAI500 := false
|
||||||
|
hasOITop := false
|
||||||
|
hasOILow := false
|
||||||
|
for _, s := range sources {
|
||||||
|
switch s {
|
||||||
|
case "ai500":
|
||||||
|
hasAI500 = true
|
||||||
|
case "oi_top":
|
||||||
|
hasOITop = true
|
||||||
|
case "oi_low":
|
||||||
|
hasOILow = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasAI500 && hasOITop {
|
||||||
return " (AI500+OI_Top dual signal)"
|
return " (AI500+OI_Top dual signal)"
|
||||||
|
}
|
||||||
|
if hasAI500 && hasOILow {
|
||||||
|
return " (AI500+OI_Low dual signal)"
|
||||||
|
}
|
||||||
|
if hasOITop && hasOILow {
|
||||||
|
return " (OI_Top+OI_Low)"
|
||||||
|
}
|
||||||
|
return " (Multiple sources)"
|
||||||
} else if len(sources) == 1 {
|
} else if len(sources) == 1 {
|
||||||
switch sources[0] {
|
switch sources[0] {
|
||||||
case "ai500":
|
case "ai500":
|
||||||
return " (AI500)"
|
return " (AI500)"
|
||||||
case "oi_top":
|
case "oi_top":
|
||||||
return " (OI_Top position growth)"
|
return " (OI_Top 持仓增加)"
|
||||||
|
case "oi_low":
|
||||||
|
return " (OI_Low 持仓减少)"
|
||||||
case "static":
|
case "static":
|
||||||
return " (Manual selection)"
|
return " (Manual selection)"
|
||||||
}
|
}
|
||||||
@@ -1767,8 +1849,8 @@ func compactArrayOpen(s string) string {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int, btcEthPosRatio, altcoinPosRatio float64) error {
|
func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int, btcEthPosRatio, altcoinPosRatio float64) error {
|
||||||
for i, decision := range decisions {
|
for i := range decisions {
|
||||||
if err := validateDecision(&decision, accountEquity, btcEthLeverage, altcoinLeverage, btcEthPosRatio, altcoinPosRatio); err != nil {
|
if err := validateDecision(&decisions[i], accountEquity, btcEthLeverage, altcoinLeverage, btcEthPosRatio, altcoinPosRatio); err != nil {
|
||||||
return fmt.Errorf("decision #%d validation failed: %w", i+1, err)
|
return fmt.Errorf("decision #%d validation failed: %w", i+1, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
108
provider/coinank/coinank_api/depth_ws.go
Normal file
108
provider/coinank/coinank_api/depth_ws.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package coinank_api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"nofx/provider/coinank/coinank_enum"
|
||||||
|
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MainDepthWsUrl = "wss://ws.coinank.com/wsDepth/wsKline"
|
||||||
|
|
||||||
|
type DepthWs struct {
|
||||||
|
conn *websocket.Conn
|
||||||
|
DepthV3Ch <-chan *WsResult[DepthV3]
|
||||||
|
}
|
||||||
|
|
||||||
|
// DepthWsConn connect ws , read data from DepthV3Ch
|
||||||
|
func DepthWsConn(ctx context.Context) (*DepthWs, error) {
|
||||||
|
conn, ch, err := depth_ws(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ws := &DepthWs{
|
||||||
|
conn: conn,
|
||||||
|
DepthV3Ch: ch,
|
||||||
|
}
|
||||||
|
return ws, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe subscribe depth
|
||||||
|
func (ws *DepthWs) Subscribe(symbol string, exchange coinank_enum.Exchange, step string) error {
|
||||||
|
var args = "depthV3@" + symbol + "@" + string(exchange) + "@SWAP@" + step
|
||||||
|
info := SubscribeInfo{
|
||||||
|
Op: "subscribe",
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
json, err := json.Marshal(info)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = websocket.Message.Send(ws.conn, json)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnSubscribe unsubscribe depth
|
||||||
|
func (ws *DepthWs) UnSubscribe(symbol string, exchange coinank_enum.Exchange, step string) error {
|
||||||
|
var args = "depthV3@" + symbol + "@" + string(exchange) + "@SWAP@" + step
|
||||||
|
info := SubscribeInfo{
|
||||||
|
Op: "unsubscribe",
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
json, err := json.Marshal(info)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = websocket.Message.Send(ws.conn, json)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close websocket
|
||||||
|
func (ws *DepthWs) Close() error {
|
||||||
|
return ws.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func depth_ws(ctx context.Context) (*websocket.Conn, <-chan *WsResult[DepthV3], error) {
|
||||||
|
config, err := websocket.NewConfig(MainDepthWsUrl, "http://localhost")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
conn, err := config.DialContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ch := make(chan *WsResult[DepthV3], 1024)
|
||||||
|
go depth_read(conn, ch)
|
||||||
|
return conn, ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func depth_read(conn *websocket.Conn, ch chan *WsResult[DepthV3]) {
|
||||||
|
defer conn.Close()
|
||||||
|
defer close(ch)
|
||||||
|
var msg string
|
||||||
|
for {
|
||||||
|
err := websocket.Message.Receive(conn, &msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var depth WsResult[DepthV3]
|
||||||
|
err = json.Unmarshal([]byte(msg), &depth)
|
||||||
|
if err == nil {
|
||||||
|
ch <- &depth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DepthV3 struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Ts uint64 `json:"ts"`
|
||||||
|
Asks [][]string `json:"asks"`
|
||||||
|
Bids [][]string `json:"bids"`
|
||||||
|
}
|
||||||
42
provider/coinank/coinank_api/depth_ws_test.go
Normal file
42
provider/coinank/coinank_api/depth_ws_test.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package coinank_api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"nofx/provider/coinank/coinank_enum"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDepthWs(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
ws, err := DepthWsConn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for tickers := range ws.DepthV3Ch {
|
||||||
|
msg, err := json.Marshal(tickers)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("json err:", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(msg))
|
||||||
|
}
|
||||||
|
fmt.Println("DepthV3Ch closed")
|
||||||
|
}()
|
||||||
|
err = ws.Subscribe("BTCUSDT", coinank_enum.Binance, "0.1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println("sub success")
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
err = ws.UnSubscribe("BTCUSDT", coinank_enum.Binance, "0.1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println("unsub success")
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
ws.Close()
|
||||||
|
fmt.Println("cancel success")
|
||||||
|
}
|
||||||
@@ -73,8 +73,10 @@ func (c *Client) fetchAI500() ([]CoinData, error) {
|
|||||||
return nil, fmt.Errorf("API returned failure status")
|
return nil, fmt.Errorf("API returned failure status")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 空列表是正常情况,不是错误
|
||||||
if len(response.Data.Coins) == 0 {
|
if len(response.Data.Coins) == 0 {
|
||||||
return nil, fmt.Errorf("coin list is empty")
|
log.Printf("ℹ️ AI500 returned empty coin list (no coins meet criteria currently)")
|
||||||
|
return []CoinData{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set IsAvailable flag
|
// Set IsAvailable flag
|
||||||
|
|||||||
@@ -106,11 +106,11 @@ func (c *Client) fetchOIRanking(rankType, duration string, limit int) ([]OIPosit
|
|||||||
|
|
||||||
// GetOITopPositions retrieves top OI increase positions (legacy compatibility)
|
// GetOITopPositions retrieves top OI increase positions (legacy compatibility)
|
||||||
func (c *Client) GetOITopPositions() ([]OIPosition, error) {
|
func (c *Client) GetOITopPositions() ([]OIPosition, error) {
|
||||||
data, err := c.GetOIRanking("1h", 20)
|
positions, _, err := c.fetchOIRanking("top", "1h", 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return data.TopPositions, nil
|
return positions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOITopSymbols retrieves OI top coin symbol list
|
// GetOITopSymbols retrieves OI top coin symbol list
|
||||||
@@ -129,6 +129,31 @@ func (c *Client) GetOITopSymbols() ([]string, error) {
|
|||||||
return symbols, nil
|
return symbols, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetOILowPositions retrieves OI decrease positions (for short opportunities)
|
||||||
|
func (c *Client) GetOILowPositions() ([]OIPosition, error) {
|
||||||
|
positions, _, err := c.fetchOIRanking("low", "1h", 20)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return positions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOILowSymbols retrieves OI low coin symbol list
|
||||||
|
func (c *Client) GetOILowSymbols() ([]string, error) {
|
||||||
|
positions, err := c.GetOILowPositions()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var symbols []string
|
||||||
|
for _, pos := range positions {
|
||||||
|
symbol := NormalizeSymbol(pos.Symbol)
|
||||||
|
symbols = append(symbols, symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
return symbols, nil
|
||||||
|
}
|
||||||
|
|
||||||
// FormatOIRankingForAI formats OI ranking data for AI consumption
|
// FormatOIRankingForAI formats OI ranking data for AI consumption
|
||||||
func FormatOIRankingForAI(data *OIRankingData, lang Language) string {
|
func FormatOIRankingForAI(data *OIRankingData, lang Language) string {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
|
|||||||
@@ -158,16 +158,19 @@ func (s *PositionStore) UpdatePositionQuantityAndPrice(id int64, addQty float64,
|
|||||||
newEntryPrice := (pos.EntryPrice*pos.Quantity + addPrice*addQty) / newQty
|
newEntryPrice := (pos.EntryPrice*pos.Quantity + addPrice*addQty) / newQty
|
||||||
newEntryPrice = math.Round(newEntryPrice*100) / 100
|
newEntryPrice = math.Round(newEntryPrice*100) / 100
|
||||||
newFee := pos.Fee + addFee
|
newFee := pos.Fee + addFee
|
||||||
|
nowMs := time.Now().UTC().UnixMilli()
|
||||||
|
|
||||||
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||||
"quantity": newQty,
|
"quantity": newQty,
|
||||||
"entry_quantity": newEntryQty,
|
"entry_quantity": newEntryQty,
|
||||||
"entry_price": newEntryPrice,
|
"entry_price": newEntryPrice,
|
||||||
"fee": newFee,
|
"fee": newFee,
|
||||||
|
"updated_at": nowMs,
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReducePositionQuantity reduces position quantity for partial close
|
// ReducePositionQuantity reduces position quantity for partial close
|
||||||
|
// If quantity reaches 0 (or near 0), automatically closes the position
|
||||||
func (s *PositionStore) ReducePositionQuantity(id int64, reduceQty float64, exitPrice float64, addFee float64, addPnL float64) error {
|
func (s *PositionStore) ReducePositionQuantity(id int64, reduceQty float64, exitPrice float64, addFee float64, addPnL float64) error {
|
||||||
var pos TraderPosition
|
var pos TraderPosition
|
||||||
if err := s.db.First(&pos, id).Error; err != nil {
|
if err := s.db.First(&pos, id).Error; err != nil {
|
||||||
@@ -187,19 +190,40 @@ func (s *PositionStore) ReducePositionQuantity(id int64, reduceQty float64, exit
|
|||||||
newExitPrice = math.Round(newExitPrice*100) / 100
|
newExitPrice = math.Round(newExitPrice*100) / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nowMs := time.Now().UTC().UnixMilli()
|
||||||
|
|
||||||
|
// Check if position should be fully closed (quantity reduced to ~0)
|
||||||
|
const QUANTITY_TOLERANCE = 0.0001
|
||||||
|
if newQty <= QUANTITY_TOLERANCE {
|
||||||
|
// Auto-close: set status to CLOSED
|
||||||
|
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||||
|
"quantity": 0,
|
||||||
|
"fee": newFee,
|
||||||
|
"exit_price": newExitPrice,
|
||||||
|
"realized_pnl": newPnL,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"exit_time": nowMs,
|
||||||
|
"close_reason": "sync",
|
||||||
|
"updated_at": nowMs,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||||
"quantity": newQty,
|
"quantity": newQty,
|
||||||
"fee": newFee,
|
"fee": newFee,
|
||||||
"exit_price": newExitPrice,
|
"exit_price": newExitPrice,
|
||||||
"realized_pnl": newPnL,
|
"realized_pnl": newPnL,
|
||||||
|
"updated_at": nowMs,
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePositionExchangeInfo updates exchange_id and exchange_type
|
// UpdatePositionExchangeInfo updates exchange_id and exchange_type
|
||||||
func (s *PositionStore) UpdatePositionExchangeInfo(id int64, exchangeID, exchangeType string) error {
|
func (s *PositionStore) UpdatePositionExchangeInfo(id int64, exchangeID, exchangeType string) error {
|
||||||
|
nowMs := time.Now().UTC().UnixMilli()
|
||||||
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||||
"exchange_id": exchangeID,
|
"exchange_id": exchangeID,
|
||||||
"exchange_type": exchangeType,
|
"exchange_type": exchangeType,
|
||||||
|
"updated_at": nowMs,
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ type PromptSectionsConfig struct {
|
|||||||
|
|
||||||
// CoinSourceConfig coin source configuration
|
// CoinSourceConfig coin source configuration
|
||||||
type CoinSourceConfig struct {
|
type CoinSourceConfig struct {
|
||||||
// source type: "static" | "ai500" | "oi_top" | "mixed"
|
// source type: "static" | "ai500" | "oi_top" | "oi_low" | "mixed"
|
||||||
SourceType string `json:"source_type"`
|
SourceType string `json:"source_type"`
|
||||||
// static coin list (used when source_type = "static")
|
// static coin list (used when source_type = "static")
|
||||||
StaticCoins []string `json:"static_coins,omitempty"`
|
StaticCoins []string `json:"static_coins,omitempty"`
|
||||||
@@ -107,10 +107,14 @@ type CoinSourceConfig struct {
|
|||||||
UseAI500 bool `json:"use_ai500"`
|
UseAI500 bool `json:"use_ai500"`
|
||||||
// AI500 coin pool maximum count
|
// AI500 coin pool maximum count
|
||||||
AI500Limit int `json:"ai500_limit,omitempty"`
|
AI500Limit int `json:"ai500_limit,omitempty"`
|
||||||
// whether to use OI Top
|
// whether to use OI Top (持仓增加榜,适合做多)
|
||||||
UseOITop bool `json:"use_oi_top"`
|
UseOITop bool `json:"use_oi_top"`
|
||||||
// OI Top maximum count
|
// OI Top maximum count
|
||||||
OITopLimit int `json:"oi_top_limit,omitempty"`
|
OITopLimit int `json:"oi_top_limit,omitempty"`
|
||||||
|
// whether to use OI Low (持仓减少榜,适合做空)
|
||||||
|
UseOILow bool `json:"use_oi_low"`
|
||||||
|
// OI Low maximum count
|
||||||
|
OILowLimit int `json:"oi_low_limit,omitempty"`
|
||||||
// Note: API URLs are now built automatically using NofxOSAPIKey from IndicatorConfig
|
// Note: API URLs are now built automatically using NofxOSAPIKey from IndicatorConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +252,9 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
|
|||||||
UseAI500: true,
|
UseAI500: true,
|
||||||
AI500Limit: 10,
|
AI500Limit: 10,
|
||||||
UseOITop: false,
|
UseOITop: false,
|
||||||
OITopLimit: 20,
|
OITopLimit: 10,
|
||||||
|
UseOILow: false,
|
||||||
|
OILowLimit: 10,
|
||||||
},
|
},
|
||||||
Indicators: IndicatorConfig{
|
Indicators: IndicatorConfig{
|
||||||
Klines: KlineConfig{
|
Klines: KlineConfig{
|
||||||
|
|||||||
@@ -534,6 +534,12 @@ func (at *AutoTrader) runCycle() error {
|
|||||||
return fmt.Errorf("failed to build trading context: %w", err)
|
return fmt.Errorf("failed to build trading context: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果没有候选币种,友好提示并跳过本周期
|
||||||
|
if len(ctx.CandidateCoins) == 0 {
|
||||||
|
logger.Infof("ℹ️ No candidate coins available, skipping this cycle")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Save equity snapshot independently (decoupled from AI decision, used for drawing profit curve)
|
// Save equity snapshot independently (decoupled from AI decision, used for drawing profit curve)
|
||||||
at.saveEquitySnapshot(ctx)
|
at.saveEquitySnapshot(ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -56,12 +56,8 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record current time BEFORE querying, to avoid missing trades during sync
|
logger.Infof("🔄 Syncing Binance trades from: %s (UTC) [ms: %d, now: %d]",
|
||||||
// This prevents race condition where trades happen between query and lastSyncTime update
|
time.UnixMilli(lastSyncTimeMs).UTC().Format("2006-01-02 15:04:05"), lastSyncTimeMs, nowMs)
|
||||||
syncStartTimeMs := nowMs
|
|
||||||
|
|
||||||
logger.Infof("🔄 Syncing Binance trades from: %s (UTC)",
|
|
||||||
time.UnixMilli(lastSyncTimeMs).UTC().Format("2006-01-02 15:04:05"))
|
|
||||||
|
|
||||||
// Step 1: Get max trade IDs from local DB for incremental sync
|
// Step 1: Get max trade IDs from local DB for incremental sync
|
||||||
maxTradeIDs, err := orderStore.GetMaxTradeIDsByExchange(exchangeID)
|
maxTradeIDs, err := orderStore.GetMaxTradeIDsByExchange(exchangeID)
|
||||||
@@ -100,10 +96,10 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
|
|||||||
symbolMap[s] = true
|
symbolMap[s] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method 4: FALLBACK - Query REALIZED_PNL income to find symbols with closed trades
|
// Method 4: ALWAYS query REALIZED_PNL income to find symbols with closed trades
|
||||||
// This catches trades that COMMISSION missed (VIP users, BNB fee discount)
|
// This catches trades that COMMISSION missed (VIP users, BNB fee discount)
|
||||||
if len(symbolMap) == 0 {
|
// IMPORTANT: Must run always, not just when symbolMap is empty,
|
||||||
logger.Infof(" 🔍 No symbols found, trying REALIZED_PNL fallback...")
|
// because a position might be fully closed (no active position) but have PnL
|
||||||
pnlSymbols, err := t.GetPnLSymbols(lastSyncTime)
|
pnlSymbols, err := t.GetPnLSymbols(lastSyncTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Infof(" ⚠️ Failed to get PnL symbols: %v", err)
|
logger.Infof(" ⚠️ Failed to get PnL symbols: %v", err)
|
||||||
@@ -113,7 +109,6 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
|
|||||||
symbolMap[s] = true
|
symbolMap[s] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var changedSymbols []string
|
var changedSymbols []string
|
||||||
for s := range symbolMap {
|
for s := range symbolMap {
|
||||||
@@ -122,10 +117,9 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
|
|||||||
|
|
||||||
if len(changedSymbols) == 0 {
|
if len(changedSymbols) == 0 {
|
||||||
logger.Infof("📭 No symbols with new trades to sync")
|
logger.Infof("📭 No symbols with new trades to sync")
|
||||||
// Update last sync time even if no changes
|
// DON'T update lastSyncTime to current time here!
|
||||||
binanceSyncStateMutex.Lock()
|
// Keep using the last actual trade time from DB to avoid creating gaps
|
||||||
binanceSyncState[exchangeID] = syncStartTimeMs
|
// The lastSyncTimeMs from DB already has +1000ms buffer added
|
||||||
binanceSyncStateMutex.Unlock()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,17 +152,12 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
|
|||||||
|
|
||||||
logger.Infof("📥 Received %d trades from Binance (%d API calls)", len(allTrades), apiCalls)
|
logger.Infof("📥 Received %d trades from Binance (%d API calls)", len(allTrades), apiCalls)
|
||||||
|
|
||||||
// Only update last sync time if ALL symbols were successfully queried
|
|
||||||
// This prevents data loss when some symbols fail due to rate limit or network issues
|
|
||||||
if len(failedSymbols) == 0 {
|
|
||||||
binanceSyncStateMutex.Lock()
|
|
||||||
binanceSyncState[exchangeID] = syncStartTimeMs
|
|
||||||
binanceSyncStateMutex.Unlock()
|
|
||||||
} else {
|
|
||||||
logger.Infof(" ⚠️ %d symbols failed, not updating lastSyncTime to retry next time: %v", len(failedSymbols), failedSymbols)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(allTrades) == 0 {
|
if len(allTrades) == 0 {
|
||||||
|
// No trades returned, but symbols were detected - might be false positive from COMMISSION/PnL detection
|
||||||
|
// Don't update lastSyncTime, keep using DB value
|
||||||
|
if len(failedSymbols) > 0 {
|
||||||
|
logger.Infof(" ⚠️ %d symbols failed: %v", len(failedSymbols), failedSymbols)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,10 +171,12 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
|
|||||||
posBuilder := store.NewPositionBuilder(positionStore)
|
posBuilder := store.NewPositionBuilder(positionStore)
|
||||||
syncedCount := 0
|
syncedCount := 0
|
||||||
|
|
||||||
|
skippedCount := 0
|
||||||
for _, trade := range allTrades {
|
for _, trade := range allTrades {
|
||||||
// Check if trade already exists
|
// Check if trade already exists
|
||||||
existing, err := orderStore.GetOrderByExchangeID(exchangeID, trade.TradeID)
|
existing, err := orderStore.GetOrderByExchangeID(exchangeID, trade.TradeID)
|
||||||
if err == nil && existing != nil {
|
if err == nil && existing != nil {
|
||||||
|
skippedCount++
|
||||||
continue // Trade already exists, skip
|
continue // Trade already exists, skip
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +271,21 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
|
|||||||
trade.Time.UTC().Format("01-02 15:04:05"))
|
trade.Time.UTC().Format("01-02 15:04:05"))
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("✅ Binance order sync completed: %d new trades synced", syncedCount)
|
// Update lastSyncTime to the LATEST trade time (not current time!)
|
||||||
|
// This ensures next sync starts from where we left off, not from "now"
|
||||||
|
// allTrades is already sorted by time ASC, so last element is the latest
|
||||||
|
if len(allTrades) > 0 && len(failedSymbols) == 0 {
|
||||||
|
latestTradeTimeMs := allTrades[len(allTrades)-1].Time.UTC().UnixMilli()
|
||||||
|
binanceSyncStateMutex.Lock()
|
||||||
|
binanceSyncState[exchangeID] = latestTradeTimeMs
|
||||||
|
binanceSyncStateMutex.Unlock()
|
||||||
|
logger.Infof("📅 Updated lastSyncTime to latest trade: %s (UTC)",
|
||||||
|
time.UnixMilli(latestTradeTimeMs).UTC().Format("2006-01-02 15:04:05"))
|
||||||
|
} else if len(failedSymbols) > 0 {
|
||||||
|
logger.Infof(" ⚠️ %d symbols failed, not updating lastSyncTime to retry next time: %v", len(failedSymbols), failedSymbols)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("✅ Binance order sync completed: %d new trades synced, %d skipped (already exist)", syncedCount, skippedCount)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Plus, X, Database, TrendingUp, List, Ban, Zap } from 'lucide-react'
|
import { Plus, X, Database, TrendingUp, TrendingDown, List, Ban, Zap, Shuffle } from 'lucide-react'
|
||||||
import type { CoinSourceConfig } from '../../types'
|
import type { CoinSourceConfig } from '../../types'
|
||||||
|
|
||||||
interface CoinSourceEditorProps {
|
interface CoinSourceEditorProps {
|
||||||
@@ -23,27 +23,38 @@ export function CoinSourceEditor({
|
|||||||
sourceType: { zh: '数据来源类型', en: 'Source Type' },
|
sourceType: { zh: '数据来源类型', en: 'Source Type' },
|
||||||
static: { zh: '静态列表', en: 'Static List' },
|
static: { zh: '静态列表', en: 'Static List' },
|
||||||
ai500: { zh: 'AI500 数据源', en: 'AI500 Data Provider' },
|
ai500: { zh: 'AI500 数据源', en: 'AI500 Data Provider' },
|
||||||
oi_top: { zh: 'OI Top 持仓增长', en: 'OI Top' },
|
oi_top: { zh: 'OI 持仓增加', en: 'OI Increase' },
|
||||||
|
oi_low: { zh: 'OI 持仓减少', en: 'OI Decrease' },
|
||||||
mixed: { zh: '混合模式', en: 'Mixed Mode' },
|
mixed: { zh: '混合模式', en: 'Mixed Mode' },
|
||||||
staticCoins: { zh: '自定义币种', en: 'Custom Coins' },
|
staticCoins: { zh: '自定义币种', en: 'Custom Coins' },
|
||||||
addCoin: { zh: '添加币种', en: 'Add Coin' },
|
addCoin: { zh: '添加币种', en: 'Add Coin' },
|
||||||
useAI500: { zh: '启用 AI500 数据源', en: 'Enable AI500 Data Provider' },
|
useAI500: { zh: '启用 AI500 数据源', en: 'Enable AI500 Data Provider' },
|
||||||
ai500Limit: { zh: '数量上限', en: 'Limit' },
|
ai500Limit: { zh: '数量上限', en: 'Limit' },
|
||||||
useOITop: { zh: '启用 OI Top 数据', en: 'Enable OI Top' },
|
useOITop: { zh: '启用 OI 持仓增加榜', en: 'Enable OI Increase' },
|
||||||
oiTopLimit: { zh: '数量上限', en: 'Limit' },
|
oiTopLimit: { zh: '数量上限', en: 'Limit' },
|
||||||
|
useOILow: { zh: '启用 OI 持仓减少榜', en: 'Enable OI Decrease' },
|
||||||
|
oiLowLimit: { zh: '数量上限', en: 'Limit' },
|
||||||
staticDesc: { zh: '手动指定交易币种列表', en: 'Manually specify trading coins' },
|
staticDesc: { zh: '手动指定交易币种列表', en: 'Manually specify trading coins' },
|
||||||
ai500Desc: {
|
ai500Desc: {
|
||||||
zh: '使用 AI500 智能筛选的热门币种',
|
zh: '使用 AI500 智能筛选的热门币种',
|
||||||
en: 'Use AI500 smart-filtered popular coins',
|
en: 'Use AI500 smart-filtered popular coins',
|
||||||
},
|
},
|
||||||
oiTopDesc: {
|
oiTopDesc: {
|
||||||
zh: '使用持仓量增长最快的币种',
|
zh: '持仓增加榜,适合做多',
|
||||||
en: 'Use coins with fastest OI growth',
|
en: 'OI increase ranking, for long',
|
||||||
|
},
|
||||||
|
oi_lowDesc: {
|
||||||
|
zh: '持仓减少榜,适合做空',
|
||||||
|
en: 'OI decrease ranking, for short',
|
||||||
},
|
},
|
||||||
mixedDesc: {
|
mixedDesc: {
|
||||||
zh: '组合多种数据源,AI500 + OI Top + 自定义',
|
zh: '组合多种数据源',
|
||||||
en: 'Combine multiple sources: AI500 + OI Top + Custom',
|
en: 'Combine multiple sources',
|
||||||
},
|
},
|
||||||
|
mixedConfig: { zh: '组合数据源配置', en: 'Combined Sources Configuration' },
|
||||||
|
mixedSummary: { zh: '已选组合', en: 'Selected Sources' },
|
||||||
|
maxCoins: { zh: '最多', en: 'Up to' },
|
||||||
|
coins: { zh: '个币种', en: 'coins' },
|
||||||
dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration' },
|
dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration' },
|
||||||
excludedCoins: { zh: '排除币种', en: 'Excluded Coins' },
|
excludedCoins: { zh: '排除币种', en: 'Excluded Coins' },
|
||||||
excludedCoinsDesc: { zh: '这些币种将从所有数据源中排除,不会被交易', en: 'These coins will be excluded from all sources and will not be traded' },
|
excludedCoinsDesc: { zh: '这些币种将从所有数据源中排除,不会被交易', en: 'These coins will be excluded from all sources and will not be traded' },
|
||||||
@@ -57,9 +68,35 @@ export function CoinSourceEditor({
|
|||||||
{ value: 'static', icon: List, color: '#848E9C' },
|
{ value: 'static', icon: List, color: '#848E9C' },
|
||||||
{ value: 'ai500', icon: Database, color: '#F0B90B' },
|
{ value: 'ai500', icon: Database, color: '#F0B90B' },
|
||||||
{ value: 'oi_top', icon: TrendingUp, color: '#0ECB81' },
|
{ value: 'oi_top', icon: TrendingUp, color: '#0ECB81' },
|
||||||
{ value: 'mixed', icon: Database, color: '#60a5fa' },
|
{ value: 'oi_low', icon: TrendingDown, color: '#F6465D' },
|
||||||
|
{ value: 'mixed', icon: Shuffle, color: '#60a5fa' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
// Calculate mixed mode summary
|
||||||
|
const getMixedSummary = () => {
|
||||||
|
const sources: string[] = []
|
||||||
|
let totalLimit = 0
|
||||||
|
|
||||||
|
if (config.use_ai500) {
|
||||||
|
sources.push(`AI500(${config.ai500_limit || 10})`)
|
||||||
|
totalLimit += config.ai500_limit || 10
|
||||||
|
}
|
||||||
|
if (config.use_oi_top) {
|
||||||
|
sources.push(`${language === 'zh' ? 'OI增' : 'OI↑'}(${config.oi_top_limit || 10})`)
|
||||||
|
totalLimit += config.oi_top_limit || 10
|
||||||
|
}
|
||||||
|
if (config.use_oi_low) {
|
||||||
|
sources.push(`${language === 'zh' ? 'OI减' : 'OI↓'}(${config.oi_low_limit || 10})`)
|
||||||
|
totalLimit += config.oi_low_limit || 10
|
||||||
|
}
|
||||||
|
if ((config.static_coins || []).length > 0) {
|
||||||
|
sources.push(`${language === 'zh' ? '自定义' : 'Custom'}(${config.static_coins?.length || 0})`)
|
||||||
|
totalLimit += config.static_coins?.length || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sources, totalLimit }
|
||||||
|
}
|
||||||
|
|
||||||
// xyz dex assets (stocks, forex, commodities) - should NOT get USDT suffix
|
// xyz dex assets (stocks, forex, commodities) - should NOT get USDT suffix
|
||||||
const xyzDexAssets = new Set([
|
const xyzDexAssets = new Set([
|
||||||
// Stocks
|
// Stocks
|
||||||
@@ -156,7 +193,7 @@ export function CoinSourceEditor({
|
|||||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||||
{t('sourceType')}
|
{t('sourceType')}
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-4 gap-3">
|
<div className="grid grid-cols-5 gap-2">
|
||||||
{sourceTypes.map(({ value, icon: Icon, color }) => (
|
{sourceTypes.map(({ value, icon: Icon, color }) => (
|
||||||
<button
|
<button
|
||||||
key={value}
|
key={value}
|
||||||
@@ -182,8 +219,8 @@ export function CoinSourceEditor({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Static Coins */}
|
{/* Static Coins - only for static mode */}
|
||||||
{(config.source_type === 'static' || config.source_type === 'mixed') && (
|
{config.source_type === 'static' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||||
{t('staticCoins')}
|
{t('staticCoins')}
|
||||||
@@ -283,8 +320,8 @@ export function CoinSourceEditor({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AI500 Options */}
|
{/* AI500 Options - only for ai500 mode */}
|
||||||
{(config.source_type === 'ai500' || config.source_type === 'mixed') && (
|
{config.source_type === 'ai500' && (
|
||||||
<div
|
<div
|
||||||
className="p-4 rounded-lg bg-nofx-gold/5 border border-nofx-gold/20"
|
className="p-4 rounded-lg bg-nofx-gold/5 border border-nofx-gold/20"
|
||||||
>
|
>
|
||||||
@@ -340,8 +377,8 @@ export function CoinSourceEditor({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* OI Top Options */}
|
{/* OI Top Options - only for oi_top mode */}
|
||||||
{(config.source_type === 'oi_top' || config.source_type === 'mixed') && (
|
{config.source_type === 'oi_top' && (
|
||||||
<div
|
<div
|
||||||
className="p-4 rounded-lg bg-nofx-success/5 border border-nofx-success/20"
|
className="p-4 rounded-lg bg-nofx-success/5 border border-nofx-success/20"
|
||||||
>
|
>
|
||||||
@@ -349,7 +386,7 @@ export function CoinSourceEditor({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
||||||
<span className="text-sm font-medium text-nofx-text">
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
OI Top {t('dataSourceConfig')}
|
OI {language === 'zh' ? '持仓增加榜' : 'Increase'} {t('dataSourceConfig')}
|
||||||
</span>
|
</span>
|
||||||
<NofxOSBadge />
|
<NofxOSBadge />
|
||||||
</div>
|
</div>
|
||||||
@@ -375,10 +412,10 @@ export function CoinSourceEditor({
|
|||||||
{t('oiTopLimit')}:
|
{t('oiTopLimit')}:
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={config.oi_top_limit || 20}
|
value={config.oi_top_limit || 10}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
!disabled &&
|
!disabled &&
|
||||||
onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 20 })
|
onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 10 })
|
||||||
}
|
}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
@@ -396,6 +433,306 @@ export function CoinSourceEditor({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* OI Low Options - only for oi_low mode */}
|
||||||
|
{config.source_type === 'oi_low' && (
|
||||||
|
<div
|
||||||
|
className="p-4 rounded-lg bg-nofx-danger/5 border border-nofx-danger/20"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<TrendingDown className="w-4 h-4 text-nofx-danger" />
|
||||||
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
|
OI {language === 'zh' ? '持仓减少榜' : 'Decrease'} {t('dataSourceConfig')}
|
||||||
|
</span>
|
||||||
|
<NofxOSBadge />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.use_oi_low}
|
||||||
|
onChange={(e) =>
|
||||||
|
!disabled && onChange({ ...config, use_oi_low: e.target.checked })
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-5 h-5 rounded accent-red-500"
|
||||||
|
/>
|
||||||
|
<span className="text-nofx-text">{t('useOILow')}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{config.use_oi_low && (
|
||||||
|
<div className="flex items-center gap-3 pl-8">
|
||||||
|
<span className="text-sm text-nofx-text-muted">
|
||||||
|
{t('oiLowLimit')}:
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
value={config.oi_low_limit || 10}
|
||||||
|
onChange={(e) =>
|
||||||
|
!disabled &&
|
||||||
|
onChange({ ...config, oi_low_limit: parseInt(e.target.value) || 10 })
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
|
>
|
||||||
|
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||||
|
<option key={n} value={n}>{n}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||||
|
{t('nofxosNote')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Mixed Mode - Unified Card Selector */}
|
||||||
|
{config.source_type === 'mixed' && (
|
||||||
|
<div className="p-4 rounded-lg bg-blue-500/5 border border-blue-500/20">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Shuffle className="w-4 h-4 text-blue-400" />
|
||||||
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
|
{t('mixedConfig')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 4 Source Cards in 2x2 Grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||||
|
{/* AI500 Card */}
|
||||||
|
<div
|
||||||
|
className={`p-3 rounded-lg border transition-all cursor-pointer ${
|
||||||
|
config.use_ai500
|
||||||
|
? 'bg-nofx-gold/10 border-nofx-gold/50'
|
||||||
|
: 'bg-nofx-bg border-nofx-border hover:border-nofx-gold/30'
|
||||||
|
}`}
|
||||||
|
onClick={() => !disabled && onChange({ ...config, use_ai500: !config.use_ai500 })}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.use_ai500}
|
||||||
|
onChange={(e) => !disabled && onChange({ ...config, use_ai500: e.target.checked })}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-4 h-4 rounded accent-nofx-gold"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<Database className="w-4 h-4 text-nofx-gold" />
|
||||||
|
<span className="text-sm font-medium text-nofx-text">AI500</span>
|
||||||
|
<NofxOSBadge />
|
||||||
|
</div>
|
||||||
|
{config.use_ai500 && (
|
||||||
|
<div className="flex items-center gap-2 mt-2 pl-6">
|
||||||
|
<span className="text-xs text-nofx-text-muted">Limit:</span>
|
||||||
|
<select
|
||||||
|
value={config.ai500_limit || 10}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
!disabled && onChange({ ...config, ai500_limit: parseInt(e.target.value) || 10 })
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
className="px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||||
|
<option key={n} value={n}>{n}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* OI Top Card */}
|
||||||
|
<div
|
||||||
|
className={`p-3 rounded-lg border transition-all cursor-pointer ${
|
||||||
|
config.use_oi_top
|
||||||
|
? 'bg-nofx-success/10 border-nofx-success/50'
|
||||||
|
: 'bg-nofx-bg border-nofx-border hover:border-nofx-success/30'
|
||||||
|
}`}
|
||||||
|
onClick={() => !disabled && onChange({ ...config, use_oi_top: !config.use_oi_top })}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.use_oi_top}
|
||||||
|
onChange={(e) => !disabled && onChange({ ...config, use_oi_top: e.target.checked })}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-4 h-4 rounded accent-nofx-success"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
||||||
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
|
{language === 'zh' ? 'OI 增加' : 'OI Increase'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
|
||||||
|
{language === 'zh' ? '适合做多' : 'For long'}
|
||||||
|
</p>
|
||||||
|
{config.use_oi_top && (
|
||||||
|
<div className="flex items-center gap-2 mt-2 pl-6">
|
||||||
|
<span className="text-xs text-nofx-text-muted">Limit:</span>
|
||||||
|
<select
|
||||||
|
value={config.oi_top_limit || 10}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
!disabled && onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 10 })
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
className="px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||||
|
<option key={n} value={n}>{n}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* OI Low Card */}
|
||||||
|
<div
|
||||||
|
className={`p-3 rounded-lg border transition-all cursor-pointer ${
|
||||||
|
config.use_oi_low
|
||||||
|
? 'bg-nofx-danger/10 border-nofx-danger/50'
|
||||||
|
: 'bg-nofx-bg border-nofx-border hover:border-nofx-danger/30'
|
||||||
|
}`}
|
||||||
|
onClick={() => !disabled && onChange({ ...config, use_oi_low: !config.use_oi_low })}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.use_oi_low}
|
||||||
|
onChange={(e) => !disabled && onChange({ ...config, use_oi_low: e.target.checked })}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-4 h-4 rounded accent-red-500"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<TrendingDown className="w-4 h-4 text-nofx-danger" />
|
||||||
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
|
{language === 'zh' ? 'OI 减少' : 'OI Decrease'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
|
||||||
|
{language === 'zh' ? '适合做空' : 'For short'}
|
||||||
|
</p>
|
||||||
|
{config.use_oi_low && (
|
||||||
|
<div className="flex items-center gap-2 mt-2 pl-6">
|
||||||
|
<span className="text-xs text-nofx-text-muted">Limit:</span>
|
||||||
|
<select
|
||||||
|
value={config.oi_low_limit || 10}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
!disabled && onChange({ ...config, oi_low_limit: parseInt(e.target.value) || 10 })
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
className="px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||||
|
<option key={n} value={n}>{n}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Static/Custom Card */}
|
||||||
|
<div
|
||||||
|
className={`p-3 rounded-lg border transition-all cursor-pointer ${
|
||||||
|
(config.static_coins || []).length > 0
|
||||||
|
? 'bg-gray-500/10 border-gray-500/50'
|
||||||
|
: 'bg-nofx-bg border-nofx-border hover:border-gray-500/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<List className="w-4 h-4 text-gray-400" />
|
||||||
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
|
{language === 'zh' ? '自定义' : 'Custom'}
|
||||||
|
</span>
|
||||||
|
{(config.static_coins || []).length > 0 && (
|
||||||
|
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400">
|
||||||
|
{config.static_coins?.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-2">
|
||||||
|
{(config.static_coins || []).slice(0, 3).map((coin) => (
|
||||||
|
<span
|
||||||
|
key={coin}
|
||||||
|
className="flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-nofx-bg-lighter text-nofx-text"
|
||||||
|
>
|
||||||
|
{coin}
|
||||||
|
{!disabled && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleRemoveCoin(coin)
|
||||||
|
}}
|
||||||
|
className="hover:text-red-400 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-2.5 h-2.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{(config.static_coins || []).length > 3 && (
|
||||||
|
<span className="text-xs text-nofx-text-muted">
|
||||||
|
+{(config.static_coins?.length || 0) - 3}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!disabled && (
|
||||||
|
<div className="flex gap-1 mt-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newCoin}
|
||||||
|
onChange={(e) => setNewCoin(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (e.key === 'Enter') handleAddCoin()
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
placeholder="BTC, ETH..."
|
||||||
|
className="flex-1 px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleAddCoin()
|
||||||
|
}}
|
||||||
|
className="px-2 py-1 rounded text-xs bg-nofx-gold text-black hover:bg-yellow-500"
|
||||||
|
>
|
||||||
|
<Plus className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary */}
|
||||||
|
{(() => {
|
||||||
|
const { sources, totalLimit } = getMixedSummary()
|
||||||
|
if (sources.length === 0) return null
|
||||||
|
return (
|
||||||
|
<div className="p-2 rounded bg-nofx-bg border border-nofx-border">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-nofx-text-muted">{t('mixedSummary')}:</span>
|
||||||
|
<span className="text-nofx-text font-medium">
|
||||||
|
{sources.join(' + ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-nofx-text-muted mt-1">
|
||||||
|
{t('maxCoins')} {totalLimit} {t('coins')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function GridConfigEditor({
|
|||||||
totalInvestment: { zh: '投资金额 (USDT)', en: 'Investment (USDT)' },
|
totalInvestment: { zh: '投资金额 (USDT)', en: 'Investment (USDT)' },
|
||||||
totalInvestmentDesc: { zh: '网格策略的总投资金额', en: 'Total investment for grid strategy' },
|
totalInvestmentDesc: { zh: '网格策略的总投资金额', en: 'Total investment for grid strategy' },
|
||||||
leverage: { zh: '杠杆倍数', en: 'Leverage' },
|
leverage: { zh: '杠杆倍数', en: 'Leverage' },
|
||||||
leverageDesc: { zh: '交易使用的杠杆倍数 (1-20)', en: 'Leverage for trading (1-20)' },
|
leverageDesc: { zh: '交易使用的杠杆倍数 (1-5)', en: 'Leverage for trading (1-5)' },
|
||||||
|
|
||||||
// Grid parameters
|
// Grid parameters
|
||||||
gridCount: { zh: '网格数量', en: 'Grid Count' },
|
gridCount: { zh: '网格数量', en: 'Grid Count' },
|
||||||
@@ -171,7 +171,7 @@ export function GridConfigEditor({
|
|||||||
onChange={(e) => updateField('leverage', parseInt(e.target.value) || 5)}
|
onChange={(e) => updateField('leverage', parseInt(e.target.value) || 5)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
min={1}
|
min={1}
|
||||||
max={20}
|
max={5}
|
||||||
className="w-full px-3 py-2 rounded"
|
className="w-full px-3 py-2 rounded"
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -509,13 +509,15 @@ export interface GridStrategyConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CoinSourceConfig {
|
export interface CoinSourceConfig {
|
||||||
source_type: 'static' | 'ai500' | 'oi_top' | 'mixed';
|
source_type: 'static' | 'ai500' | 'oi_top' | 'oi_low' | 'mixed';
|
||||||
static_coins?: string[];
|
static_coins?: string[];
|
||||||
excluded_coins?: string[]; // 排除的币种列表
|
excluded_coins?: string[]; // 排除的币种列表
|
||||||
use_ai500: boolean;
|
use_ai500: boolean;
|
||||||
ai500_limit?: number;
|
ai500_limit?: number;
|
||||||
use_oi_top: boolean;
|
use_oi_top: boolean;
|
||||||
oi_top_limit?: number;
|
oi_top_limit?: number;
|
||||||
|
use_oi_low: boolean;
|
||||||
|
oi_low_limit?: number;
|
||||||
// Note: API URLs are now built automatically using nofxos_api_key from IndicatorConfig
|
// Note: API URLs are now built automatically using nofxos_api_key from IndicatorConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user