22 Commits

Author SHA1 Message Date
tinkle-community
5ea9a3990e docs: center README titles 2026-01-31 02:17:06 +08:00
tinkle-community
5b5199359c docs: rebrand as AI Trading OS across all languages 2026-01-31 02:15:07 +08:00
wqqqqqq
9dbc861cdf feat: add depth websocket from coinank (#1362) 2026-01-27 22:07:38 +08:00
tinkle-community
fcc0267a46 docs: update sponsors list (11 sponsors) 2026-01-23 21:23:38 +08:00
tinkle-community
c9150e8273 feat: add OI Low coin source and improve Mixed mode UI
- Add oi_low as independent source_type for short opportunities
- Redesign Mixed mode with card-based selector (2x2 grid)
- Show combination summary with total coin limit
- Support both Chinese and English languages
- Change default limits to 10 for OI Top and OI Low
2026-01-23 20:50:23 +08:00
tinkle-community
fcaabea6cb feat: add oi_low coin source for short opportunities
- Add GetOILowPositions/GetOILowSymbols in oi.go
- Add UseOILow/OILowLimit config fields
- Add oi_low case in GetCandidateCoins
- Support oi_low in mixed mode
- Update source tag formatting
2026-01-23 20:16:30 +08:00
tinkle-community
b5716ff3cb fix: handle empty AI500 coin list gracefully instead of error 2026-01-23 20:12:11 +08:00
tinkle-community
2f54d1d4c0 docs: update sponsors list (8 sponsors) 2026-01-19 23:23:14 +08:00
tinkle-community
0b448558ca docs: update sponsors list (5 sponsors) 2026-01-19 20:27:41 +08:00
tinkle-community
84276f64ae docs: add sponsor @1733055465 2026-01-19 19:11:55 +08:00
tinkle-community
5560df133e docs: use manual sponsor list instead of workflow 2026-01-19 19:06:54 +08:00
tinkle-community
f43c63699b docs: trigger sponsors update on new sponsorship events 2026-01-19 19:05:12 +08:00
tinkle-community
7b1edaa51f docs: add auto-update sponsors workflow 2026-01-19 19:04:33 +08:00
tinkle-community
ed8ad63288 docs: add sponsors section to README 2026-01-19 18:48:36 +08:00
tinkle-community
a7370efc2f fix(sync): use actual trade time instead of current time for lastSyncTime
- Remove syncStartTimeMs that was causing sync gaps
- Update binanceSyncState to latest trade's timestamp after successful sync
- Don't update lastSyncTime when no trades found (keep using DB value)

Fixes issue where trades between last sync and current time were missed
2026-01-19 17:33:13 +08:00
tinkle-community
5b384d126f fix(sync): add diagnostic logging for debugging sync issues
- Log lastSyncTimeMs and nowMs raw values for timestamp debugging
- Count and log skipped trades (already exist in DB)
- Helps diagnose positions sync stops at 6am issue
2026-01-19 16:25:02 +08:00
tinkle-community
1532b55d77 fix(sync): always query REALIZED_PNL to detect closed positions
Previously Method 4 (REALIZED_PNL) only ran when symbolMap was empty.
This caused fully-closed positions to be missed if other symbols were detected.

Now REALIZED_PNL is always queried to catch positions that:
- Have no active position (fully closed)
- Were missed by COMMISSION detection (VIP users, BNB fee discount)
2026-01-19 15:50:53 +08:00
tinkle-community
0e75b80d95 Revert "fix(sync): handle close trades without matching open position"
This reverts commit 9c57134dfb.
2026-01-19 15:35:17 +08:00
tinkle-community
9c57134dfb fix(sync): handle close trades without matching open position
- Create synthetic CLOSED position when close trade has no matching open position
- This happens when position was opened before sync window (>24h) but closed during sync
- Multiple close trades are merged into same synthetic position
- Added GetSyntheticClosedPosition and UpdateSyntheticPosition functions
- Synthetic positions marked with close_reason='sync_partial' for identification
2026-01-19 15:33:29 +08:00
tinkle-community
7ce7361cef fix(sync): add updated_at to position updates and auto-close when quantity=0
- UpdatePositionQuantityAndPrice: add updated_at timestamp
- ReducePositionQuantity: add updated_at and auto-close position when qty <= 0.0001
- UpdatePositionExchangeInfo: add updated_at timestamp

Fixes position sync issue after int64 timestamp migration where GORM autoUpdateTime
tag no longer works with int64 fields
2026-01-19 15:13:34 +08:00
tinkle-community
7a1643c56c fix: leverage validation bug and limit grid leverage to 1-5
- Fix Go range loop copy issue in validateDecisions (leverage auto-adjust was modifying copy, not original)
- Limit grid leverage from 1-20 to 1-5 for safer grid trading
2026-01-19 13:16:16 +08:00
tinkle-community
7e96c5d0f2 Ai grid (#1344)
* feat: add AI grid trading and market regime classification

- Add GridTrader interface with PlaceLimitOrder, CancelOrder, GetOrderBook
- Implement GridTrader for all exchanges (Binance, Bybit, OKX, Bitget, Hyperliquid, Aster, Lighter)
- Add grid engine with ATR-based boundary calculation and fund distribution
- Add market regime classification documents (Chinese/English)
- Add GridConfigEditor component for frontend configuration

* fix: implement GetOpenOrders for Lighter exchange

* debug: add logging for Lighter GetActiveOrders API call

* fix: correct Lighter API response parsing for GetOpenOrders

- Changed response field from 'data' to 'orders' to match Lighter API
- Updated OrderResponse struct to match Lighter's actual field names
- Fixed field types: price/quantity as strings, is_ask for side

* feat: implement GetOpenOrders for Aster, OKX, Bitget exchanges

- Aster: uses /fapi/v3/openOrders endpoint
- OKX: uses /api/v5/trade/orders-pending and orders-algo-pending
- Bitget: uses /api/v2/mix/order/orders-pending and orders-plan-pending

* fix: address code review issues for GetOpenOrders

- Add error logging for OKX/Bitget API failures (was silently swallowed)
- Fix Lighter position side logic to handle reduce-only orders
- Change verbose debug logs from Infof to Debugf level

* fix: provide FromAccountIndex and ApiKeyIndex for Lighter nonce auto-fetch

Root cause: SDK requires these fields to fetch nonce from API, otherwise nonce gets cached/stuck

* fix: use auth query parameter instead of Authorization header for Lighter API

* test: add Lighter API authentication tests and diagnostic tools

* fix(grid): add leverage setting before order placement

CRITICAL BUG FIX:
- Call SetLeverage() in GridTraderAdapter.PlaceLimitOrder()
- Set leverage during grid initialization
- Log leverage setting results

* fix(grid): prevent CancelOrder from canceling all orders

CRITICAL BUG FIX:
- CancelOrder no longer calls CancelAllOrders
- Try exchange-specific CancelOrder if available
- Return error if individual cancellation not supported

* fix(grid): add total position value limit check

CRITICAL: Prevent excessive position accumulation
- New checkTotalPositionLimit() function
- Checks current + pending + new order value
- Rejects orders that would exceed TotalInvestment x Leverage
- Logs clear error messages when limit exceeded

* feat(grid): implement stop loss execution

CRITICAL: Add code-level stop loss protection
- New checkAndExecuteStopLoss() function
- Checks each filled level against StopLossPct
- Automatically closes positions exceeding stop loss
- Called during every grid state sync

* feat(grid): add breakout detection and auto-pause

CRITICAL: Detect price breakout from grid range
- New checkBreakout() function to detect upper/lower breakouts
- Auto-pause grid on significant breakout (>2%)
- Cancel all orders when breakout detected
- Prevent continued losses in trending market
- Minor breakouts (1-2%) logged for AI consideration

* feat(grid): enforce max drawdown limit with emergency exit

CRITICAL: Add drawdown protection
- New checkMaxDrawdown() function tracks peak equity
- emergencyExit() closes all positions and cancels orders
- Auto-pause grid when MaxDrawdownPct exceeded
- Protect capital from excessive losses

* feat(grid): enforce daily loss limit

- Add checkDailyLossLimit() function to check if daily loss exceeds limit
- Track daily PnL with auto-reset at midnight
- Pause grid when DailyLossLimitPct exceeded
- Add updateDailyPnL() helper for realized PnL tracking
- Prevent excessive single-day losses

* fix(grid): update daily PnL when stop loss is executed

The updateDailyPnL() function was added but never called, leaving
DailyPnL always at 0 and preventing daily loss limit checks from
triggering.

This fix updates DailyPnL and TotalProfit directly in checkAndExecuteStopLoss()
when a stop loss is executed. We update directly rather than calling
updateDailyPnL() because the mutex is already held in that function.

* feat(grid): add automatic grid adjustment

- New checkGridSkew() detects imbalanced grid
- autoAdjustGrid() reinitializes around current price
- Prevents grid from becoming ineffective after drift
- Triggers when one side is 3x more filled than other

* fix(grid): recalculate bounds in autoAdjustGrid before reinitializing levels

Critical fix for grid auto-adjustment:
- Recalculate grid bounds (UpperPrice, LowerPrice, GridSpacing) centered
  on current price before reinitializing grid levels
- Preserve filled positions during adjustment by saving and restoring
  them to the closest new level after reinitialization
- Hold mutex lock for the entire adjustment operation to ensure atomicity
- Add locked variants of calculateDefaultBounds, calculateATRBounds, and
  initializeGridLevels to use during adjustment

Without this fix, autoAdjustGrid was using old boundaries when creating
new grid levels, defeating the purpose of auto-adjustment when price
moved significantly.

* fix(grid): improve order state sync logic

- Don't assume missing orders are filled
- Compare position size to determine fill vs cancel
- Properly reset cancelled orders to empty state
- More accurate grid state tracking

* fix(grid): use actual PositionSize sum instead of count in syncGridState heuristic

The position-based heuristic was using `float64(previousFilledCount) * level.OrderQuantity`
which incorrectly assumed uniform order quantities. Since the grid uses weighted distribution
(gaussian, pyramid, uniform) where orders have different quantities, this could lead to
incorrect fill detection.

Now sums the actual PositionSize from filled levels for accurate comparison.
Also adds warning log when GetPositions() fails.

* docs: add grid market regime detection design

Design for enhanced market state recognition with:
- Multi-dimensional indicators (ATR, Bollinger, EMA, MACD, RSI)
- Multi-period box indicators (72/240/500 1h candles)
- 4-level ranging classification
- Breakout detection and handling
- Frontend risk control panel

* docs: add grid market regime implementation plan

20 tasks covering:
- Donchian channel calculation
- Box data types and API
- Regime classification (4 levels)
- Breakout detection and handling
- False breakout recovery
- Frontend risk panel
- AI prompt updates

* feat(market): add Donchian channel calculation

Add calculateDonchian function to compute highest high and lowest low
over a specified period. This is the foundation for box (range) detection
in the multi-period box indicator system for grid trading.

* fix(market): handle invalid period in calculateDonchian

* feat(market): add BoxData and RegimeLevel types

* feat(market): add GetBoxData for multi-period box calculation

Adds calculateBoxData internal function and GetBoxData public API that
fetches 1h klines and computes three Donchian box levels (short/mid/long).
This will be used by the grid trading system to detect market regime.

* feat(store): add box and regime fields to grid models

* feat(trader): add regime classification and breakout detection

Implements Tasks 6-9 for grid market regime awareness:
- Task 6: classifyRegimeLevel with Bollinger/ATR thresholds
- Task 7: detectBoxBreakout for multi-period box breakouts
- Task 8: confirmBreakout with 3-candle confirmation logic
- Task 9: getBreakoutAction mapping breakout levels to actions

* feat(trader): integrate box breakout detection into grid cycle

- Task 10: Add checkBoxBreakout with 3-candle confirmation
- Task 11: Add checkFalseBreakoutRecovery for 50% position recovery
- Task 12: Add box/breakout/regime fields to GridState

* feat: add grid risk panel with API endpoint

- Task 13: Add GridRiskInfo type to frontend
- Task 14: Add /traders/:id/grid-risk API endpoint
- Task 15: Add GetGridRiskInfo method to AutoTrader
- Task 16: Create GridRiskPanel component with i18n

* feat(kernel): add box indicators to AI prompt

- Add BoxData field to GridContext
- Add box indicator table to both zh/en prompts
- Show breakout/warning alerts based on price position

* feat(web): integrate GridRiskPanel into TraderDashboardPage

* feat(lighter): improve API key validation and market caching

- Add API key validation status tracking
- Add market list caching to reduce API calls
- Improve logging (debug vs info levels)
- Add comprehensive integration tests
- Update trader manager and store for lighter support

* fix: remove hardcoded test wallet address

* fix(grid): improve GridRiskPanel layout and fix liquidation data

- Make panel collapsible with summary badges when collapsed
- Use compact 2-column grid layout for detailed info
- Fix auth token key (token -> auth_token)
- Only calculate liquidation distance when position exists

* fix(grid): add isRunning checks to prevent trades after Stop() is called
2026-01-19 12:07:14 +08:00
20 changed files with 842 additions and 127 deletions

View File

@@ -1,9 +1,21 @@
# NOFX - Agentic Trading OS <h1 align="center">NOFX — Open Source AI Trading OS</h1>
[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://golang.org/) <p align="center">
[![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react)](https://reactjs.org/) <strong>The infrastructure layer for AI-powered financial trading.</strong>
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) </p>
[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](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
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date) [![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date)

View File

@@ -1,18 +1,26 @@
# NOFX - AI トレーディングシステム <h1 align="center">NOFX — オープンソース AI トレーディング OS</h1>
[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://golang.org/) <p align="center">
[![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react)](https://reactjs.org/) <strong>AI 駆動金融取引のインフラストラクチャレイヤー</strong>
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) </p>
[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](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 を実行 - いつでもモデルを切り替え可能

View File

@@ -1,18 +1,26 @@
# NOFX - AI 트레이딩 시스템 <h1 align="center">NOFX — 오픈소스 AI 트레이딩 OS</h1>
[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://golang.org/) <p align="center">
[![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react)](https://reactjs.org/) <strong>AI 기반 금융 거래를 위한 인프라 레이어</strong>
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) </p>
[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](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 실행 - 언제든 모델 전환 가능

View File

@@ -1,18 +1,26 @@
# NOFX - AI Торговая Система <h1 align="center">NOFX — Open Source AI Торговая ОС</h1>
[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://golang.org/) <p align="center">
[![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react)](https://reactjs.org/) <strong>Инфраструктурный слой для AI-powered финансовой торговли</strong>
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) </p>
[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](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 — переключайтесь между моделями в любое время

View File

@@ -1,18 +1,26 @@
# NOFX - AI Торгова Система <h1 align="center">NOFX — Open Source AI Торгова ОС</h1>
[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://golang.org/) <p align="center">
[![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react)](https://reactjs.org/) <strong>Інфраструктурний рівень для AI-powered фінансової торгівлі</strong>
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) </p>
[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](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 — перемикайтеся між моделями будь-коли

View File

@@ -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>
[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://golang.org/) <p align="center">
[![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react)](https://reactjs.org/) <strong>Lớp cơ sở hạ tầng cho giao dịch tài chính AI-powered</strong>
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) </p>
[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](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

View File

@@ -1,9 +1,21 @@
# NOFX - AI 交易系统 <h1 align="center">NOFX — 开源 AI 交易操作系统</h1>
[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://golang.org/) <p align="center">
[![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react)](https://reactjs.org/) <strong>AI 驱动金融交易的基础设施层</strong>
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) </p>
[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](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 - 随时切换模型

View File

@@ -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)
} }
} }

View 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"`
}

View 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")
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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
} }

View File

@@ -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{

View File

@@ -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)

View File

@@ -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
} }

View File

@@ -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>
) )
} }

View File

@@ -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}
/> />

View File

@@ -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
} }