mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-22 03:37:36 +08:00
Compare commits
23 Commits
ea74f91de2
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39eac5aca7 | ||
|
|
0f3e71560c | ||
|
|
eabd279d10 | ||
|
|
09b7ac9e92 | ||
|
|
dc68884559 | ||
|
|
21407030ea | ||
|
|
7a66d048f3 | ||
|
|
8c8cd9b61f | ||
|
|
d84e22ab82 | ||
|
|
221cf3d61d | ||
|
|
390db72cdb | ||
|
|
c1b660bb45 | ||
|
|
c53a563bff | ||
|
|
e593874912 | ||
|
|
e9873b5b86 | ||
|
|
026c5d3089 | ||
|
|
ee5917adc6 | ||
|
|
9b70455e7f | ||
|
|
a744328313 | ||
|
|
b17d97effc | ||
|
|
e61f12d307 | ||
|
|
30f3feb586 | ||
|
|
49613830ce |
323
README.md
323
README.md
@@ -1,22 +1,14 @@
|
||||
<p align="center"><strong>Backed by <a href="https://vergex.trade">vergex.trade</a></strong></p>
|
||||
|
||||
<h1 align="center">NOFX</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>AI trading terminal for global markets.</strong><br/>
|
||||
<strong>Research, strategy generation, execution, and monitoring for US stocks, commodities, forex, and crypto.</strong>
|
||||
<img src="docs/assets/nofx-banner.svg" alt="NOFX — AI trading terminal" width="100%"/>
|
||||
</p>
|
||||
|
||||
<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://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-E0483B?style=flat-square&labelColor=1A1813" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/telegram-community-E0483B?style=flat-square&labelColor=1A1813&logo=telegram&logoColor=white" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -29,23 +21,29 @@
|
||||
<a href="docs/i18n/vi/README.md">Tiếng Việt</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
NOFX is an open-source AI trading terminal for active traders who want one workspace for market research, strategy development, execution, and portfolio monitoring.
|
||||
NOFX is an open-source trading terminal where the strategy is a language model. Each trader runs a continuous loop — read market structure, decide, execute, record the reasoning — while a Go runtime clamps every order to hard risk limits the model cannot override.
|
||||
|
||||
The product is built around global liquid markets: US equities, commodity contracts, FX pairs, and digital assets. The AI layer helps translate market intent into watchlists, signals, strategy logic, risk controls, and execution workflows.
|
||||
Traders compose freely: any model, any of nine exchanges, any strategy. Run several side by side and compare them on a public leaderboard by realized return. Everything runs on your own machine; exchange credentials are encrypted at rest and never leave it.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
Open **http://127.0.0.1:3000**.
|
||||
The terminal opens at `http://127.0.0.1:3000`.
|
||||
|
||||
---
|
||||
**First run**
|
||||
|
||||
1. Register — the first account becomes the owner of the instance.
|
||||
2. Follow the guided launch: put **$1+ USDC** (Base network) in the AI fee wallet it creates for you, then connect Hyperliquid and deposit **$12+ USDC** to trade with.
|
||||
3. Start **Autopilot**. The AI scans the market every few minutes and trades on its own; every decision appears on the dashboard as it happens. Stop it anytime with one click.
|
||||
|
||||
<br/>
|
||||
|
||||
## Register exchanges
|
||||
|
||||
Use the links below to open trading accounts for crypto and supported US stock, FX, and commodity derivative markets. These routes are part of NOFX partner programs and may include fee discounts or referral benefits.
|
||||
NOFX is free and open source. Opening an account through the partner links below carries reduced trading fees and funds continued development.
|
||||
|
||||
| Exchange | Status | Register with fee discount |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------- | :----: | :---------------------------------------------------------------------------------- |
|
||||
@@ -59,59 +57,47 @@ Use the links below to open trading accounts for crypto and supported US stock,
|
||||
| <img src="web/public/exchange-icons/aster.svg" width="20" height="20" style="vertical-align: middle;"/> **Aster** | ✅ | [Register](https://www.asterdex.com/en/referral/fdfc0e) |
|
||||
| <img src="web/public/exchange-icons/lighter.png" width="20" height="20" style="vertical-align: middle;"/> **Lighter** | ✅ | [Register](https://app.lighter.xyz/?referral=68151432) |
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Quick demo
|
||||
## Demo
|
||||
|
||||
https://github.com/user-attachments/assets/3310f495-14c5-4586-a1cc-3d32e44aa505
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Markets
|
||||
## The model proposes. The runtime disposes.
|
||||
|
||||
**US Stocks · Commodities · Forex · Crypto**
|
||||
Decisions come from a language model reading the [Claw402.ai](https://claw402.ai) · Vergex data stack: a live signal board that ranks every market with directional bias and signal strength, per-symbol Signal Lab deep signals, cost-basis and liquidation heatmaps that show where the crowd's fuel and walls sit, and real-time market net flow — cross-checked against raw candles and the trader's own live track record. Execution does not.
|
||||
|
||||
NOFX organizes research, strategy construction, execution, and monitoring around multi-asset workflows instead of single-venue screens.
|
||||
Every order passes through limits enforced in code, outside the model's reach:
|
||||
|
||||
---
|
||||
| | |
|
||||
| :----------------------- | :--------------------------------------------------------------------------------- |
|
||||
| Position limits | Max concurrent positions, notional capped as a ratio of equity, one position per symbol |
|
||||
| Leverage clamps | Hard caps applied at order-sizing time, independent of what the model requests |
|
||||
| Exchange-side protection | Stop-loss and take-profit placed on the exchange immediately after every entry |
|
||||
| Drawdown auto-close | Profitable positions that give back too much from their peak are closed |
|
||||
| Trade throttling | Minimum hold times, per-symbol re-entry cooldowns, per-cycle and per-hour entry limits |
|
||||
| Safe mode | Repeated model failures block new entries until the model recovers |
|
||||
| Launch preflight | Model access, wallet funds, strategy, and exchange balances verified before a trader may start |
|
||||
|
||||
## AI model access
|
||||
Each decision is stored with the model's full reasoning. There is no position without a paper trail.
|
||||
|
||||
NOFX routes AI inference through [Claw402](https://claw402.ai) automatically. Users do not need to configure model providers, manage API keys, or maintain separate AI accounts. The terminal accesses supported models on demand through Claw402's pay-as-you-go infrastructure, with traffic routed through the official discounted channel.
|
||||
<br/>
|
||||
|
||||
| Provider | Access |
|
||||
| :------- | :----- |
|
||||
| **Claw402** | [Access pay-as-you-go AI models with official discount](https://claw402.ai) |
|
||||
## Terminal
|
||||
|
||||
---
|
||||
|
||||
## Capabilities
|
||||
|
||||
| Capability | Description |
|
||||
| :-------------------------- | :-------------------------------------------------------------------------- |
|
||||
| **AI trading terminal** | Unified workspace for US stocks, commodities, forex, and crypto workflows |
|
||||
| **AI model access** | Unified model access through Claw402-supported providers |
|
||||
| **Exchange connectivity** | Binance, Bybit, OKX, Hyperliquid, Bitget, KuCoin, Gate, Aster, and Lighter |
|
||||
| **Strategy Studio** | Market universes, indicators, risk controls, and strategy logic |
|
||||
| **Model competition** | Compare model-driven traders with live performance and leaderboard tracking |
|
||||
| **Telegram agent** | Control and monitor the trading assistant through chat |
|
||||
| **Portfolio dashboard** | Positions, P/L, execution history, and model decision logs |
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| **Autopilot** | Guided launch: fund, connect, deposit, start — with server-side preflight throughout |
|
||||
| **Strategy Studio** | Style presets, coin universes, indicators, leverage, entry confidence, custom prompts |
|
||||
| **Competition** | Public leaderboard ranked by realized return, each entry attributed to its model |
|
||||
| **Dashboard** | Live positions, orders, statistics, and the reasoning behind every decision |
|
||||
|
||||
<details>
|
||||
<summary><b>Config Page</b></summary>
|
||||
<summary>Screenshots</summary>
|
||||
|
||||
| Configuration | Traders List |
|
||||
| :----------------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="screenshots/config-ai-exchanges.png" width="400"/> | <img src="screenshots/config-traders-list.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Dashboard</b></summary>
|
||||
<br/>
|
||||
|
||||
| Overview | Market Chart |
|
||||
| :-----------------------------------------------------: | :-------------------------------------------------------------: |
|
||||
@@ -121,131 +107,51 @@ NOFX routes AI inference through [Claw402](https://claw402.ai) automatically. Us
|
||||
| :--------------------------------------------------------------: | :-----------------------------------------------------------------: |
|
||||
| <img src="screenshots/dashboard-trading-stats.png" width="400"/> | <img src="screenshots/dashboard-position-history.png" width="400"/> |
|
||||
|
||||
| Positions | Trader Details |
|
||||
| :----------------------------------------------------------: | :---------------------------------------------------: |
|
||||
| <img src="screenshots/dashboard-positions.png" width="400"/> | <img src="screenshots/details-page.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Strategy Studio</b></summary>
|
||||
|
||||
| Strategy Editor | Indicators Config |
|
||||
| :------------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="screenshots/strategy-studio.png" width="400"/> | <img src="screenshots/strategy-indicators.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Competition</b></summary>
|
||||
|
||||
| Competition Mode |
|
||||
| :-------------------------------------------------------: |
|
||||
| <img src="screenshots/competition-page.png" width="400"/> |
|
||||
| Competition | Configuration |
|
||||
| :-------------------------------------------------------: | :-----------------------------------------------------------: |
|
||||
| <img src="screenshots/competition-page.png" width="400"/> | <img src="screenshots/config-ai-exchanges.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Install
|
||||
## Models
|
||||
|
||||
### Linux / macOS
|
||||
Eight providers with your own keys — DeepSeek, OpenAI, Claude, Qwen, Gemini, Grok, Kimi, MiniMax — including custom endpoints and model names.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
Or no keys at all: [Claw402](https://claw402.ai) meters model usage per call in USDC over the x402 protocol. A wallet on Base replaces every API key.
|
||||
|
||||
### Railway (Cloud)
|
||||
| Provider | Access |
|
||||
| :------- | :----- |
|
||||
| **Claw402** | [Pay-as-you-go AI models with official discount](https://claw402.ai) |
|
||||
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
## Markets
|
||||
|
||||
### Docker
|
||||
Crypto perpetuals on all nine exchanges. On Hyperliquid, the same runtime also trades tokenized US equities, commodities, indices, FX, and pre-IPO perps — TSLA, NVDA, GOLD, SPX, EUR, OPENAI — alongside crypto.
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Install [Docker Desktop](https://www.docker.com/products/docker-desktop/), then:
|
||||
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
# Prerequisites: Go 1.21+, Node.js 18+, TA-Lib
|
||||
# macOS: brew install ta-lib
|
||||
# Ubuntu: sudo apt-get install libta-lib0-dev
|
||||
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx # backend
|
||||
cd web && npm install && npm run dev # frontend (new terminal)
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
**Beginner mode**: Guided onboarding walks new users through model selection, exchange connection, strategy setup, and first deployment.
|
||||
|
||||
**Advanced mode**:
|
||||
|
||||
1. Configure AI model access
|
||||
2. Connect exchange credentials
|
||||
3. Build or import a strategy
|
||||
4. Create an AI trader profile
|
||||
5. Launch, monitor, and iterate from the dashboard
|
||||
|
||||
All configuration is available from the web UI at **http://127.0.0.1:3000**.
|
||||
|
||||
---
|
||||
|
||||
## Deploy to server
|
||||
|
||||
**HTTP deployment:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# Access via http://YOUR_IP:3000
|
||||
```
|
||||
|
||||
**HTTPS via Cloudflare:**
|
||||
|
||||
1. Add domain to [Cloudflare](https://dash.cloudflare.com) (free plan)
|
||||
2. A record → your server IP (Proxied)
|
||||
3. SSL/TLS → Flexible
|
||||
4. Set `TRANSPORT_ENCRYPTION=true` in `.env`
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
NOFX
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Trading Terminal │
|
||||
│ React + TypeScript + TradingView │
|
||||
│ US Stocks · Commodities · Forex · Crypto │
|
||||
│ React · TypeScript · TradingView │
|
||||
│ Dashboard · Strategy Studio · Competition │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ API Server (Go) │
|
||||
│ JWT auth · encrypted credential store │
|
||||
├──────────────┬──────────────┬───────────────────┤
|
||||
│ Strategy │ Telegram │ Trader Runtime │
|
||||
│ Engine │ Agent │ Risk Controls │
|
||||
│ Strategy │ Autopilot │ Trader Runtime │
|
||||
│ Engine │ Preflight │ Risk Engine │
|
||||
├──────────────┴──────────────┴───────────────────┤
|
||||
│ AI Model Layer │
|
||||
│ Unified provider access through Claw402 │
|
||||
│ Model routing · payment · execution support │
|
||||
│ DeepSeek · OpenAI · Claude · Qwen · Gemini │
|
||||
│ Grok · Kimi · MiniMax · Claw402 (x402 USDC) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Exchange Connectivity │
|
||||
│ Binance · Bybit · OKX · Hyperliquid · Bitget │
|
||||
@@ -253,26 +159,86 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Docs
|
||||
## Install
|
||||
|
||||
**Linux / macOS**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
**Railway**
|
||||
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
|
||||
**Docker**
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**Windows** — install [Docker Desktop](https://www.docker.com/products/docker-desktop/), then:
|
||||
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**From source** — Go 1.21+, Node.js 18+:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx # backend
|
||||
cd web && npm install && npm run dev # frontend, in a second terminal
|
||||
```
|
||||
|
||||
**Update** — re-run the install script; it upgrades in place.
|
||||
|
||||
<details>
|
||||
<summary>Server deployment</summary>
|
||||
|
||||
<br/>
|
||||
|
||||
**HTTP**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# http://YOUR_IP:3000
|
||||
```
|
||||
|
||||
**HTTPS via Cloudflare**
|
||||
|
||||
1. Add the domain to [Cloudflare](https://dash.cloudflare.com) (free plan)
|
||||
2. A record → server IP, proxied
|
||||
3. SSL/TLS → Flexible
|
||||
4. `TRANSPORT_ENCRYPTION=true` in `.env`
|
||||
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
|
||||
## Documentation
|
||||
|
||||
| | |
|
||||
| :------------------------------------------------------ | :------------------------------------ |
|
||||
| [Getting Started](docs/getting-started/README.md) | Deployment and exchange API guides |
|
||||
| [Architecture](docs/architecture/README.md) | System design and module index |
|
||||
| [Strategy Module](docs/architecture/STRATEGY_MODULE.md) | Coin selection, AI prompts, execution |
|
||||
| [FAQ](docs/faq/README.md) | Common questions |
|
||||
| [Getting Started](docs/getting-started/README.md) | Deployment guide |
|
||||
| [FAQ](docs/guides/faq.en.md) | Common questions |
|
||||
| [Troubleshooting](docs/guides/TROUBLESHOOTING.md) | Diagnosing common issues |
|
||||
|
||||
---
|
||||
## Community
|
||||
|
||||
[Telegram](https://t.me/nofx_dev_community) · [Twitter/X](https://x.com/vergex_ai) · [Issues](https://github.com/NoFxAiOS/nofx/issues) · [vergex.trade](https://vergex.trade) · [Live dashboard](https://vergex.trade/explore)
|
||||
|
||||
## Contributing
|
||||
|
||||
See [Contributing Guide](CONTRIBUTING.md), [Code of Conduct](CODE_OF_CONDUCT.md), and [Security Policy](SECURITY.md).
|
||||
Code, documentation, translations, and bug reports are all welcome — see the [Contributing Guide](CONTRIBUTING.md), [Code of Conduct](CODE_OF_CONDUCT.md), and [Security Policy](SECURITY.md).
|
||||
|
||||
### Contributor Airdrop Program
|
||||
|
||||
NOFX tracks meaningful contributions and intends to reward contributors as the ecosystem grows. Priority issues carry higher reward weight.
|
||||
NOFX tracks meaningful contributions and intends to reward contributors as the ecosystem grows. Priority issues carry higher weight.
|
||||
|
||||
| Contribution | Weight |
|
||||
| :---------------- | :----: |
|
||||
@@ -283,20 +249,9 @@ NOFX tracks meaningful contributions and intends to reward contributors as the e
|
||||
| Bug Reports | ★★ |
|
||||
| Documentation | ★★ |
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
| | |
|
||||
| :-------- | :---------------------------------------------------- |
|
||||
| Website | [vergex.trade](https://vergex.trade) |
|
||||
| Dashboard | [vergex.trade/explore](https://vergex.trade/explore) |
|
||||
| Telegram | [nofx_dev_community](https://t.me/nofx_dev_community) |
|
||||
| Twitter | [@vergex_ai](https://x.com/vergex_ai) |
|
||||
|
||||
> **Risk warning**: Automated trading involves substantial risk. Use appropriate position sizing, understand each exchange venue, and do not trade funds you cannot afford to lose.
|
||||
|
||||
---
|
||||
<a href="https://github.com/NoFxAiOS/nofx/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=NoFxAiOS/nofx" alt="Contributors"/>
|
||||
</a>
|
||||
|
||||
## Sponsors
|
||||
|
||||
@@ -314,8 +269,14 @@ NOFX tracks meaningful contributions and intends to reward contributors as the e
|
||||
|
||||
[Become a sponsor](https://github.com/sponsors/NoFxAiOS)
|
||||
|
||||
<br/>
|
||||
|
||||
If NOFX is useful to you, a star helps other traders find it.
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0](LICENSE)
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
<sub>Automated trading involves substantial risk. AI-driven strategies are experimental and can lose money. Size positions appropriately, understand each venue, and never trade funds you cannot afford to lose. Full [disclaimer](DISCLAIMER.md).</sub>
|
||||
|
||||
@@ -26,6 +26,7 @@ type beginnerOnboardingResponse struct {
|
||||
DefaultModel string `json:"default_model"`
|
||||
ConfiguredModelID string `json:"configured_model_id"`
|
||||
BalanceUSDC string `json:"balance_usdc"`
|
||||
BalanceStatus string `json:"balance_status,omitempty"`
|
||||
EnvSaved bool `json:"env_saved"`
|
||||
EnvPath string `json:"env_path,omitempty"`
|
||||
ReusedExisting bool `json:"reused_existing"`
|
||||
@@ -36,10 +37,22 @@ type currentBeginnerWalletResponse struct {
|
||||
Found bool `json:"found"`
|
||||
Address string `json:"address,omitempty"`
|
||||
BalanceUSDC string `json:"balance_usdc,omitempty"`
|
||||
BalanceStatus string `json:"balance_status,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Claw402Status string `json:"claw402_status"`
|
||||
}
|
||||
|
||||
// queryBeginnerWalletBalance returns the wallet balance plus a status flag so
|
||||
// the UI can distinguish "RPC unreachable" from a genuinely empty wallet.
|
||||
// Uses the 30s cache — safe for UI polling.
|
||||
func queryBeginnerWalletBalance(address string) (balanceUSDC string, balanceStatus string) {
|
||||
balance, err := wallet.QueryUSDCBalanceCached(address)
|
||||
if err != nil {
|
||||
return "", "unknown"
|
||||
}
|
||||
return fmt.Sprintf("%.2f", balance), "ok"
|
||||
}
|
||||
|
||||
func (s *Server) handleBeginnerOnboarding(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
@@ -72,6 +85,7 @@ func (s *Server) handleBeginnerOnboarding(c *gin.Context) {
|
||||
os.Setenv("CLAW402_DEFAULT_MODEL", payment.DefaultClaw402Model)
|
||||
|
||||
envSaved, envPath, envErr := persistBeginnerWalletEnv(privateKey, address)
|
||||
balanceUSDC, balanceStatus := queryBeginnerWalletBalance(address)
|
||||
resp := beginnerOnboardingResponse{
|
||||
Address: address,
|
||||
PrivateKey: privateKey,
|
||||
@@ -80,7 +94,8 @@ func (s *Server) handleBeginnerOnboarding(c *gin.Context) {
|
||||
Provider: "claw402",
|
||||
DefaultModel: payment.DefaultClaw402Model,
|
||||
ConfiguredModelID: configuredModelID,
|
||||
BalanceUSDC: wallet.QueryUSDCBalanceStr(address),
|
||||
BalanceUSDC: balanceUSDC,
|
||||
BalanceStatus: balanceStatus,
|
||||
EnvSaved: envSaved,
|
||||
EnvPath: envPath,
|
||||
ReusedExisting: reusedExisting,
|
||||
@@ -124,10 +139,12 @@ func (s *Server) handleCurrentBeginnerWallet(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
balanceUSDC, balanceStatus := queryBeginnerWalletBalance(address)
|
||||
c.JSON(http.StatusOK, currentBeginnerWalletResponse{
|
||||
Found: true,
|
||||
Address: address,
|
||||
BalanceUSDC: wallet.QueryUSDCBalanceStr(address),
|
||||
BalanceUSDC: balanceUSDC,
|
||||
BalanceStatus: balanceStatus,
|
||||
Source: "model",
|
||||
Claw402Status: claw402Status,
|
||||
})
|
||||
@@ -136,10 +153,12 @@ func (s *Server) handleCurrentBeginnerWallet(c *gin.Context) {
|
||||
|
||||
address := strings.TrimSpace(os.Getenv("CLAW402_WALLET_ADDRESS"))
|
||||
if address != "" {
|
||||
balanceUSDC, balanceStatus := queryBeginnerWalletBalance(address)
|
||||
c.JSON(http.StatusOK, currentBeginnerWalletResponse{
|
||||
Found: true,
|
||||
Address: address,
|
||||
BalanceUSDC: wallet.QueryUSDCBalanceStr(address),
|
||||
BalanceUSDC: balanceUSDC,
|
||||
BalanceStatus: balanceStatus,
|
||||
Source: "env",
|
||||
Claw402Status: claw402Status,
|
||||
})
|
||||
|
||||
@@ -237,7 +237,7 @@ func (s *Server) handlePositionHistory(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Get statistics
|
||||
stats, _ := store.Position().GetFullStatsByTraderFilters(traderIDs, traderIDPatterns)
|
||||
stats, _ := store.Position().GetFullStatsByTraderFilters(traderIDs, traderIDPatterns, trader.GetInitialBalance())
|
||||
|
||||
// Get symbol stats
|
||||
symbolStats, _ := store.Position().GetSymbolStatsByTraderFilters(traderIDs, traderIDPatterns, 10)
|
||||
|
||||
@@ -48,7 +48,7 @@ func (s *Server) handleStatisticsFull(c *gin.Context) {
|
||||
traderIDPatterns = append(traderIDPatterns, "%_"+userID+"_claw402_%")
|
||||
}
|
||||
|
||||
stats, err := store.Position().GetFullStatsByTraderFilters(traderIDs, traderIDPatterns)
|
||||
stats, err := store.Position().GetFullStatsByTraderFilters(traderIDs, traderIDPatterns, trader.GetInitialBalance())
|
||||
if err != nil {
|
||||
SafeInternalError(c, "Get full statistics", err)
|
||||
return
|
||||
|
||||
@@ -731,7 +731,10 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
|
||||
"trader_id": traderID,
|
||||
"trader_name": req.Name,
|
||||
"ai_model": req.AIModelID,
|
||||
"message": "Trader updated successfully",
|
||||
// A running trader is restarted with the new config (async above), so
|
||||
// report it as running — callers must not fire a redundant start.
|
||||
"is_running": wasRunning,
|
||||
"message": "Trader updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -792,7 +795,10 @@ func (s *Server) handleStartTrader(c *gin.Context) {
|
||||
if existingTrader != nil {
|
||||
status := existingTrader.GetStatus()
|
||||
if isRunning, ok := status["is_running"].(bool); ok && isRunning {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Trader is already running"})
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Trader is already running",
|
||||
"error_key": "trader.start.already_running",
|
||||
})
|
||||
return
|
||||
}
|
||||
// Trader exists but is stopped - remove from memory to reload fresh config
|
||||
@@ -850,6 +856,24 @@ func (s *Server) handleStartTrader(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Server-side launch gate: the trader cannot function without a funded AI
|
||||
// wallet and a ready exchange account, so verify both before the run loop
|
||||
// starts. `?force=true` skips the gate for deliberate manual overrides.
|
||||
if c.Query("force") != "true" {
|
||||
// strategyRequired=false: a trader that loaded into memory necessarily
|
||||
// has a valid strategy (the manager refuses to load without one), so the
|
||||
// preflight strategy check would be redundant here.
|
||||
preflight := s.runLaunchPreflight(userID, fullCfg.AIModel, fullCfg.Exchange, fullCfg.Strategy, false)
|
||||
if !preflight.Ready {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": formatTraderStartError(preflight.Summary(), "Complete the failing checks, then start the bot again"),
|
||||
"error_key": "trader.start.preflight_failed",
|
||||
"preflight": preflight,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Start trader
|
||||
go func() {
|
||||
logger.Infof("▶️ Starting trader %s (%s)", traderID, trader.GetName())
|
||||
|
||||
@@ -263,10 +263,13 @@ func (s *Server) createDefaultStrategies(userID string, lang string) error {
|
||||
c.CoinSource.VergexMarketType = "all"
|
||||
c.CoinSource.VergexChain = "hyperliquid"
|
||||
c.RiskControl.MaxPositions = 2
|
||||
c.RiskControl.BTCETHMaxLeverage = 10
|
||||
c.RiskControl.AltcoinMaxLeverage = 10
|
||||
c.RiskControl.BTCETHMaxPositionValueRatio = 10.0
|
||||
c.RiskControl.AltcoinMaxPositionValueRatio = 10.0
|
||||
c.RiskControl.BTCETHMaxLeverage = 5
|
||||
c.RiskControl.AltcoinMaxLeverage = 5
|
||||
// Few, concentrated positions held for big moves. 5x leverage so a
|
||||
// wide (-5%) stop is survivable rather than an instant liquidation;
|
||||
// 2 positions × 2.5x = 5x total notional (full margin, ~20% cushion).
|
||||
c.RiskControl.BTCETHMaxPositionValueRatio = 2.5
|
||||
c.RiskControl.AltcoinMaxPositionValueRatio = 2.5
|
||||
c.RiskControl.MaxMarginUsage = 1.0
|
||||
c.RiskControl.MinConfidence = 78
|
||||
c.RiskControl.MinRiskRewardRatio = 3.0
|
||||
|
||||
@@ -54,16 +54,16 @@ func TestCreateDefaultStrategiesUsesOneReadyToRunClaw402Preset(t *testing.T) {
|
||||
if trendCfg.CoinSource.SourceType != "vergex_signal" || trendCfg.CoinSource.VergexLimit != 10 || trendCfg.CoinSource.VergexMarketType != "all" {
|
||||
t.Fatalf("default strategy should use the Claw402/Vergex all-market signal ranking, got %+v", trendCfg.CoinSource)
|
||||
}
|
||||
if trendCfg.CoinSource.UseAI500 || trendCfg.RiskControl.MaxPositions > 2 {
|
||||
t.Fatalf("default strategy should be Claw402/Vergex native with at most two positions, got coin=%+v risk=%+v", trendCfg.CoinSource, trendCfg.RiskControl)
|
||||
if trendCfg.CoinSource.UseAI500 || trendCfg.RiskControl.MaxPositions != 2 {
|
||||
t.Fatalf("default strategy should be Claw402/Vergex native with a 2-position concentrated book, got coin=%+v risk=%+v", trendCfg.CoinSource, trendCfg.RiskControl)
|
||||
}
|
||||
if trendCfg.RiskControl.BTCETHMaxLeverage != 10 || trendCfg.RiskControl.AltcoinMaxLeverage != 10 {
|
||||
t.Fatalf("default strategy should use 10x leverage for all Claw402 opens, got risk=%+v", trendCfg.RiskControl)
|
||||
if trendCfg.RiskControl.BTCETHMaxLeverage != 5 || trendCfg.RiskControl.AltcoinMaxLeverage != 5 {
|
||||
t.Fatalf("default strategy should use 5x leverage for all Claw402 opens, got risk=%+v", trendCfg.RiskControl)
|
||||
}
|
||||
if trendCfg.RiskControl.BTCETHMaxPositionValueRatio != 10 ||
|
||||
trendCfg.RiskControl.AltcoinMaxPositionValueRatio != 10 ||
|
||||
if trendCfg.RiskControl.BTCETHMaxPositionValueRatio != 2.5 ||
|
||||
trendCfg.RiskControl.AltcoinMaxPositionValueRatio != 2.5 ||
|
||||
trendCfg.RiskControl.MaxMarginUsage != 1.0 {
|
||||
t.Fatalf("default strategy should use full-size 10x notional for Claw402 opens, got risk=%+v", trendCfg.RiskControl)
|
||||
t.Fatalf("default strategy should size Claw402 opens at 2.5x equity notional (2 positions = 5x total at 5x), got risk=%+v", trendCfg.RiskControl)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
375
api/launch_preflight.go
Normal file
375
api/launch_preflight.go
Normal file
@@ -0,0 +1,375 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"nofx/store"
|
||||
"nofx/wallet"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Launch readiness minimums. These are the single source of truth — the
|
||||
// frontend reads them from the preflight response instead of hardcoding.
|
||||
const (
|
||||
// MinAIFeeUSDC is the minimum Base USDC a claw402 fee wallet needs so the
|
||||
// trader can pay for its first AI/data calls.
|
||||
MinAIFeeUSDC = 1.0
|
||||
// MinTradingUSDC is the minimum available balance the exchange account
|
||||
// needs before the trader can place its first order.
|
||||
MinTradingUSDC = 12.0
|
||||
)
|
||||
|
||||
const (
|
||||
launchCheckStatusOK = "ok"
|
||||
launchCheckStatusFailed = "failed"
|
||||
launchCheckStatusWarning = "warning"
|
||||
launchCheckStatusSkipped = "skipped"
|
||||
)
|
||||
|
||||
// Check IDs — stable identifiers the frontend maps to guided-setup steps.
|
||||
const (
|
||||
launchCheckAIModel = "ai_model"
|
||||
launchCheckAIWallet = "ai_wallet"
|
||||
launchCheckAIWalletFunds = "ai_wallet_funds"
|
||||
launchCheckStrategy = "strategy"
|
||||
launchCheckExchangeConfig = "exchange_config"
|
||||
launchCheckExchangeAccount = "exchange_account"
|
||||
launchCheckExchangeFunds = "exchange_funds"
|
||||
)
|
||||
|
||||
// queryAIWalletBalance is swappable in tests to avoid live Base RPC calls.
|
||||
var queryAIWalletBalance = wallet.QueryUSDCBalanceCached
|
||||
|
||||
type LaunchCheck struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
// Numeric context so the UI can render progress like "8.40 / 12 USDC".
|
||||
Required float64 `json:"required,omitempty"`
|
||||
Actual *float64 `json:"actual,omitempty"`
|
||||
Asset string `json:"asset,omitempty"`
|
||||
// Address is the funding address for balance checks, so the UI can offer
|
||||
// a deposit shortcut next to the failing item.
|
||||
Address string `json:"address,omitempty"`
|
||||
}
|
||||
|
||||
type LaunchPreflightResult struct {
|
||||
Ready bool `json:"ready"`
|
||||
Checks []LaunchCheck `json:"checks"`
|
||||
MinAIFeeUSDC float64 `json:"min_ai_fee_usdc"`
|
||||
MinTradingUSDC float64 `json:"min_trading_usdc"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
}
|
||||
|
||||
func (r LaunchPreflightResult) failedChecks() []LaunchCheck {
|
||||
var failed []LaunchCheck
|
||||
for _, check := range r.Checks {
|
||||
if check.Status == launchCheckStatusFailed {
|
||||
failed = append(failed, check)
|
||||
}
|
||||
}
|
||||
return failed
|
||||
}
|
||||
|
||||
// Summary joins the failing messages into one human-readable sentence.
|
||||
func (r LaunchPreflightResult) Summary() string {
|
||||
failed := r.failedChecks()
|
||||
if len(failed) == 0 {
|
||||
return ""
|
||||
}
|
||||
messages := make([]string, 0, len(failed))
|
||||
for _, check := range failed {
|
||||
if check.Message != "" {
|
||||
messages = append(messages, check.Message)
|
||||
}
|
||||
}
|
||||
return strings.Join(messages, " ")
|
||||
}
|
||||
|
||||
type launchPreflightRequest struct {
|
||||
AIModelID string `json:"ai_model_id"`
|
||||
ExchangeID string `json:"exchange_id"`
|
||||
StrategyID string `json:"strategy_id"`
|
||||
}
|
||||
|
||||
// handleLaunchPreflight runs launch readiness checks for a model/exchange
|
||||
// pair before any trader is created or mutated. POST /api/launch/preflight
|
||||
func (s *Server) handleLaunchPreflight(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
|
||||
var req launchPreflightRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
SafeBadRequest(c, "Invalid preflight request")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.AIModelID) == "" || strings.TrimSpace(req.ExchangeID) == "" {
|
||||
SafeBadRequest(c, "ai_model_id and exchange_id are required")
|
||||
return
|
||||
}
|
||||
|
||||
model, err := s.store.AIModel().Get(userID, req.AIModelID)
|
||||
if err != nil {
|
||||
model = nil
|
||||
}
|
||||
exchange, err := s.store.Exchange().GetByID(userID, req.ExchangeID)
|
||||
if err != nil {
|
||||
exchange = nil
|
||||
}
|
||||
|
||||
var strategy *store.Strategy
|
||||
strategyRequired := strings.TrimSpace(req.StrategyID) != ""
|
||||
if strategyRequired {
|
||||
strategy, err = s.store.Strategy().Get(userID, req.StrategyID)
|
||||
if err != nil {
|
||||
strategy = nil
|
||||
}
|
||||
}
|
||||
|
||||
result := s.runLaunchPreflight(userID, model, exchange, strategy, strategyRequired)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// handleTraderPreflight runs the same checks against an existing trader's
|
||||
// full configuration. GET /api/traders/:id/preflight
|
||||
func (s *Server) handleTraderPreflight(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
traderID := c.Param("id")
|
||||
|
||||
fullCfg, err := s.store.Trader().GetFullConfig(userID, traderID)
|
||||
if err != nil || fullCfg == nil || fullCfg.Trader == nil {
|
||||
SafeNotFound(c, "Trader")
|
||||
return
|
||||
}
|
||||
|
||||
result := s.runLaunchPreflight(userID, fullCfg.AIModel, fullCfg.Exchange, fullCfg.Strategy, true)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// runLaunchPreflight composes every launch readiness check. A check that
|
||||
// cannot be evaluated (RPC outage, probe timeout) reports "warning" instead
|
||||
// of "failed" so an infrastructure hiccup never hard-blocks a launch.
|
||||
func (s *Server) runLaunchPreflight(
|
||||
userID string,
|
||||
model *store.AIModel,
|
||||
exchange *store.Exchange,
|
||||
strategy *store.Strategy,
|
||||
strategyRequired bool,
|
||||
) LaunchPreflightResult {
|
||||
checks := []LaunchCheck{checkLaunchAIModel(model)}
|
||||
checks = append(checks, checkLaunchAIWallet(model)...)
|
||||
checks = append(checks, checkLaunchStrategy(strategy, strategyRequired))
|
||||
checks = append(checks, s.checkLaunchExchange(userID, exchange)...)
|
||||
|
||||
ready := true
|
||||
for _, check := range checks {
|
||||
if check.Status == launchCheckStatusFailed {
|
||||
ready = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return LaunchPreflightResult{
|
||||
Ready: ready,
|
||||
Checks: checks,
|
||||
MinAIFeeUSDC: MinAIFeeUSDC,
|
||||
MinTradingUSDC: MinTradingUSDC,
|
||||
CheckedAt: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
func checkLaunchAIModel(model *store.AIModel) LaunchCheck {
|
||||
check := LaunchCheck{ID: launchCheckAIModel}
|
||||
|
||||
switch {
|
||||
case model == nil:
|
||||
check.Status = launchCheckStatusFailed
|
||||
check.Code = "MODEL_NOT_FOUND"
|
||||
check.Message = "The selected AI model was not found. Configure an AI model first."
|
||||
case !model.Enabled:
|
||||
check.Status = launchCheckStatusFailed
|
||||
check.Code = "MODEL_DISABLED"
|
||||
check.Message = fmt.Sprintf("AI model \"%s\" is disabled. Enable it first.", model.Name)
|
||||
case strings.TrimSpace(model.APIKey.String()) == "":
|
||||
check.Status = launchCheckStatusFailed
|
||||
check.Code = "MODEL_MISSING_CREDENTIALS"
|
||||
check.Message = fmt.Sprintf("AI model \"%s\" has no credential saved. Add the API key or wallet key first.", model.Name)
|
||||
default:
|
||||
check.Status = launchCheckStatusOK
|
||||
check.Message = model.Name
|
||||
}
|
||||
|
||||
return check
|
||||
}
|
||||
|
||||
// checkLaunchAIWallet validates the claw402 fee wallet (address + Base USDC
|
||||
// balance). Non-claw402 providers pay per API key, so both checks are skipped.
|
||||
func checkLaunchAIWallet(model *store.AIModel) []LaunchCheck {
|
||||
walletCheck := LaunchCheck{ID: launchCheckAIWallet}
|
||||
fundsCheck := LaunchCheck{ID: launchCheckAIWalletFunds, Asset: "USDC", Required: MinAIFeeUSDC}
|
||||
|
||||
if model == nil || model.Provider != "claw402" || strings.TrimSpace(model.APIKey.String()) == "" {
|
||||
walletCheck.Status = launchCheckStatusSkipped
|
||||
fundsCheck.Status = launchCheckStatusSkipped
|
||||
return []LaunchCheck{walletCheck, fundsCheck}
|
||||
}
|
||||
|
||||
address, err := walletAddressFromPrivateKey(model.APIKey.String())
|
||||
if err != nil {
|
||||
walletCheck.Status = launchCheckStatusFailed
|
||||
walletCheck.Code = "AI_WALLET_INVALID_KEY"
|
||||
walletCheck.Message = "The Claw402 wallet key is invalid. Recreate the Base USDC payment wallet."
|
||||
fundsCheck.Status = launchCheckStatusSkipped
|
||||
return []LaunchCheck{walletCheck, fundsCheck}
|
||||
}
|
||||
|
||||
walletCheck.Status = launchCheckStatusOK
|
||||
walletCheck.Address = address
|
||||
fundsCheck.Address = address
|
||||
|
||||
balance, err := queryAIWalletBalance(address)
|
||||
if err != nil {
|
||||
fundsCheck.Status = launchCheckStatusWarning
|
||||
fundsCheck.Code = "AI_WALLET_BALANCE_UNKNOWN"
|
||||
fundsCheck.Message = "Could not verify the Base USDC balance right now. The trader will start, but AI calls fail if the wallet is empty."
|
||||
return []LaunchCheck{walletCheck, fundsCheck}
|
||||
}
|
||||
|
||||
fundsCheck.Actual = &balance
|
||||
if balance < MinAIFeeUSDC {
|
||||
fundsCheck.Status = launchCheckStatusFailed
|
||||
fundsCheck.Code = "AI_WALLET_INSUFFICIENT_FUNDS"
|
||||
fundsCheck.Message = fmt.Sprintf(
|
||||
"The Claw402 wallet holds %.2f USDC but needs at least %.0f USDC on Base to pay for AI and data calls.",
|
||||
balance, MinAIFeeUSDC,
|
||||
)
|
||||
} else {
|
||||
fundsCheck.Status = launchCheckStatusOK
|
||||
}
|
||||
|
||||
return []LaunchCheck{walletCheck, fundsCheck}
|
||||
}
|
||||
|
||||
func checkLaunchStrategy(strategy *store.Strategy, required bool) LaunchCheck {
|
||||
check := LaunchCheck{ID: launchCheckStrategy}
|
||||
|
||||
switch {
|
||||
case strategy != nil:
|
||||
check.Status = launchCheckStatusOK
|
||||
check.Message = strategy.Name
|
||||
case required:
|
||||
check.Status = launchCheckStatusFailed
|
||||
check.Code = "STRATEGY_NOT_FOUND"
|
||||
check.Message = "The selected strategy was not found. Pick or create a strategy first."
|
||||
default:
|
||||
check.Status = launchCheckStatusSkipped
|
||||
}
|
||||
|
||||
return check
|
||||
}
|
||||
|
||||
// checkLaunchExchange validates exchange configuration completeness and then
|
||||
// probes the live account (30s server cache) for status and balance.
|
||||
func (s *Server) checkLaunchExchange(userID string, exchange *store.Exchange) []LaunchCheck {
|
||||
configCheck := LaunchCheck{ID: launchCheckExchangeConfig}
|
||||
accountCheck := LaunchCheck{ID: launchCheckExchangeAccount}
|
||||
fundsCheck := LaunchCheck{ID: launchCheckExchangeFunds, Required: MinTradingUSDC}
|
||||
|
||||
if msg, code := describeExchangeConfigIssue(exchange); code != "" {
|
||||
configCheck.Status = launchCheckStatusFailed
|
||||
configCheck.Code = code
|
||||
configCheck.Message = msg
|
||||
accountCheck.Status = launchCheckStatusSkipped
|
||||
fundsCheck.Status = launchCheckStatusSkipped
|
||||
return []LaunchCheck{configCheck, accountCheck, fundsCheck}
|
||||
}
|
||||
|
||||
configCheck.Status = launchCheckStatusOK
|
||||
configCheck.Message = exchangeDisplayName(exchange)
|
||||
fundsCheck.Asset = accountAssetForExchange(exchange.ExchangeType)
|
||||
|
||||
states, err := s.getExchangeAccountStates(userID)
|
||||
if err != nil {
|
||||
accountCheck.Status = launchCheckStatusWarning
|
||||
accountCheck.Code = "EXCHANGE_STATE_UNKNOWN"
|
||||
accountCheck.Message = "Could not verify the exchange account right now."
|
||||
fundsCheck.Status = launchCheckStatusSkipped
|
||||
return []LaunchCheck{configCheck, accountCheck, fundsCheck}
|
||||
}
|
||||
|
||||
state, ok := states[exchange.ID]
|
||||
if !ok {
|
||||
accountCheck.Status = launchCheckStatusWarning
|
||||
accountCheck.Code = "EXCHANGE_STATE_UNKNOWN"
|
||||
accountCheck.Message = "Could not verify the exchange account right now."
|
||||
fundsCheck.Status = launchCheckStatusSkipped
|
||||
return []LaunchCheck{configCheck, accountCheck, fundsCheck}
|
||||
}
|
||||
|
||||
if state.Status != exchangeAccountStatusOK {
|
||||
accountCheck.Status = launchCheckStatusFailed
|
||||
accountCheck.Code = state.ErrorCode
|
||||
accountCheck.Message = state.ErrorMessage
|
||||
if accountCheck.Message == "" {
|
||||
accountCheck.Message = fmt.Sprintf("Exchange account \"%s\" is not ready (%s).", exchangeDisplayName(exchange), state.Status)
|
||||
}
|
||||
fundsCheck.Status = launchCheckStatusSkipped
|
||||
return []LaunchCheck{configCheck, accountCheck, fundsCheck}
|
||||
}
|
||||
|
||||
accountCheck.Status = launchCheckStatusOK
|
||||
accountCheck.Message = state.DisplayBalance
|
||||
|
||||
// Gate on the better of available balance and total equity: a bot with
|
||||
// capital deployed in open positions has low *available* margin but is
|
||||
// clearly funded — restarting it must not be blocked.
|
||||
funded := state.AvailableBalance
|
||||
if state.TotalEquity > funded {
|
||||
funded = state.TotalEquity
|
||||
}
|
||||
fundsCheck.Actual = &funded
|
||||
|
||||
if funded < MinTradingUSDC {
|
||||
message := fmt.Sprintf(
|
||||
"Exchange account \"%s\" holds %.2f %s but needs at least %.0f %s to place the first trade.",
|
||||
exchangeDisplayName(exchange), funded, fundsCheck.Asset, MinTradingUSDC, fundsCheck.Asset,
|
||||
)
|
||||
if exchange.Testnet {
|
||||
// Testnet balances are play money — warn instead of block.
|
||||
fundsCheck.Status = launchCheckStatusWarning
|
||||
} else {
|
||||
fundsCheck.Status = launchCheckStatusFailed
|
||||
}
|
||||
fundsCheck.Code = "EXCHANGE_INSUFFICIENT_FUNDS"
|
||||
fundsCheck.Message = message
|
||||
} else {
|
||||
fundsCheck.Status = launchCheckStatusOK
|
||||
}
|
||||
|
||||
return []LaunchCheck{configCheck, accountCheck, fundsCheck}
|
||||
}
|
||||
|
||||
// describeExchangeConfigIssue mirrors the create-time exchange validation but
|
||||
// returns stable uppercase codes for the checklist UI.
|
||||
func describeExchangeConfigIssue(exchange *store.Exchange) (string, string) {
|
||||
if exchange == nil {
|
||||
return "The selected exchange account was not found. Connect an exchange first.", "EXCHANGE_NOT_FOUND"
|
||||
}
|
||||
if !exchange.Enabled {
|
||||
return fmt.Sprintf("Exchange account \"%s\" is disabled. Enable it first.", exchangeDisplayName(exchange)), "EXCHANGE_DISABLED"
|
||||
}
|
||||
if missing := missingExchangeFields(exchange); len(missing) > 0 {
|
||||
return fmt.Sprintf(
|
||||
"Exchange account \"%s\" is missing %s. Complete the connection first.",
|
||||
exchangeDisplayName(exchange), strings.Join(missing, ", "),
|
||||
), "EXCHANGE_MISSING_FIELDS"
|
||||
}
|
||||
if exchange.ExchangeType == "hyperliquid" && !exchange.HyperliquidBuilderApproved {
|
||||
return "Hyperliquid builder authorization is not complete. Reconnect the wallet and finish the authorization.", "HYPERLIQUID_BUILDER_NOT_APPROVED"
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
293
api/launch_preflight_test.go
Normal file
293
api/launch_preflight_test.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"nofx/crypto"
|
||||
"nofx/store"
|
||||
)
|
||||
|
||||
// Well-known throwaway development key (hardhat account #1) — never funded.
|
||||
const testClaw402Key = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
|
||||
|
||||
func withAIWalletBalance(t *testing.T, balance float64, err error) {
|
||||
t.Helper()
|
||||
original := queryAIWalletBalance
|
||||
queryAIWalletBalance = func(string) (float64, error) {
|
||||
return balance, err
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
queryAIWalletBalance = original
|
||||
})
|
||||
}
|
||||
|
||||
func findCheck(t *testing.T, checks []LaunchCheck, id string) LaunchCheck {
|
||||
t.Helper()
|
||||
for _, check := range checks {
|
||||
if check.ID == id {
|
||||
return check
|
||||
}
|
||||
}
|
||||
t.Fatalf("check %q not found in %+v", id, checks)
|
||||
return LaunchCheck{}
|
||||
}
|
||||
|
||||
func TestCheckLaunchAIModel(t *testing.T) {
|
||||
if got := checkLaunchAIModel(nil); got.Status != launchCheckStatusFailed || got.Code != "MODEL_NOT_FOUND" {
|
||||
t.Fatalf("nil model: expected failed/MODEL_NOT_FOUND, got %+v", got)
|
||||
}
|
||||
|
||||
disabled := &store.AIModel{Name: "Claw402", Provider: "claw402", Enabled: false}
|
||||
if got := checkLaunchAIModel(disabled); got.Code != "MODEL_DISABLED" {
|
||||
t.Fatalf("disabled model: expected MODEL_DISABLED, got %+v", got)
|
||||
}
|
||||
|
||||
noKey := &store.AIModel{Name: "Claw402", Provider: "claw402", Enabled: true}
|
||||
if got := checkLaunchAIModel(noKey); got.Code != "MODEL_MISSING_CREDENTIALS" {
|
||||
t.Fatalf("missing key: expected MODEL_MISSING_CREDENTIALS, got %+v", got)
|
||||
}
|
||||
|
||||
ready := &store.AIModel{Name: "Claw402", Provider: "claw402", Enabled: true, APIKey: crypto.EncryptedString(testClaw402Key)}
|
||||
if got := checkLaunchAIModel(ready); got.Status != launchCheckStatusOK {
|
||||
t.Fatalf("ready model: expected ok, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLaunchAIWalletSkipsNonClaw402(t *testing.T) {
|
||||
model := &store.AIModel{Name: "DeepSeek", Provider: "deepseek", Enabled: true, APIKey: crypto.EncryptedString("sk-test")}
|
||||
checks := checkLaunchAIWallet(model)
|
||||
if got := findCheck(t, checks, launchCheckAIWallet); got.Status != launchCheckStatusSkipped {
|
||||
t.Fatalf("non-claw402 wallet check should be skipped, got %+v", got)
|
||||
}
|
||||
if got := findCheck(t, checks, launchCheckAIWalletFunds); got.Status != launchCheckStatusSkipped {
|
||||
t.Fatalf("non-claw402 funds check should be skipped, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLaunchAIWalletInvalidKey(t *testing.T) {
|
||||
model := &store.AIModel{Name: "Claw402", Provider: "claw402", Enabled: true, APIKey: crypto.EncryptedString("not-a-key")}
|
||||
checks := checkLaunchAIWallet(model)
|
||||
got := findCheck(t, checks, launchCheckAIWallet)
|
||||
if got.Status != launchCheckStatusFailed || got.Code != "AI_WALLET_INVALID_KEY" {
|
||||
t.Fatalf("invalid key: expected failed/AI_WALLET_INVALID_KEY, got %+v", got)
|
||||
}
|
||||
if funds := findCheck(t, checks, launchCheckAIWalletFunds); funds.Status != launchCheckStatusSkipped {
|
||||
t.Fatalf("funds check should be skipped when the key is invalid, got %+v", funds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLaunchAIWalletInsufficientFunds(t *testing.T) {
|
||||
withAIWalletBalance(t, 0.25, nil)
|
||||
|
||||
model := &store.AIModel{Name: "Claw402", Provider: "claw402", Enabled: true, APIKey: crypto.EncryptedString(testClaw402Key)}
|
||||
checks := checkLaunchAIWallet(model)
|
||||
|
||||
wallet := findCheck(t, checks, launchCheckAIWallet)
|
||||
if wallet.Status != launchCheckStatusOK || wallet.Address == "" {
|
||||
t.Fatalf("wallet check should pass with derived address, got %+v", wallet)
|
||||
}
|
||||
|
||||
funds := findCheck(t, checks, launchCheckAIWalletFunds)
|
||||
if funds.Status != launchCheckStatusFailed || funds.Code != "AI_WALLET_INSUFFICIENT_FUNDS" {
|
||||
t.Fatalf("expected failed/AI_WALLET_INSUFFICIENT_FUNDS, got %+v", funds)
|
||||
}
|
||||
if funds.Actual == nil || *funds.Actual != 0.25 {
|
||||
t.Fatalf("expected actual balance 0.25, got %+v", funds.Actual)
|
||||
}
|
||||
if funds.Required != MinAIFeeUSDC {
|
||||
t.Fatalf("expected required %v, got %v", MinAIFeeUSDC, funds.Required)
|
||||
}
|
||||
if funds.Address == "" {
|
||||
t.Fatalf("funds check should carry the deposit address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLaunchAIWalletRPCOutageIsWarningNotFailure(t *testing.T) {
|
||||
withAIWalletBalance(t, 0, errors.New("rpc unreachable"))
|
||||
|
||||
model := &store.AIModel{Name: "Claw402", Provider: "claw402", Enabled: true, APIKey: crypto.EncryptedString(testClaw402Key)}
|
||||
checks := checkLaunchAIWallet(model)
|
||||
|
||||
funds := findCheck(t, checks, launchCheckAIWalletFunds)
|
||||
if funds.Status != launchCheckStatusWarning || funds.Code != "AI_WALLET_BALANCE_UNKNOWN" {
|
||||
t.Fatalf("RPC outage must degrade to warning, got %+v", funds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLaunchAIWalletFunded(t *testing.T) {
|
||||
withAIWalletBalance(t, 25.5, nil)
|
||||
|
||||
model := &store.AIModel{Name: "Claw402", Provider: "claw402", Enabled: true, APIKey: crypto.EncryptedString(testClaw402Key)}
|
||||
checks := checkLaunchAIWallet(model)
|
||||
|
||||
funds := findCheck(t, checks, launchCheckAIWalletFunds)
|
||||
if funds.Status != launchCheckStatusOK {
|
||||
t.Fatalf("funded wallet should pass, got %+v", funds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeExchangeConfigIssue(t *testing.T) {
|
||||
if _, code := describeExchangeConfigIssue(nil); code != "EXCHANGE_NOT_FOUND" {
|
||||
t.Fatalf("nil exchange: expected EXCHANGE_NOT_FOUND, got %s", code)
|
||||
}
|
||||
|
||||
disabled := &store.Exchange{ID: "ex", ExchangeType: "hyperliquid", Enabled: false}
|
||||
if _, code := describeExchangeConfigIssue(disabled); code != "EXCHANGE_DISABLED" {
|
||||
t.Fatalf("disabled: expected EXCHANGE_DISABLED, got %s", code)
|
||||
}
|
||||
|
||||
missing := &store.Exchange{ID: "ex", ExchangeType: "hyperliquid", Enabled: true}
|
||||
if _, code := describeExchangeConfigIssue(missing); code != "EXCHANGE_MISSING_FIELDS" {
|
||||
t.Fatalf("missing fields: expected EXCHANGE_MISSING_FIELDS, got %s", code)
|
||||
}
|
||||
|
||||
unapproved := &store.Exchange{
|
||||
ID: "ex",
|
||||
ExchangeType: "hyperliquid",
|
||||
Enabled: true,
|
||||
APIKey: crypto.EncryptedString(testClaw402Key),
|
||||
HyperliquidWalletAddr: "0x1111111111111111111111111111111111111111",
|
||||
}
|
||||
if _, code := describeExchangeConfigIssue(unapproved); code != "HYPERLIQUID_BUILDER_NOT_APPROVED" {
|
||||
t.Fatalf("builder unapproved: expected HYPERLIQUID_BUILDER_NOT_APPROVED, got %s", code)
|
||||
}
|
||||
|
||||
unapproved.HyperliquidBuilderApproved = true
|
||||
if _, code := describeExchangeConfigIssue(unapproved); code != "" {
|
||||
t.Fatalf("ready exchange: expected no issue, got %s", code)
|
||||
}
|
||||
}
|
||||
|
||||
func readyHyperliquidExchange() *store.Exchange {
|
||||
return &store.Exchange{
|
||||
ID: "ex-hl",
|
||||
ExchangeType: "hyperliquid",
|
||||
Enabled: true,
|
||||
APIKey: crypto.EncryptedString(testClaw402Key),
|
||||
HyperliquidWalletAddr: "0x1111111111111111111111111111111111111111",
|
||||
HyperliquidBuilderApproved: true,
|
||||
}
|
||||
}
|
||||
|
||||
func preflightTestServer(t *testing.T, userID string, states map[string]ExchangeAccountState) *Server {
|
||||
t.Helper()
|
||||
server := &Server{exchangeAccountStateCache: NewExchangeAccountStateCache()}
|
||||
if states != nil {
|
||||
server.exchangeAccountStateCache.Set(userID, states)
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
func TestCheckLaunchExchangeInsufficientFundsBlocks(t *testing.T) {
|
||||
exchange := readyHyperliquidExchange()
|
||||
server := preflightTestServer(t, "user-1", map[string]ExchangeAccountState{
|
||||
exchange.ID: {ExchangeID: exchange.ID, Status: exchangeAccountStatusOK, AvailableBalance: 5.5, TotalEquity: 5.5},
|
||||
})
|
||||
|
||||
checks := server.checkLaunchExchange("user-1", exchange)
|
||||
funds := findCheck(t, checks, launchCheckExchangeFunds)
|
||||
if funds.Status != launchCheckStatusFailed || funds.Code != "EXCHANGE_INSUFFICIENT_FUNDS" {
|
||||
t.Fatalf("expected failed/EXCHANGE_INSUFFICIENT_FUNDS, got %+v", funds)
|
||||
}
|
||||
if funds.Actual == nil || *funds.Actual != 5.5 {
|
||||
t.Fatalf("expected actual 5.5, got %+v", funds.Actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLaunchExchangeDeployedMarginPasses(t *testing.T) {
|
||||
// A running bot with capital locked in positions: low available balance
|
||||
// but healthy equity. Restart must not be blocked.
|
||||
exchange := readyHyperliquidExchange()
|
||||
server := preflightTestServer(t, "user-1", map[string]ExchangeAccountState{
|
||||
exchange.ID: {ExchangeID: exchange.ID, Status: exchangeAccountStatusOK, AvailableBalance: 3, TotalEquity: 100},
|
||||
})
|
||||
|
||||
checks := server.checkLaunchExchange("user-1", exchange)
|
||||
funds := findCheck(t, checks, launchCheckExchangeFunds)
|
||||
if funds.Status != launchCheckStatusOK {
|
||||
t.Fatalf("deployed margin with healthy equity should pass, got %+v", funds)
|
||||
}
|
||||
if funds.Actual == nil || *funds.Actual != 100 {
|
||||
t.Fatalf("expected actual 100 (equity), got %+v", funds.Actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLaunchExchangeTestnetLowFundsIsWarning(t *testing.T) {
|
||||
exchange := readyHyperliquidExchange()
|
||||
exchange.Testnet = true
|
||||
server := preflightTestServer(t, "user-1", map[string]ExchangeAccountState{
|
||||
exchange.ID: {ExchangeID: exchange.ID, Status: exchangeAccountStatusOK, AvailableBalance: 0},
|
||||
})
|
||||
|
||||
checks := server.checkLaunchExchange("user-1", exchange)
|
||||
funds := findCheck(t, checks, launchCheckExchangeFunds)
|
||||
if funds.Status != launchCheckStatusWarning {
|
||||
t.Fatalf("testnet low funds should warn, not block, got %+v", funds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLaunchExchangeInvalidCredentials(t *testing.T) {
|
||||
exchange := readyHyperliquidExchange()
|
||||
server := preflightTestServer(t, "user-1", map[string]ExchangeAccountState{
|
||||
exchange.ID: {
|
||||
ExchangeID: exchange.ID,
|
||||
Status: exchangeAccountStatusInvalidCredentials,
|
||||
ErrorCode: "INVALID_CREDENTIALS",
|
||||
ErrorMessage: "Exchange credentials are invalid",
|
||||
},
|
||||
})
|
||||
|
||||
checks := server.checkLaunchExchange("user-1", exchange)
|
||||
account := findCheck(t, checks, launchCheckExchangeAccount)
|
||||
if account.Status != launchCheckStatusFailed || account.Code != "INVALID_CREDENTIALS" {
|
||||
t.Fatalf("expected failed/INVALID_CREDENTIALS, got %+v", account)
|
||||
}
|
||||
if funds := findCheck(t, checks, launchCheckExchangeFunds); funds.Status != launchCheckStatusSkipped {
|
||||
t.Fatalf("funds check should be skipped when account probe fails, got %+v", funds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLaunchPreflightAggregatesReadiness(t *testing.T) {
|
||||
withAIWalletBalance(t, 10, nil)
|
||||
|
||||
exchange := readyHyperliquidExchange()
|
||||
server := preflightTestServer(t, "user-1", map[string]ExchangeAccountState{
|
||||
exchange.ID: {ExchangeID: exchange.ID, Status: exchangeAccountStatusOK, AvailableBalance: 100, TotalEquity: 100},
|
||||
})
|
||||
model := &store.AIModel{Name: "Claw402", Provider: "claw402", Enabled: true, APIKey: crypto.EncryptedString(testClaw402Key)}
|
||||
strategy := &store.Strategy{ID: "strat-1", Name: "Autopilot"}
|
||||
|
||||
result := server.runLaunchPreflight("user-1", model, exchange, strategy, true)
|
||||
if !result.Ready {
|
||||
t.Fatalf("expected ready, got %+v", result)
|
||||
}
|
||||
if result.MinAIFeeUSDC != MinAIFeeUSDC || result.MinTradingUSDC != MinTradingUSDC {
|
||||
t.Fatalf("minimums must be exposed in the response, got %+v", result)
|
||||
}
|
||||
|
||||
// Break one prerequisite → not ready, and Summary explains it.
|
||||
withAIWalletBalance(t, 0, nil)
|
||||
result = server.runLaunchPreflight("user-1", model, exchange, strategy, true)
|
||||
if result.Ready {
|
||||
t.Fatalf("expected not ready with empty AI wallet, got %+v", result)
|
||||
}
|
||||
if result.Summary() == "" {
|
||||
t.Fatalf("summary should describe the failing check")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLaunchPreflightWarningsDoNotBlock(t *testing.T) {
|
||||
withAIWalletBalance(t, 0, errors.New("rpc down"))
|
||||
|
||||
exchange := readyHyperliquidExchange()
|
||||
server := preflightTestServer(t, "user-1", map[string]ExchangeAccountState{
|
||||
exchange.ID: {ExchangeID: exchange.ID, Status: exchangeAccountStatusOK, AvailableBalance: 100},
|
||||
})
|
||||
model := &store.AIModel{Name: "Claw402", Provider: "claw402", Enabled: true, APIKey: crypto.EncryptedString(testClaw402Key)}
|
||||
|
||||
result := server.runLaunchPreflight("user-1", model, exchange, nil, false)
|
||||
if !result.Ready {
|
||||
t.Fatalf("warnings must not block launch, got %+v", result)
|
||||
}
|
||||
}
|
||||
@@ -237,8 +237,17 @@ Only include fields you want to change.`,
|
||||
`:id = trader_id from GET /api/my-traders. Stops and permanently removes the trader and all its data.`,
|
||||
s.handleDeleteTrader)
|
||||
s.routeWithSchema(protected, "POST", "/traders/:id/start", "Start trader — begins live trading",
|
||||
`:id = trader_id from GET /api/my-traders. No request body needed. The trader must have a valid exchange and AI model configured.`,
|
||||
`:id = trader_id from GET /api/my-traders. No request body needed. The trader must have a valid exchange and AI model configured.
|
||||
Runs launch preflight first and returns 400 with {"error_key":"trader.start.preflight_failed","preflight":{...}} when checks fail. Append ?force=true to skip balance gates.`,
|
||||
s.handleStartTrader)
|
||||
s.routeWithSchema(protected, "GET", "/traders/:id/preflight", "Run launch readiness checks for a trader",
|
||||
`:id = trader_id from GET /api/my-traders.
|
||||
Returns: {"ready":<bool>,"checks":[{"id":"ai_model|ai_wallet|ai_wallet_funds|strategy|exchange_config|exchange_account|exchange_funds","status":"ok|failed|warning|skipped","code":"<string>","message":"<string>","required":<number>,"actual":<number>,"asset":"<string>","address":"<string>"}],"min_ai_fee_usdc":<number>,"min_trading_usdc":<number>}`,
|
||||
s.handleTraderPreflight)
|
||||
s.routeWithSchema(protected, "POST", "/launch/preflight", "Run launch readiness checks before creating a trader",
|
||||
`Body: {"ai_model_id":"<EXACT id from GET /api/models>","exchange_id":"<EXACT id from GET /api/exchanges>","strategy_id":"<optional, EXACT id from GET /api/strategies>"}
|
||||
Returns the same shape as GET /api/traders/:id/preflight. Use this before POST /api/traders to surface every missing prerequisite at once.`,
|
||||
s.handleLaunchPreflight)
|
||||
s.routeWithSchema(protected, "POST", "/traders/:id/stop", "Stop trader — halts live trading",
|
||||
`:id = trader_id from GET /api/my-traders. No request body needed. Gracefully stops the trading loop.`,
|
||||
s.handleStopTrader)
|
||||
|
||||
44
docs/assets/nofx-banner.svg
Normal file
44
docs/assets/nofx-banner.svg
Normal file
@@ -0,0 +1,44 @@
|
||||
<svg width="1200" height="380" viewBox="0 0 1200 380" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<clipPath id="frame"><rect width="1200" height="380" rx="14"/></clipPath>
|
||||
<pattern id="dots" width="28" height="28" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1.5" cy="1.5" r="1.2" fill="#1A1813" opacity="0.07"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<g clip-path="url(#frame)">
|
||||
<rect width="1200" height="380" fill="#F1ECE2"/>
|
||||
<rect width="1200" height="380" fill="url(#dots)"/>
|
||||
|
||||
<!-- title bar -->
|
||||
<rect width="1200" height="52" fill="#F7F4EC"/>
|
||||
<line x1="0" y1="52.5" x2="1200" y2="52.5" stroke="#1A1813" stroke-opacity="0.14"/>
|
||||
<circle cx="30" cy="26" r="6" fill="#D6433A" opacity="0.9"/>
|
||||
<circle cx="54" cy="26" r="6" fill="#B4B2A9"/>
|
||||
<circle cx="78" cy="26" r="6" fill="#2E8B57" opacity="0.9"/>
|
||||
<text x="600" y="31" text-anchor="middle" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="13" letter-spacing="2" fill="#8A8478">nofx — ai trading terminal</text>
|
||||
|
||||
<!-- wordmark -->
|
||||
<text x="64" y="182" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="92" font-weight="700" letter-spacing="4" fill="#1A1813">NOFX<tspan fill="#E0483B">_</tspan></text>
|
||||
<text x="64" y="228" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="17" letter-spacing="9" fill="#6B6557">AI TRADING TERMINAL</text>
|
||||
|
||||
<text x="64" y="286" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="15" fill="#6B6557">Language models make the decisions.</text>
|
||||
<text x="64" y="312" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="15" fill="#6B6557">A Go runtime enforces the limits.</text>
|
||||
|
||||
<!-- divider -->
|
||||
<line x1="672" y1="84" x2="672" y2="344" stroke="#1A1813" stroke-opacity="0.12"/>
|
||||
|
||||
<!-- session log -->
|
||||
<g font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="14.5">
|
||||
<text x="712" y="112" fill="#8A8478">$ nofx start</text>
|
||||
<text x="712" y="146"><tspan fill="#2E8B57">✓</tspan><tspan fill="#1A1813" dx="10">preflight passed</tspan><tspan fill="#8A8478" dx="10">7/7 checks</tspan></text>
|
||||
<text x="712" y="180"><tspan fill="#2E8B57">✓</tspan><tspan fill="#1A1813" dx="10">trader online</tspan><tspan fill="#8A8478" dx="10">deepseek · hyperliquid</tspan></text>
|
||||
<text x="712" y="222"><tspan fill="#8A8478">decision</tspan><tspan fill="#E0483B" dx="12" font-weight="700">OPEN_LONG</tspan><tspan fill="#1A1813" dx="12">BTC ×3</tspan></text>
|
||||
<text x="712" y="252"><tspan fill="#8A8478">reason</tspan><tspan fill="#6B6557" dx="12">oi rising · funding neutral · 4h breakout</tspan></text>
|
||||
<text x="712" y="282"><tspan fill="#8A8478">risk</tspan><tspan fill="#6B6557" dx="12">sl −2.5% · tp +5.0% · placed on exchange</tspan></text>
|
||||
<text x="712" y="324"><tspan fill="#8A8478">equity</tspan><tspan fill="#1A1813" dx="12">$12,847</tspan><tspan fill="#8A8478" dx="18">pnl</tspan><tspan fill="#2E8B57" dx="12">+4.2%</tspan></text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<rect x="0.5" y="0.5" width="1199" height="379" rx="14" fill="none" stroke="#1A1813" stroke-opacity="0.22"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -1,22 +1,14 @@
|
||||
<p align="center"><strong><a href="https://vergex.trade">vergex.trade</a> によるバックアップ</strong></p>
|
||||
|
||||
<h1 align="center">NOFX</h1>
|
||||
<p align="center"><strong><a href="https://vergex.trade">vergex.trade</a> の支援を受けています</strong></p>
|
||||
|
||||
<p align="center">
|
||||
<strong>グローバル市場向け AI トレーディングターミナル。</strong><br/>
|
||||
<strong>米国株、コモディティ、FX、暗号資産のリサーチ、戦略生成、執行、モニタリング。</strong>
|
||||
<img src="../../assets/nofx-banner.svg" alt="NOFX — AI トレーディングターミナル" width="100%"/>
|
||||
</p>
|
||||
|
||||
<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://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-E0483B?style=flat-square&labelColor=1A1813" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/telegram-community-E0483B?style=flat-square&labelColor=1A1813&logo=telegram&logoColor=white" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -29,89 +21,83 @@
|
||||
<a href="../vi/README.md">Tiếng Việt</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
NOFX は、マーケットリサーチ、戦略開発、取引執行、ポートフォリオ監視をひとつのワークスペースで行うためのオープンソース AI トレーディングターミナルです。
|
||||
NOFX は、戦略そのものが言語モデルであるオープンソースのトレーディングターミナルです。各トレーダーは、市場構造を読み取り、判断し、執行し、その根拠を記録するという連続的なループを回し続けます。その間、Go ランタイムがすべての注文を、モデルには上書きできないハードなリスク制限内に抑え込みます。
|
||||
|
||||
対象は米国株、コモディティ契約、FX ペア、デジタル資産などの高流動性グローバル市場です。AI レイヤーは取引意図をウォッチリスト、シグナル、戦略ロジック、リスク制御、執行ワークフローへ変換します。
|
||||
トレーダーの構成は自由です。任意のモデル、9 つの取引所のいずれか、任意の戦略を組み合わせられます。複数のトレーダーを並行して走らせ、実現リターンに基づく公開リーダーボードで比較できます。すべては自分のマシン上で動作し、取引所の認証情報は保存時に暗号化され、外部に送信されることはありません。
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
**http://127.0.0.1:3000** を開きます。
|
||||
ターミナルは `http://127.0.0.1:3000` で開きます。
|
||||
|
||||
---
|
||||
**初回起動**
|
||||
|
||||
## 取引所登録
|
||||
1. 登録します — 最初に作成したアカウントがこのインスタンスのオーナーになります。
|
||||
2. ガイド付きの起動手順に従います。自動作成される AI 手数料ウォレットに **$1+ USDC**(Base ネットワーク)を入金し、続いて Hyperliquid を接続して取引資金として **$12+ USDC** を入金します。
|
||||
3. **Autopilot** を開始します。AI は数分ごとに市場をスキャンして自律的に取引し、すべての判断はその場でダッシュボードに表示されます。ワンクリックでいつでも停止できます。
|
||||
|
||||
以下のリンクから、暗号資産および対応する米国株、FX、コモディティデリバティブ市場向けの取引口座を開設できます。これらは NOFX のパートナープログラム経由で、手数料割引または紹介特典が適用される場合があります。
|
||||
<br/>
|
||||
|
||||
| 取引所 | 状態 | 手数料割引付き登録 |
|
||||
| :--- | :---: | :--- |
|
||||
| <img src="../../../web/public/exchange-icons/binance.jpg" width="20" height="20" style="vertical-align: middle;"/> **Binance** | ✅ | [登録](https://www.binance.com/join?ref=NOFXENG) |
|
||||
| <img src="../../../web/public/exchange-icons/bybit.png" width="20" height="20" style="vertical-align: middle;"/> **Bybit** | ✅ | [登録](https://partner.bybit.com/b/83856) |
|
||||
| <img src="../../../web/public/exchange-icons/okx.svg" width="20" height="20" style="vertical-align: middle;"/> **OKX** | ✅ | [登録](https://www.okx.com/join/1865360) |
|
||||
| <img src="../../../web/public/exchange-icons/hyperliquid.png" width="20" height="20" style="vertical-align: middle;"/> **Hyperliquid** | ✅ | [登録](https://app.hyperliquid.xyz/join/AITRADING) |
|
||||
| <img src="../../../web/public/exchange-icons/bitget.svg" width="20" height="20" style="vertical-align: middle;"/> **Bitget** | ✅ | [登録](https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172) |
|
||||
| <img src="../../../web/public/exchange-icons/kucoin.svg" width="20" height="20" style="vertical-align: middle;"/> **KuCoin** | ✅ | [登録](https://www.kucoin.com/r/broker/CXEV7XKK) |
|
||||
| <img src="../../../web/public/exchange-icons/gate.svg" width="20" height="20" style="vertical-align: middle;"/> **Gate** | ✅ | [登録](https://www.gatenode.xyz/share/VQBGUAxY) |
|
||||
| <img src="../../../web/public/exchange-icons/aster.svg" width="20" height="20" style="vertical-align: middle;"/> **Aster** | ✅ | [登録](https://www.asterdex.com/en/referral/fdfc0e) |
|
||||
| <img src="../../../web/public/exchange-icons/lighter.png" width="20" height="20" style="vertical-align: middle;"/> **Lighter** | ✅ | [登録](https://app.lighter.xyz/?referral=68151432) |
|
||||
## 取引所の登録
|
||||
|
||||
---
|
||||
NOFX は無料のオープンソースソフトウェアです。以下のパートナーリンク経由で口座を開設すると、取引手数料の割引が受けられるうえ、継続的な開発の資金にもなります。
|
||||
|
||||
## クイックデモ
|
||||
| 取引所 | 状態 | 手数料割引付きで登録 |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------- | :----: | :---------------------------------------------------------------------------------- |
|
||||
| <img src="../../../web/public/exchange-icons/binance.jpg" width="20" height="20" style="vertical-align: middle;"/> **Binance** | ✅ | [登録](https://www.binance.com/join?ref=NOFXENG) |
|
||||
| <img src="../../../web/public/exchange-icons/bybit.png" width="20" height="20" style="vertical-align: middle;"/> **Bybit** | ✅ | [登録](https://partner.bybit.com/b/83856) |
|
||||
| <img src="../../../web/public/exchange-icons/okx.svg" width="20" height="20" style="vertical-align: middle;"/> **OKX** | ✅ | [登録](https://www.okx.com/join/1865360) |
|
||||
| <img src="../../../web/public/exchange-icons/hyperliquid.png" width="20" height="20" style="vertical-align: middle;"/> **Hyperliquid** | ✅ | [登録](https://app.hyperliquid.xyz/join/AITRADING) |
|
||||
| <img src="../../../web/public/exchange-icons/bitget.svg" width="20" height="20" style="vertical-align: middle;"/> **Bitget** | ✅ | [登録](https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172) |
|
||||
| <img src="../../../web/public/exchange-icons/kucoin.svg" width="20" height="20" style="vertical-align: middle;"/> **KuCoin** | ✅ | [登録](https://www.kucoin.com/r/broker/CXEV7XKK) |
|
||||
| <img src="../../../web/public/exchange-icons/gate.svg" width="20" height="20" style="vertical-align: middle;"/> **Gate** | ✅ | [登録](https://www.gatenode.xyz/share/VQBGUAxY) |
|
||||
| <img src="../../../web/public/exchange-icons/aster.svg" width="20" height="20" style="vertical-align: middle;"/> **Aster** | ✅ | [登録](https://www.asterdex.com/en/referral/fdfc0e) |
|
||||
| <img src="../../../web/public/exchange-icons/lighter.png" width="20" height="20" style="vertical-align: middle;"/> **Lighter** | ✅ | [登録](https://app.lighter.xyz/?referral=68151432) |
|
||||
|
||||
<br/>
|
||||
|
||||
## デモ
|
||||
|
||||
https://github.com/user-attachments/assets/3310f495-14c5-4586-a1cc-3d32e44aa505
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 市場
|
||||
## モデルは提案する。ランタイムが決める。
|
||||
|
||||
**米国株 · コモディティ · FX · 暗号資産**
|
||||
判断は、[Claw402.ai](https://claw402.ai)・Vergex のデータスタックを読み取る言語モデルから生まれます。全市場を方向バイアスとシグナル強度でランク付けするライブシグナルボード、銘柄ごとの Signal Lab ディープシグナル、群衆の「燃料」と「壁」の位置を映すコストベース・清算ヒートマップ、リアルタイムのネットフロー — これらを生のローソク足とトレーダー自身のライブ実績と突き合わせます。ただし、執行はそうではありません。
|
||||
|
||||
NOFX は単一取引所の画面ではなく、マルチアセットのリサーチ、戦略構築、執行、監視ワークフローを中心に設計されています。
|
||||
すべての注文は、モデルの手が届かない、コードで強制されるリスク制限を通過します。
|
||||
|
||||
---
|
||||
| | |
|
||||
| :----------------------- | :--------------------------------------------------------------------------------- |
|
||||
| ポジション制限 | 同時ポジション数の上限、想定元本は口座資産に対する比率で制限、1 シンボルにつき 1 ポジション |
|
||||
| レバレッジ制限 | モデルの要求とは無関係に、注文サイズの算出時にハードキャップを適用 |
|
||||
| 取引所側の保護 | すべてのエントリー直後に、ストップロスとテイクプロフィットを取引所側に設置 |
|
||||
| ドローダウン自動クローズ | ピークから利益を大きく吐き出したポジションは自動的にクローズ |
|
||||
| 取引スロットリング | 最低保有時間、シンボルごとの再エントリークールダウン、サイクルごと・1 時間ごとのエントリー回数制限 |
|
||||
| セーフモード | モデルの失敗が繰り返された場合、モデルが回復するまで新規エントリーをブロック |
|
||||
| 起動前チェック | トレーダーの開始前に、モデルアクセス、ウォレット資金、戦略、取引所残高を検証 |
|
||||
|
||||
## AI モデルアクセス
|
||||
各判断は、モデルの推論全文とともに保存されます。記録の残らないポジションは存在しません。
|
||||
|
||||
NOFX は AI 推論を [Claw402](https://claw402.ai) 経由で自動ルーティングします。ユーザーはモデルプロバイダーの設定、API キー管理、個別 AI アカウントの維持を行う必要がありません。ターミナルは Claw402 の従量課金インフラを使って対応モデルへオンデマンドにアクセスし、公式割引チャネルを通じてルーティングします。
|
||||
<br/>
|
||||
|
||||
| プロバイダー | アクセス |
|
||||
## ターミナル
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| **Claw402** | [公式割引で従量課金 AI モデルにアクセス](https://claw402.ai) |
|
||||
|
||||
---
|
||||
|
||||
## 機能
|
||||
|
||||
| 機能 | 説明 |
|
||||
| :--- | :--- |
|
||||
| **AI トレーディングターミナル** | 米国株、コモディティ、FX、暗号資産向けの統合ワークスペース |
|
||||
| **AI モデルアクセス** | Claw402 経由で対応プロバイダーへ自動接続 |
|
||||
| **取引所接続** | Binance、Bybit、OKX、Hyperliquid、Bitget、KuCoin、Gate、Aster、Lighter |
|
||||
| **Strategy Studio** | 市場ユニバース、インジケーター、リスク制御、戦略ロジック |
|
||||
| **モデル競争** | AI トレーダーのライブ成績とランキングを比較 |
|
||||
| **Telegram Agent** | チャットからトレーディングアシスタントを操作・監視 |
|
||||
| **ポートフォリオダッシュボード** | ポジション、損益、執行履歴、モデル判断ログ |
|
||||
|
||||
---
|
||||
|
||||
## スクリーンショット
|
||||
| **Autopilot** | ガイド付きの起動フロー:資金投入、接続、入金、開始 — 全工程をサーバー側の事前チェックが支えます |
|
||||
| **Strategy Studio** | スタイルプリセット、コインユニバース、インジケーター、レバレッジ、エントリー確信度、カスタムプロンプト |
|
||||
| **Competition** | 実現リターンでランク付けされる公開リーダーボード。各エントリーには使用モデルが明記されます |
|
||||
| **Dashboard** | ライブのポジション、注文、統計、そしてすべての判断の背後にある推論 |
|
||||
|
||||
<details>
|
||||
<summary><b>設定ページ</b></summary>
|
||||
<summary>スクリーンショット</summary>
|
||||
|
||||
| 設定 | トレーダー一覧 |
|
||||
| :----------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/config-ai-exchanges.png" width="400"/> | <img src="../../../screenshots/config-traders-list.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>ダッシュボード</b></summary>
|
||||
<br/>
|
||||
|
||||
| 概要 | マーケットチャート |
|
||||
| :-----------------------------------------------------: | :-------------------------------------------------------------: |
|
||||
@@ -121,131 +107,51 @@ NOFX は AI 推論を [Claw402](https://claw402.ai) 経由で自動ルーティ
|
||||
| :--------------------------------------------------------------: | :-----------------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-trading-stats.png" width="400"/> | <img src="../../../screenshots/dashboard-position-history.png" width="400"/> |
|
||||
|
||||
| ポジション | トレーダー詳細 |
|
||||
| :----------------------------------------------------------: | :---------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-positions.png" width="400"/> | <img src="../../../screenshots/details-page.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Strategy Studio</b></summary>
|
||||
|
||||
| 戦略エディタ | インジケーター設定 |
|
||||
| :------------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/strategy-studio.png" width="400"/> | <img src="../../../screenshots/strategy-indicators.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>コンペティション</b></summary>
|
||||
|
||||
| コンペティションモード |
|
||||
| :-------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/competition-page.png" width="400"/> |
|
||||
| コンペティション | 設定 |
|
||||
| :-------------------------------------------------------: | :-----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/competition-page.png" width="400"/> | <img src="../../../screenshots/config-ai-exchanges.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## インストール
|
||||
## モデル
|
||||
|
||||
### Linux / macOS
|
||||
自分の API キーで 8 つのプロバイダーを利用できます — DeepSeek、OpenAI、Claude、Qwen、Gemini、Grok、Kimi、MiniMax。カスタムエンドポイントとカスタムモデル名にも対応しています。
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
あるいは、キーを一切使わない方法もあります。[Claw402](https://claw402.ai) は x402 プロトコル上で、モデルの利用量を呼び出しごとに USDC で課金します。Base 上のウォレットひとつが、すべての API キーの代わりになります。
|
||||
|
||||
### Railway(クラウド)
|
||||
| プロバイダー | アクセス |
|
||||
| :------- | :----- |
|
||||
| **Claw402** | [公式割引付きの従量課金 AI モデル](https://claw402.ai) |
|
||||
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
## マーケット
|
||||
|
||||
### Docker
|
||||
9 つの取引所すべてで暗号資産の無期限先物を取引できます。Hyperliquid では、同じランタイムが暗号資産に加えて、トークン化された米国株、コモディティ、株価指数、FX、プレ IPO 無期限先物 — TSLA、NVDA、GOLD、SPX、EUR、OPENAI — も取引します。
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
[Docker Desktop](https://www.docker.com/products/docker-desktop/) をインストールしてから:
|
||||
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### ソースからビルド
|
||||
|
||||
```bash
|
||||
# Prerequisites: Go 1.21+, Node.js 18+, TA-Lib
|
||||
# macOS: brew install ta-lib
|
||||
# Ubuntu: sudo apt-get install libta-lib0-dev
|
||||
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx
|
||||
cd web && npm install && npm run dev
|
||||
```
|
||||
|
||||
### アップデート
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## セットアップ
|
||||
|
||||
**初心者モード**:ガイド付き onboarding により、モデルアクセス、取引所接続、戦略設定、初回デプロイまで進められます。
|
||||
|
||||
**上級モード**:
|
||||
|
||||
1. AI モデルアクセスを設定
|
||||
2. 取引所の認証情報を接続
|
||||
3. 戦略を構築またはインポート
|
||||
4. AI トレーダープロファイルを作成
|
||||
5. ダッシュボードから起動、監視、改善
|
||||
|
||||
すべての設定は Web UI **http://127.0.0.1:3000** から行えます。
|
||||
|
||||
---
|
||||
|
||||
## サーバーへのデプロイ
|
||||
|
||||
**HTTP デプロイ:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# http://YOUR_IP:3000 でアクセス
|
||||
```
|
||||
|
||||
**Cloudflare 経由の HTTPS:**
|
||||
|
||||
1. [Cloudflare](https://dash.cloudflare.com)(無料プラン)にドメインを追加
|
||||
2. A レコードをサーバー IP に設定(Proxied)
|
||||
3. SSL/TLS を Flexible に設定
|
||||
4. `.env` に `TRANSPORT_ENCRYPTION=true` を設定
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## アーキテクチャ
|
||||
|
||||
```
|
||||
NOFX
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Trading Terminal │
|
||||
│ React + TypeScript + TradingView │
|
||||
│ US Stocks · Commodities · Forex · Crypto │
|
||||
│ React · TypeScript · TradingView │
|
||||
│ Dashboard · Strategy Studio · Competition │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ API Server (Go) │
|
||||
│ JWT auth · encrypted credential store │
|
||||
├──────────────┬──────────────┬───────────────────┤
|
||||
│ Strategy │ Telegram │ Trader Runtime │
|
||||
│ Engine │ Agent │ Risk Controls │
|
||||
│ Strategy │ Autopilot │ Trader Runtime │
|
||||
│ Engine │ Preflight │ Risk Engine │
|
||||
├──────────────┴──────────────┴───────────────────┤
|
||||
│ AI Model Layer │
|
||||
│ Unified provider access through Claw402 │
|
||||
│ Model routing · payment · execution support │
|
||||
│ DeepSeek · OpenAI · Claude · Qwen · Gemini │
|
||||
│ Grok · Kimi · MiniMax · Claw402 (x402 USDC) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Exchange Connectivity │
|
||||
│ Binance · Bybit · OKX · Hyperliquid · Bitget │
|
||||
@@ -253,50 +159,99 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## インストール
|
||||
|
||||
**Linux / macOS**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
**Railway**
|
||||
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
|
||||
**Docker**
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**Windows** — [Docker Desktop](https://www.docker.com/products/docker-desktop/) をインストールしてから、次を実行します。
|
||||
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**ソースからビルド** — Go 1.21+、Node.js 18+ が必要です。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx # backend
|
||||
cd web && npm install && npm run dev # frontend, in a second terminal
|
||||
```
|
||||
|
||||
**アップデート** — インストールスクリプトを再実行すると、その場でアップグレードされます。
|
||||
|
||||
<details>
|
||||
<summary>サーバーへのデプロイ</summary>
|
||||
|
||||
<br/>
|
||||
|
||||
**HTTP**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# http://YOUR_IP:3000
|
||||
```
|
||||
|
||||
**Cloudflare 経由の HTTPS**
|
||||
|
||||
1. [Cloudflare](https://dash.cloudflare.com)(無料プラン)にドメインを追加
|
||||
2. A レコード → サーバー IP(Proxied を有効化)
|
||||
3. SSL/TLS → Flexible
|
||||
4. `.env` に `TRANSPORT_ENCRYPTION=true` を設定
|
||||
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
|
||||
## ドキュメント
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| [アーキテクチャ](../../architecture/README.md) | システム設計とモジュール索引 |
|
||||
| | |
|
||||
| :------------------------------------------------------ | :------------------------------------ |
|
||||
| [はじめに](../../getting-started/README.md) | デプロイと取引所 API のガイド |
|
||||
| [アーキテクチャ](../../architecture/README.md) | システム設計とモジュール索引 |
|
||||
| [戦略モジュール](../../architecture/STRATEGY_MODULE.md) | 銘柄選択、AI プロンプト、執行 |
|
||||
| [FAQ](../../faq/README.md) | よくある質問 |
|
||||
| [はじめに](../../getting-started/README.md) | デプロイガイド |
|
||||
| [FAQ](../../guides/faq.en.md) | よくある質問 |
|
||||
| [トラブルシューティング](../../guides/TROUBLESHOOTING.md) | よくある問題の診断 |
|
||||
|
||||
---
|
||||
## コミュニティ
|
||||
|
||||
## 貢献
|
||||
[Telegram](https://t.me/nofx_dev_community) · [Twitter/X](https://x.com/vergex_ai) · [Issues](https://github.com/NoFxAiOS/nofx/issues) · [vergex.trade](https://vergex.trade) · [ライブダッシュボード](https://vergex.trade/explore)
|
||||
|
||||
[貢献ガイド](../../../CONTRIBUTING.md)、[行動規範](../../../CODE_OF_CONDUCT.md)、[セキュリティポリシー](../../../SECURITY.md) を参照してください。
|
||||
## コントリビューション
|
||||
|
||||
### 貢献者プログラム
|
||||
コード、ドキュメント、翻訳、バグ報告のいずれも歓迎します。詳細は[貢献ガイド](../../../CONTRIBUTING.md)、[行動規範](../../../CODE_OF_CONDUCT.md)、[セキュリティポリシー](../../../SECURITY.md)をご覧ください。
|
||||
|
||||
NOFX は有意義な貢献を記録し、エコシステムの成長に応じて貢献者へ還元する予定です。優先 Issue は高い報酬ウェイトを持ちます。
|
||||
NOFX は有意義な貢献を記録しており、エコシステムの成長に応じて貢献者に還元していく予定です。優先度の高い Issue にはより高いウェイトが付きます。
|
||||
|
||||
| Contribution | Weight |
|
||||
| :--- | :---: |
|
||||
| Pinned Issue PRs | ★★★★★★ |
|
||||
| Code (Merged PRs) | ★★★★★ |
|
||||
| Bug Fixes | ★★★★ |
|
||||
| Feature Ideas | ★★★ |
|
||||
| Bug Reports | ★★ |
|
||||
| Documentation | ★★ |
|
||||
| 貢献の種類 | ウェイト |
|
||||
| :---------------- | :----: |
|
||||
| ピン留め Issue の PR | ★★★★★★ |
|
||||
| コード(マージ済み PR) | ★★★★★ |
|
||||
| バグ修正 | ★★★★ |
|
||||
| 機能アイデア | ★★★ |
|
||||
| バグ報告 | ★★ |
|
||||
| ドキュメント | ★★ |
|
||||
|
||||
---
|
||||
|
||||
## リンク
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| Website | [vergex.trade](https://vergex.trade) |
|
||||
| Dashboard | [vergex.trade/explore](https://vergex.trade/explore) |
|
||||
| Telegram | [nofx_dev_community](https://t.me/nofx_dev_community) |
|
||||
| Twitter | [@vergex_ai](https://x.com/vergex_ai) |
|
||||
|
||||
> **リスク警告**:自動売買には大きなリスクがあります。適切なポジションサイズを守り、各取引所の仕組みを理解し、失ってもよい資金だけを使用してください。
|
||||
|
||||
---
|
||||
<a href="https://github.com/NoFxAiOS/nofx/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=NoFxAiOS/nofx" alt="Contributors"/>
|
||||
</a>
|
||||
|
||||
## スポンサー
|
||||
|
||||
@@ -314,8 +269,14 @@ NOFX は有意義な貢献を記録し、エコシステムの成長に応じて
|
||||
|
||||
[スポンサーになる](https://github.com/sponsors/NoFxAiOS)
|
||||
|
||||
## License
|
||||
<br/>
|
||||
|
||||
NOFX が役に立ったら、スターを付けていただけると、他のトレーダーがこのプロジェクトを見つけやすくなります。
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
|
||||
## ライセンス
|
||||
|
||||
[AGPL-3.0](../../../LICENSE)
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
<sub>自動売買には大きなリスクが伴います。AI 駆動の戦略は実験的なものであり、損失を出す可能性があります。適切なポジションサイズを守り、各取引所の仕組みを理解し、失っても差し支えのない資金以外では決して取引しないでください。詳細は[免責事項](../../../DISCLAIMER.md)をご覧ください。</sub>
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
<p align="center"><strong><a href="https://vergex.trade">vergex.trade</a>가 지원합니다</strong></p>
|
||||
|
||||
<h1 align="center">NOFX</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>글로벌 시장을 위한 AI 트레이딩 터미널.</strong><br/>
|
||||
<strong>미국 주식, 원자재, 외환, 암호화폐 리서치, 전략 생성, 실행, 모니터링.</strong>
|
||||
<img src="../../assets/nofx-banner.svg" alt="NOFX — AI trading terminal" width="100%"/>
|
||||
</p>
|
||||
|
||||
<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://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-E0483B?style=flat-square&labelColor=1A1813" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/telegram-community-E0483B?style=flat-square&labelColor=1A1813&logo=telegram&logoColor=white" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -29,89 +21,83 @@
|
||||
<a href="../vi/README.md">Tiếng Việt</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
NOFX는 시장 리서치, 전략 개발, 거래 실행, 포트폴리오 모니터링을 하나의 워크스페이스에서 처리하는 오픈소스 AI 트레이딩 터미널입니다.
|
||||
NOFX는 언어 모델이 곧 전략이 되는 오픈소스 트레이딩 터미널입니다. 각 트레이더는 시장 구조 읽기, 판단, 실행, 근거 기록으로 이어지는 루프를 끊임없이 반복하고, Go 런타임은 모든 주문을 모델이 절대 우회할 수 없는 하드 리스크 한도 안에 묶어 둡니다.
|
||||
|
||||
제품은 미국 주식, 원자재 계약, FX 페어, 디지털 자산 등 글로벌 유동성 시장을 중심으로 설계되었습니다. AI 레이어는 거래 의도를 워치리스트, 신호, 전략 로직, 리스크 제어, 실행 워크플로로 변환합니다.
|
||||
트레이더는 자유롭게 조합할 수 있습니다. 어떤 모델이든, 아홉 개 거래소 중 어디든, 어떤 전략이든 가능합니다. 여러 트레이더를 나란히 돌리면서 실현 수익률 기준의 공개 리더보드에서 비교해 보세요. 모든 것은 사용자의 머신에서 실행되며, 거래소 자격 증명은 저장 시 암호화되고 절대 외부로 나가지 않습니다.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
**http://127.0.0.1:3000** 을 엽니다.
|
||||
터미널은 `http://127.0.0.1:3000`에서 열립니다.
|
||||
|
||||
---
|
||||
**첫 실행**
|
||||
|
||||
1. 계정을 등록합니다 — 첫 번째 계정이 해당 인스턴스의 소유자가 됩니다.
|
||||
2. 가이드 런치를 따라갑니다. 자동으로 생성된 AI 수수료 지갑에 **$1+ USDC**(Base 네트워크)를 넣은 뒤, Hyperliquid를 연결하고 거래 자금으로 **$12+ USDC**를 입금합니다.
|
||||
3. **Autopilot**을 시작합니다. AI가 몇 분마다 시장을 스캔하며 스스로 거래하고, 모든 결정은 발생하는 즉시 대시보드에 표시됩니다. 언제든 클릭 한 번으로 중지할 수 있습니다.
|
||||
|
||||
<br/>
|
||||
|
||||
## 거래소 등록
|
||||
|
||||
아래 링크를 통해 암호화폐와 지원되는 미국 주식, FX, 원자재 파생상품 시장용 거래 계정을 개설할 수 있습니다. 이 링크는 NOFX 파트너 프로그램을 통해 제공되며 수수료 할인 또는 추천 혜택이 포함될 수 있습니다.
|
||||
NOFX는 무료 오픈소스입니다. 아래 파트너 링크로 계정을 개설하면 거래 수수료가 할인되며, 프로젝트의 지속적인 개발에도 보탬이 됩니다.
|
||||
|
||||
| 거래소 | 상태 | 수수료 할인 등록 |
|
||||
| :--- | :---: | :--- |
|
||||
| <img src="../../../web/public/exchange-icons/binance.jpg" width="20" height="20" style="vertical-align: middle;"/> **Binance** | ✅ | [등록](https://www.binance.com/join?ref=NOFXENG) |
|
||||
| <img src="../../../web/public/exchange-icons/bybit.png" width="20" height="20" style="vertical-align: middle;"/> **Bybit** | ✅ | [등록](https://partner.bybit.com/b/83856) |
|
||||
| <img src="../../../web/public/exchange-icons/okx.svg" width="20" height="20" style="vertical-align: middle;"/> **OKX** | ✅ | [등록](https://www.okx.com/join/1865360) |
|
||||
| <img src="../../../web/public/exchange-icons/hyperliquid.png" width="20" height="20" style="vertical-align: middle;"/> **Hyperliquid** | ✅ | [등록](https://app.hyperliquid.xyz/join/AITRADING) |
|
||||
| <img src="../../../web/public/exchange-icons/bitget.svg" width="20" height="20" style="vertical-align: middle;"/> **Bitget** | ✅ | [등록](https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172) |
|
||||
| <img src="../../../web/public/exchange-icons/kucoin.svg" width="20" height="20" style="vertical-align: middle;"/> **KuCoin** | ✅ | [등록](https://www.kucoin.com/r/broker/CXEV7XKK) |
|
||||
| <img src="../../../web/public/exchange-icons/gate.svg" width="20" height="20" style="vertical-align: middle;"/> **Gate** | ✅ | [등록](https://www.gatenode.xyz/share/VQBGUAxY) |
|
||||
| <img src="../../../web/public/exchange-icons/aster.svg" width="20" height="20" style="vertical-align: middle;"/> **Aster** | ✅ | [등록](https://www.asterdex.com/en/referral/fdfc0e) |
|
||||
| <img src="../../../web/public/exchange-icons/lighter.png" width="20" height="20" style="vertical-align: middle;"/> **Lighter** | ✅ | [등록](https://app.lighter.xyz/?referral=68151432) |
|
||||
| 거래소 | 상태 | 수수료 할인 등록 |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------- | :----: | :---------------------------------------------------------------------------------- |
|
||||
| <img src="../../../web/public/exchange-icons/binance.jpg" width="20" height="20" style="vertical-align: middle;"/> **Binance** | ✅ | [등록](https://www.binance.com/join?ref=NOFXENG) |
|
||||
| <img src="../../../web/public/exchange-icons/bybit.png" width="20" height="20" style="vertical-align: middle;"/> **Bybit** | ✅ | [등록](https://partner.bybit.com/b/83856) |
|
||||
| <img src="../../../web/public/exchange-icons/okx.svg" width="20" height="20" style="vertical-align: middle;"/> **OKX** | ✅ | [등록](https://www.okx.com/join/1865360) |
|
||||
| <img src="../../../web/public/exchange-icons/hyperliquid.png" width="20" height="20" style="vertical-align: middle;"/> **Hyperliquid** | ✅ | [등록](https://app.hyperliquid.xyz/join/AITRADING) |
|
||||
| <img src="../../../web/public/exchange-icons/bitget.svg" width="20" height="20" style="vertical-align: middle;"/> **Bitget** | ✅ | [등록](https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172) |
|
||||
| <img src="../../../web/public/exchange-icons/kucoin.svg" width="20" height="20" style="vertical-align: middle;"/> **KuCoin** | ✅ | [등록](https://www.kucoin.com/r/broker/CXEV7XKK) |
|
||||
| <img src="../../../web/public/exchange-icons/gate.svg" width="20" height="20" style="vertical-align: middle;"/> **Gate** | ✅ | [등록](https://www.gatenode.xyz/share/VQBGUAxY) |
|
||||
| <img src="../../../web/public/exchange-icons/aster.svg" width="20" height="20" style="vertical-align: middle;"/> **Aster** | ✅ | [등록](https://www.asterdex.com/en/referral/fdfc0e) |
|
||||
| <img src="../../../web/public/exchange-icons/lighter.png" width="20" height="20" style="vertical-align: middle;"/> **Lighter** | ✅ | [등록](https://app.lighter.xyz/?referral=68151432) |
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 빠른 데모
|
||||
## 데모
|
||||
|
||||
https://github.com/user-attachments/assets/3310f495-14c5-4586-a1cc-3d32e44aa505
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 시장
|
||||
## 모델은 제안하고, 런타임이 결정합니다
|
||||
|
||||
**미국 주식 · 원자재 · 외환 · 암호화폐**
|
||||
의사결정은 [Claw402.ai](https://claw402.ai) · Vergex 데이터 스택을 읽는 언어 모델에서 나옵니다. 모든 시장을 방향 편향과 시그널 강도로 순위 매기는 실시간 시그널 보드, 종목별 Signal Lab 딥 시그널, 시장의 '연료'와 '벽'이 어디에 있는지 보여주는 코스트·청산 히트맵, 실시간 순유입 — 이를 원시 캔들과 트레이더 자신의 실시간 트랙 레코드로 교차 검증합니다. 그러나 실행은 그렇지 않습니다.
|
||||
|
||||
NOFX는 단일 거래소 화면이 아니라 멀티에셋 리서치, 전략 구축, 실행, 모니터링 워크플로를 중심으로 구성됩니다.
|
||||
모든 주문은 모델의 손이 닿지 않는 곳에서 코드로 강제되는 한도를 통과해야 합니다.
|
||||
|
||||
---
|
||||
| | |
|
||||
| :----------------------- | :--------------------------------------------------------------------------------- |
|
||||
| 포지션 한도 | 최대 동시 포지션 수, 자본 대비 비율로 상한이 걸린 명목 금액, 심볼당 포지션 1개 |
|
||||
| 레버리지 클램프 | 모델이 무엇을 요청하든 관계없이 주문 크기 산정 시점에 적용되는 하드 캡 |
|
||||
| 거래소 측 보호 | 모든 진입 직후 거래소에 스탑로스와 테이크프로핏을 즉시 배치 |
|
||||
| 드로다운 자동 청산 | 고점 대비 이익을 과도하게 반납한 수익 포지션은 자동으로 청산 |
|
||||
| 거래 스로틀링 | 최소 보유 시간, 심볼별 재진입 쿨다운, 사이클당·시간당 진입 횟수 제한 |
|
||||
| 세이프 모드 | 모델 오류가 반복되면 모델이 복구될 때까지 신규 진입을 차단 |
|
||||
| 런치 프리플라이트 | 트레이더 시작 전에 모델 접근성, 지갑 자금, 전략, 거래소 잔고를 검증 |
|
||||
|
||||
## AI 모델 액세스
|
||||
각 결정은 모델의 전체 추론 과정과 함께 저장됩니다. 기록 없는 포지션은 존재하지 않습니다.
|
||||
|
||||
NOFX는 AI 추론을 [Claw402](https://claw402.ai)를 통해 자동 라우팅합니다. 사용자는 모델 제공업체를 설정하거나 API 키를 관리하거나 별도 AI 계정을 유지할 필요가 없습니다. 터미널은 Claw402의 사용량 기반 인프라를 통해 지원 모델에 온디맨드로 접근하며 공식 할인 채널로 트래픽을 라우팅합니다.
|
||||
<br/>
|
||||
|
||||
| 제공업체 | 액세스 |
|
||||
## 터미널
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| **Claw402** | [공식 할인으로 사용량 기반 AI 모델 이용](https://claw402.ai) |
|
||||
|
||||
---
|
||||
|
||||
## 기능
|
||||
|
||||
| 기능 | 설명 |
|
||||
| :--- | :--- |
|
||||
| **AI 트레이딩 터미널** | 미국 주식, 원자재, 외환, 암호화폐 워크플로를 위한 통합 워크스페이스 |
|
||||
| **AI 모델 액세스** | Claw402를 통해 지원 모델 제공업체에 자동 연결 |
|
||||
| **거래소 연결** | Binance, Bybit, OKX, Hyperliquid, Bitget, KuCoin, Gate, Aster, Lighter |
|
||||
| **Strategy Studio** | 시장 유니버스, 지표, 리스크 제어, 전략 로직 |
|
||||
| **모델 경쟁** | AI 트레이더의 실시간 성과와 리더보드 비교 |
|
||||
| **Telegram Agent** | 채팅으로 트레이딩 어시스턴트 제어 및 모니터링 |
|
||||
| **포트폴리오 대시보드** | 포지션, 손익, 실행 기록, 모델 의사결정 로그 |
|
||||
|
||||
---
|
||||
|
||||
## 스크린샷
|
||||
| **Autopilot** | 가이드 런치: 자금 입금, 연결, 예치, 시작 — 전 과정에 서버 측 프리플라이트 적용 |
|
||||
| **Strategy Studio** | 스타일 프리셋, 코인 유니버스, 지표, 레버리지, 진입 신뢰도, 커스텀 프롬프트 |
|
||||
| **경쟁** | 실현 수익률로 순위가 매겨지는 공개 리더보드, 각 항목마다 사용한 모델 표기 |
|
||||
| **대시보드** | 실시간 포지션, 주문, 통계, 그리고 모든 결정 뒤에 있는 추론 과정 |
|
||||
|
||||
<details>
|
||||
<summary><b>설정 페이지</b></summary>
|
||||
<summary>스크린샷</summary>
|
||||
|
||||
| 설정 | 트레이더 목록 |
|
||||
| :----------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/config-ai-exchanges.png" width="400"/> | <img src="../../../screenshots/config-traders-list.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>대시보드</b></summary>
|
||||
<br/>
|
||||
|
||||
| 개요 | 마켓 차트 |
|
||||
| :-----------------------------------------------------: | :-------------------------------------------------------------: |
|
||||
@@ -121,131 +107,51 @@ NOFX는 AI 추론을 [Claw402](https://claw402.ai)를 통해 자동 라우팅합
|
||||
| :--------------------------------------------------------------: | :-----------------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-trading-stats.png" width="400"/> | <img src="../../../screenshots/dashboard-position-history.png" width="400"/> |
|
||||
|
||||
| 포지션 | 트레이더 상세 |
|
||||
| :----------------------------------------------------------: | :---------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-positions.png" width="400"/> | <img src="../../../screenshots/details-page.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Strategy Studio</b></summary>
|
||||
|
||||
| 전략 에디터 | 지표 설정 |
|
||||
| :------------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/strategy-studio.png" width="400"/> | <img src="../../../screenshots/strategy-indicators.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>경쟁</b></summary>
|
||||
|
||||
| 경쟁 모드 |
|
||||
| :-------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/competition-page.png" width="400"/> |
|
||||
| 경쟁 | 설정 |
|
||||
| :-------------------------------------------------------: | :-----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/competition-page.png" width="400"/> | <img src="../../../screenshots/config-ai-exchanges.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 설치
|
||||
## 모델
|
||||
|
||||
### Linux / macOS
|
||||
자신의 키로 사용할 수 있는 여덟 개 제공업체 — DeepSeek, OpenAI, Claude, Qwen, Gemini, Grok, Kimi, MiniMax — 커스텀 엔드포인트와 모델 이름도 지원합니다.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
키가 전혀 없어도 됩니다. [Claw402](https://claw402.ai)는 x402 프로토콜을 통해 모델 사용량을 호출 단위로 USDC로 정산합니다. Base 네트워크의 지갑 하나가 모든 API 키를 대신합니다.
|
||||
|
||||
### Railway(클라우드)
|
||||
| 제공업체 | 이용 방법 |
|
||||
| :------- | :----- |
|
||||
| **Claw402** | [공식 할인이 적용된 종량제 AI 모델](https://claw402.ai) |
|
||||
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
## 시장
|
||||
|
||||
### Docker
|
||||
아홉 개 거래소 전체에서 암호화폐 무기한 선물을 거래합니다. Hyperliquid에서는 같은 런타임이 암호화폐와 함께 토큰화된 미국 주식, 원자재, 지수, 외환, 프리 IPO 무기한 선물 — TSLA, NVDA, GOLD, SPX, EUR, OPENAI — 도 거래합니다.
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
[Docker Desktop](https://www.docker.com/products/docker-desktop/) 설치 후:
|
||||
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### 소스에서 빌드
|
||||
|
||||
```bash
|
||||
# Prerequisites: Go 1.21+, Node.js 18+, TA-Lib
|
||||
# macOS: brew install ta-lib
|
||||
# Ubuntu: sudo apt-get install libta-lib0-dev
|
||||
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx
|
||||
cd web && npm install && npm run dev
|
||||
```
|
||||
|
||||
### 업데이트
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 설정
|
||||
|
||||
**초보자 모드**: 가이드 온보딩이 모델 액세스, 거래소 연결, 전략 설정, 첫 배포까지 안내합니다.
|
||||
|
||||
**고급 모드**:
|
||||
|
||||
1. AI 모델 액세스 설정
|
||||
2. 거래소 인증 정보 연결
|
||||
3. 전략 생성 또는 가져오기
|
||||
4. AI 트레이더 프로필 생성
|
||||
5. 대시보드에서 실행, 모니터링, 개선
|
||||
|
||||
모든 설정은 Web UI **http://127.0.0.1:3000** 에서 가능합니다.
|
||||
|
||||
---
|
||||
|
||||
## 서버에 배포
|
||||
|
||||
**HTTP 배포:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# http://YOUR_IP:3000 으로 접근
|
||||
```
|
||||
|
||||
**Cloudflare를 통한 HTTPS:**
|
||||
|
||||
1. [Cloudflare](https://dash.cloudflare.com)(무료 플랜)에 도메인 추가
|
||||
2. A 레코드를 서버 IP로 지정(Proxied)
|
||||
3. SSL/TLS를 Flexible로 설정
|
||||
4. `.env`에 `TRANSPORT_ENCRYPTION=true` 설정
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 아키텍처
|
||||
|
||||
```
|
||||
NOFX
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Trading Terminal │
|
||||
│ React + TypeScript + TradingView │
|
||||
│ US Stocks · Commodities · Forex · Crypto │
|
||||
│ React · TypeScript · TradingView │
|
||||
│ Dashboard · Strategy Studio · Competition │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ API Server (Go) │
|
||||
│ JWT auth · encrypted credential store │
|
||||
├──────────────┬──────────────┬───────────────────┤
|
||||
│ Strategy │ Telegram │ Trader Runtime │
|
||||
│ Engine │ Agent │ Risk Controls │
|
||||
│ Strategy │ Autopilot │ Trader Runtime │
|
||||
│ Engine │ Preflight │ Risk Engine │
|
||||
├──────────────┴──────────────┴───────────────────┤
|
||||
│ AI Model Layer │
|
||||
│ Unified provider access through Claw402 │
|
||||
│ Model routing · payment · execution support │
|
||||
│ DeepSeek · OpenAI · Claude · Qwen · Gemini │
|
||||
│ Grok · Kimi · MiniMax · Claw402 (x402 USDC) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Exchange Connectivity │
|
||||
│ Binance · Bybit · OKX · Hyperliquid · Bitget │
|
||||
@@ -253,50 +159,99 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 설치
|
||||
|
||||
**Linux / macOS**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
**Railway**
|
||||
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
|
||||
**Docker**
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**Windows** — [Docker Desktop](https://www.docker.com/products/docker-desktop/)을 설치한 뒤:
|
||||
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**소스에서 빌드** — Go 1.21+, Node.js 18+:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx # backend
|
||||
cd web && npm install && npm run dev # frontend, in a second terminal
|
||||
```
|
||||
|
||||
**업데이트** — 설치 스크립트를 다시 실행하면 제자리에서 업그레이드됩니다.
|
||||
|
||||
<details>
|
||||
<summary>서버 배포</summary>
|
||||
|
||||
<br/>
|
||||
|
||||
**HTTP**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# http://YOUR_IP:3000
|
||||
```
|
||||
|
||||
**Cloudflare를 통한 HTTPS**
|
||||
|
||||
1. [Cloudflare](https://dash.cloudflare.com)에 도메인 추가(무료 플랜)
|
||||
2. A 레코드 → 서버 IP, Proxied 설정
|
||||
3. SSL/TLS → Flexible
|
||||
4. `.env`에 `TRANSPORT_ENCRYPTION=true` 설정
|
||||
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
|
||||
## 문서
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| [아키텍처](../../architecture/README.md) | 시스템 설계와 모듈 색인 |
|
||||
| [전략 모듈](../../architecture/STRATEGY_MODULE.md) | 종목 선택, AI 프롬프트, 실행 |
|
||||
| [FAQ](../../faq/README.md) | 자주 묻는 질문 |
|
||||
| [시작하기](../../getting-started/README.md) | 배포 가이드 |
|
||||
| | |
|
||||
| :------------------------------------------------------ | :------------------------------------ |
|
||||
| [시작하기](../../getting-started/README.md) | 배포 및 거래소 API 가이드 |
|
||||
| [아키텍처](../../architecture/README.md) | 시스템 설계와 모듈 색인 |
|
||||
| [전략 모듈](../../architecture/STRATEGY_MODULE.md) | 코인 선정, AI 프롬프트, 실행 |
|
||||
| [FAQ](../../guides/faq.en.md) | 자주 묻는 질문 |
|
||||
| [문제 해결](../../guides/TROUBLESHOOTING.md) | 흔한 문제 진단하기 |
|
||||
|
||||
---
|
||||
## 커뮤니티
|
||||
|
||||
[Telegram](https://t.me/nofx_dev_community) · [Twitter/X](https://x.com/vergex_ai) · [Issues](https://github.com/NoFxAiOS/nofx/issues) · [vergex.trade](https://vergex.trade) · [라이브 대시보드](https://vergex.trade/explore)
|
||||
|
||||
## 기여
|
||||
|
||||
[기여 가이드](../../../CONTRIBUTING.md), [행동 강령](../../../CODE_OF_CONDUCT.md), [보안 정책](../../../SECURITY.md)을 확인하세요.
|
||||
코드, 문서, 번역, 버그 리포트 모두 환영합니다 — [기여 가이드](../../../CONTRIBUTING.md), [행동 강령](../../../CODE_OF_CONDUCT.md), [보안 정책](../../../SECURITY.md)을 참고하세요.
|
||||
|
||||
### 기여자 프로그램
|
||||
NOFX는 의미 있는 기여를 기록하며, 생태계가 성장함에 따라 기여자에게 보상할 계획입니다. 우선순위 이슈에는 더 높은 가중치가 부여됩니다.
|
||||
|
||||
NOFX는 의미 있는 기여를 기록하며 생태계 성장에 따라 기여자에게 보상할 계획입니다. 우선순위 이슈는 더 높은 보상 가중치를 가집니다.
|
||||
| 기여 유형 | 가중치 |
|
||||
| :---------------- | :----: |
|
||||
| 고정 이슈 PR | ★★★★★★ |
|
||||
| 코드(머지된 PR) | ★★★★★ |
|
||||
| 버그 수정 | ★★★★ |
|
||||
| 기능 아이디어 | ★★★ |
|
||||
| 버그 리포트 | ★★ |
|
||||
| 문서 | ★★ |
|
||||
|
||||
| Contribution | Weight |
|
||||
| :--- | :---: |
|
||||
| Pinned Issue PRs | ★★★★★★ |
|
||||
| Code (Merged PRs) | ★★★★★ |
|
||||
| Bug Fixes | ★★★★ |
|
||||
| Feature Ideas | ★★★ |
|
||||
| Bug Reports | ★★ |
|
||||
| Documentation | ★★ |
|
||||
|
||||
---
|
||||
|
||||
## 링크
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| Website | [vergex.trade](https://vergex.trade) |
|
||||
| Dashboard | [vergex.trade/explore](https://vergex.trade/explore) |
|
||||
| Telegram | [nofx_dev_community](https://t.me/nofx_dev_community) |
|
||||
| Twitter | [@vergex_ai](https://x.com/vergex_ai) |
|
||||
|
||||
> **위험 고지**: 자동매매에는 상당한 위험이 따릅니다. 적절한 포지션 규모를 사용하고 각 거래소 구조를 이해하며 감당 가능한 자금만 거래하세요.
|
||||
|
||||
---
|
||||
<a href="https://github.com/NoFxAiOS/nofx/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=NoFxAiOS/nofx" alt="Contributors"/>
|
||||
</a>
|
||||
|
||||
## 스폰서
|
||||
|
||||
@@ -314,8 +269,14 @@ NOFX는 의미 있는 기여를 기록하며 생태계 성장에 따라 기여
|
||||
|
||||
[스폰서 되기](https://github.com/sponsors/NoFxAiOS)
|
||||
|
||||
## License
|
||||
<br/>
|
||||
|
||||
NOFX가 도움이 되었다면, 스타 하나가 다른 트레이더들이 이 프로젝트를 발견하는 데 큰 힘이 됩니다.
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
|
||||
## 라이선스
|
||||
|
||||
[AGPL-3.0](../../../LICENSE)
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
<sub>자동매매에는 상당한 위험이 따릅니다. AI 기반 전략은 실험적이며 손실이 발생할 수 있습니다. 포지션 규모를 적절히 관리하고, 각 거래소의 구조를 이해하며, 잃어도 감당할 수 있는 자금으로만 거래하세요. 전체 [면책 조항](../../../DISCLAIMER.md)을 확인하세요.</sub>
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
<p align="center"><strong>При поддержке <a href="https://vergex.trade">vergex.trade</a></strong></p>
|
||||
|
||||
<h1 align="center">NOFX</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>AI-терминал для глобальных рынков.</strong><br/>
|
||||
<strong>Исследования, генерация стратегий, исполнение и мониторинг для акций США, сырьевых товаров, FX и криптоактивов.</strong>
|
||||
<img src="../../assets/nofx-banner.svg" alt="NOFX — AI trading terminal" width="100%"/>
|
||||
</p>
|
||||
|
||||
<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://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-E0483B?style=flat-square&labelColor=1A1813" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/telegram-community-E0483B?style=flat-square&labelColor=1A1813&logo=telegram&logoColor=white" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -29,223 +21,137 @@
|
||||
<a href="../vi/README.md">Tiếng Việt</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
NOFX — open-source AI-терминал для активных трейдеров, которым нужен единый рабочий контур для анализа рынков, разработки стратегий, исполнения сделок и мониторинга портфеля.
|
||||
NOFX — торговый терминал с открытым исходным кодом, где стратегией выступает языковая модель. Каждый трейдер работает в непрерывном цикле — читает структуру рынка, принимает решение, исполняет его и записывает ход рассуждений, — а рантайм на Go прижимает каждый ордер к жёстким риск-лимитам, которые модель не может обойти.
|
||||
|
||||
Продукт ориентирован на ликвидные глобальные рынки: акции США, товарные контракты, валютные пары и цифровые активы. AI-слой превращает торговое намерение в списки наблюдения, сигналы, стратегическую логику, риск-контроль и рабочие процессы исполнения.
|
||||
Трейдеры собираются свободно: любая модель, любая из девяти бирж, любая стратегия. Запускайте несколько параллельно и сравнивайте их в публичной таблице лидеров по реализованной доходности. Всё работает на вашей собственной машине; биржевые ключи шифруются при хранении и никогда её не покидают.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
**http://127.0.0.1:3000** 을 엽니다.
|
||||
Терминал откроется по адресу `http://127.0.0.1:3000`.
|
||||
|
||||
---
|
||||
**Первый запуск**
|
||||
|
||||
1. Зарегистрируйтесь — первый аккаунт становится владельцем экземпляра.
|
||||
2. Пройдите пошаговый запуск: положите **$1+ USDC** (сеть Base) в кошелёк для оплаты AI, который он создаст для вас, затем подключите Hyperliquid и внесите **$12+ USDC** для торговли.
|
||||
3. Запустите **Autopilot**. AI сканирует рынок каждые несколько минут и торгует самостоятельно; каждое решение появляется на дашборде в момент принятия. Остановить можно в любой момент одним кликом.
|
||||
|
||||
<br/>
|
||||
|
||||
## Регистрация на биржах
|
||||
|
||||
Используйте ссылки ниже для открытия торговых счетов на крипторынках и поддерживаемых рынках деривативов на акции США, FX и сырьевые товары. Эти маршруты относятся к партнерским программам NOFX и могут включать скидки на комиссии или реферальные преимущества.
|
||||
NOFX бесплатен и открыт. Открытие счёта по партнёрским ссылкам ниже даёт сниженные торговые комиссии и поддерживает дальнейшую разработку.
|
||||
|
||||
| Биржа | Статус | Регистрация со скидкой |
|
||||
| :--- | :---: | :--- |
|
||||
| <img src="../../../web/public/exchange-icons/binance.jpg" width="20" height="20" style="vertical-align: middle;"/> **Binance** | ✅ | [Регистрация](https://www.binance.com/join?ref=NOFXENG) |
|
||||
| <img src="../../../web/public/exchange-icons/bybit.png" width="20" height="20" style="vertical-align: middle;"/> **Bybit** | ✅ | [Регистрация](https://partner.bybit.com/b/83856) |
|
||||
| <img src="../../../web/public/exchange-icons/okx.svg" width="20" height="20" style="vertical-align: middle;"/> **OKX** | ✅ | [Регистрация](https://www.okx.com/join/1865360) |
|
||||
| <img src="../../../web/public/exchange-icons/hyperliquid.png" width="20" height="20" style="vertical-align: middle;"/> **Hyperliquid** | ✅ | [Регистрация](https://app.hyperliquid.xyz/join/AITRADING) |
|
||||
| <img src="../../../web/public/exchange-icons/bitget.svg" width="20" height="20" style="vertical-align: middle;"/> **Bitget** | ✅ | [Регистрация](https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172) |
|
||||
| <img src="../../../web/public/exchange-icons/kucoin.svg" width="20" height="20" style="vertical-align: middle;"/> **KuCoin** | ✅ | [Регистрация](https://www.kucoin.com/r/broker/CXEV7XKK) |
|
||||
| <img src="../../../web/public/exchange-icons/gate.svg" width="20" height="20" style="vertical-align: middle;"/> **Gate** | ✅ | [Регистрация](https://www.gatenode.xyz/share/VQBGUAxY) |
|
||||
| <img src="../../../web/public/exchange-icons/aster.svg" width="20" height="20" style="vertical-align: middle;"/> **Aster** | ✅ | [Регистрация](https://www.asterdex.com/en/referral/fdfc0e) |
|
||||
| <img src="../../../web/public/exchange-icons/lighter.png" width="20" height="20" style="vertical-align: middle;"/> **Lighter** | ✅ | [Регистрация](https://app.lighter.xyz/?referral=68151432) |
|
||||
| Биржа | Статус | Регистрация со скидкой на комиссии |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------- | :----: | :---------------------------------------------------------------------------------- |
|
||||
| <img src="../../../web/public/exchange-icons/binance.jpg" width="20" height="20" style="vertical-align: middle;"/> **Binance** | ✅ | [Зарегистрироваться](https://www.binance.com/join?ref=NOFXENG) |
|
||||
| <img src="../../../web/public/exchange-icons/bybit.png" width="20" height="20" style="vertical-align: middle;"/> **Bybit** | ✅ | [Зарегистрироваться](https://partner.bybit.com/b/83856) |
|
||||
| <img src="../../../web/public/exchange-icons/okx.svg" width="20" height="20" style="vertical-align: middle;"/> **OKX** | ✅ | [Зарегистрироваться](https://www.okx.com/join/1865360) |
|
||||
| <img src="../../../web/public/exchange-icons/hyperliquid.png" width="20" height="20" style="vertical-align: middle;"/> **Hyperliquid** | ✅ | [Зарегистрироваться](https://app.hyperliquid.xyz/join/AITRADING) |
|
||||
| <img src="../../../web/public/exchange-icons/bitget.svg" width="20" height="20" style="vertical-align: middle;"/> **Bitget** | ✅ | [Зарегистрироваться](https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172) |
|
||||
| <img src="../../../web/public/exchange-icons/kucoin.svg" width="20" height="20" style="vertical-align: middle;"/> **KuCoin** | ✅ | [Зарегистрироваться](https://www.kucoin.com/r/broker/CXEV7XKK) |
|
||||
| <img src="../../../web/public/exchange-icons/gate.svg" width="20" height="20" style="vertical-align: middle;"/> **Gate** | ✅ | [Зарегистрироваться](https://www.gatenode.xyz/share/VQBGUAxY) |
|
||||
| <img src="../../../web/public/exchange-icons/aster.svg" width="20" height="20" style="vertical-align: middle;"/> **Aster** | ✅ | [Зарегистрироваться](https://www.asterdex.com/en/referral/fdfc0e) |
|
||||
| <img src="../../../web/public/exchange-icons/lighter.png" width="20" height="20" style="vertical-align: middle;"/> **Lighter** | ✅ | [Зарегистрироваться](https://app.lighter.xyz/?referral=68151432) |
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Короткая демонстрация
|
||||
## Демо
|
||||
|
||||
https://github.com/user-attachments/assets/3310f495-14c5-4586-a1cc-3d32e44aa505
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Рынки
|
||||
## Модель предполагает. Рантайм располагает.
|
||||
|
||||
**Акции США · Сырьевые товары · FX · Криптоактивы**
|
||||
Решения принимает языковая модель, читающая стек данных [Claw402.ai](https://claw402.ai) · Vergex: живой сигнальный борд, ранжирующий каждый рынок по направлению и силе сигнала, глубокие сигналы Signal Lab по каждому инструменту, тепловые карты себестоимости и ликвидаций, показывающие, где у толпы «топливо» и «стены», и чистый поток средств в реальном времени — всё это сверяется с сырыми свечами и собственным живым трек-рекордом трейдера. Исполнение — нет.
|
||||
|
||||
NOFX организует исследования, построение стратегий, исполнение и мониторинг вокруг мультиактивных рабочих процессов, а не вокруг одного торгового экрана.
|
||||
Каждый ордер проходит через лимиты, зашитые в код и недоступные модели:
|
||||
|
||||
---
|
||||
| | |
|
||||
| :----------------------- | :--------------------------------------------------------------------------------- |
|
||||
| Лимиты позиций | Максимум одновременных позиций, номинал ограничен долей от капитала, одна позиция на инструмент |
|
||||
| Ограничение плеча | Жёсткие пределы применяются при расчёте размера ордера, независимо от того, что запросила модель |
|
||||
| Защита на стороне биржи | Стоп-лосс и тейк-профит выставляются на бирже сразу после каждого входа |
|
||||
| Автозакрытие по просадке | Прибыльные позиции, отдавшие слишком много от своего пика, закрываются |
|
||||
| Троттлинг сделок | Минимальное время удержания, кулдауны повторного входа по инструменту, лимиты входов на цикл и на час |
|
||||
| Безопасный режим | Повторяющиеся сбои модели блокируют новые входы, пока модель не восстановится |
|
||||
| Предстартовая проверка | Доступ к модели, средства в кошельке, стратегия и балансы бирж проверяются до того, как трейдер сможет стартовать |
|
||||
|
||||
## Доступ к AI-моделям
|
||||
Каждое решение сохраняется вместе с полным ходом рассуждений модели. Ни одной позиции без документального следа.
|
||||
|
||||
NOFX автоматически маршрутизирует AI-инференс через [Claw402](https://claw402.ai). Пользователям не нужно настраивать провайдеров моделей, управлять API-ключами или поддерживать отдельные AI-аккаунты. Терминал обращается к поддерживаемым моделям по требованию через pay-as-you-go инфраструктуру Claw402 и официальный канал со скидкой.
|
||||
<br/>
|
||||
|
||||
| Провайдер | Доступ |
|
||||
## Терминал
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| **Claw402** | [Доступ к AI-моделям по мере использования с официальной скидкой](https://claw402.ai) |
|
||||
|
||||
---
|
||||
|
||||
## Возможности
|
||||
|
||||
| Возможность | Описание |
|
||||
| :--- | :--- |
|
||||
| **AI trading terminal** | Единое рабочее пространство для акций США, сырья, FX и криптоактивов |
|
||||
| **Доступ к AI-моделям** | Автоматический доступ к поддерживаемым провайдерам через Claw402 |
|
||||
| **Подключение бирж** | Binance, Bybit, OKX, Hyperliquid, Bitget, KuCoin, Gate, Aster, Lighter |
|
||||
| **Strategy Studio** | Рыночные универсумы, индикаторы, риск-контроль и логика стратегий |
|
||||
| **Соревнование моделей** | Сравнение AI-трейдеров по live-результатам и таблице лидеров |
|
||||
| **Telegram Agent** | Управление и мониторинг ассистента через чат |
|
||||
| **Портфельный дашборд** | Позиции, P/L, история исполнения и логи решений модели |
|
||||
|
||||
---
|
||||
|
||||
## Скриншоты
|
||||
| **Autopilot** | Пошаговый запуск: пополнить, подключить, внести депозит, стартовать — с серверной предстартовой проверкой на каждом шаге |
|
||||
| **Strategy Studio** | Пресеты стилей, наборы монет, индикаторы, плечо, порог уверенности для входа, собственные промпты |
|
||||
| **Competition** | Публичная таблица лидеров по реализованной доходности, каждая запись привязана к своей модели |
|
||||
| **Dashboard** | Живые позиции, ордера, статистика и обоснование каждого решения |
|
||||
|
||||
<details>
|
||||
<summary><b>Страница настроек</b></summary>
|
||||
<summary>Скриншоты</summary>
|
||||
|
||||
| Конфигурация | Список трейдеров |
|
||||
| :----------------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/config-ai-exchanges.png" width="400"/> | <img src="../../../screenshots/config-traders-list.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Дашборд</b></summary>
|
||||
<br/>
|
||||
|
||||
| Обзор | График рынка |
|
||||
| :-----------------------------------------------------: | :-------------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-page.png" width="400"/> | <img src="../../../screenshots/dashboard-market-chart.png" width="400"/> |
|
||||
|
||||
| Статистика торгов | История позиций |
|
||||
| Статистика торговли | История позиций |
|
||||
| :--------------------------------------------------------------: | :-----------------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-trading-stats.png" width="400"/> | <img src="../../../screenshots/dashboard-position-history.png" width="400"/> |
|
||||
|
||||
| Позиции | Детали трейдера |
|
||||
| :----------------------------------------------------------: | :---------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-positions.png" width="400"/> | <img src="../../../screenshots/details-page.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Strategy Studio</b></summary>
|
||||
|
||||
| Редактор стратегий | Настройка индикаторов |
|
||||
| :------------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/strategy-studio.png" width="400"/> | <img src="../../../screenshots/strategy-indicators.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Соревнование</b></summary>
|
||||
|
||||
| Режим соревнования |
|
||||
| :-------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/competition-page.png" width="400"/> |
|
||||
| Соревнование | Конфигурация |
|
||||
| :-------------------------------------------------------: | :-----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/competition-page.png" width="400"/> | <img src="../../../screenshots/config-ai-exchanges.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Установка
|
||||
## Модели
|
||||
|
||||
### Linux / macOS
|
||||
Восемь провайдеров с вашими собственными ключами — DeepSeek, OpenAI, Claude, Qwen, Gemini, Grok, Kimi, MiniMax — включая пользовательские эндпоинты и имена моделей.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
Или вовсе без ключей: [Claw402](https://claw402.ai) тарифицирует использование моделей за каждый вызов в USDC по протоколу x402. Кошелёк в сети Base заменяет все API-ключи.
|
||||
|
||||
### Railway (облако)
|
||||
| Провайдер | Доступ |
|
||||
| :------- | :----- |
|
||||
| **Claw402** | [AI-модели с оплатой по мере использования и официальной скидкой](https://claw402.ai) |
|
||||
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
## Рынки
|
||||
|
||||
### Docker
|
||||
Криптовалютные бессрочные контракты на всех девяти биржах. На Hyperliquid тот же рантайм торгует также токенизированными акциями США, сырьевыми товарами, индексами, валютными парами и pre-IPO перпетуалами — TSLA, NVDA, GOLD, SPX, EUR, OPENAI — наряду с криптовалютой.
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Установите [Docker Desktop](https://www.docker.com/products/docker-desktop/), затем:
|
||||
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Сборка из исходников
|
||||
|
||||
```bash
|
||||
# Prerequisites: Go 1.21+, Node.js 18+, TA-Lib
|
||||
# macOS: brew install ta-lib
|
||||
# Ubuntu: sudo apt-get install libta-lib0-dev
|
||||
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx
|
||||
cd web && npm install && npm run dev
|
||||
```
|
||||
|
||||
### Обновление
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Настройка
|
||||
|
||||
**Режим для новичков**: пошаговый onboarding проводит через доступ к моделям, подключение биржи, настройку стратегии и первый запуск.
|
||||
|
||||
**Продвинутый режим**:
|
||||
|
||||
1. Настройте доступ к AI-моделям
|
||||
2. Подключите учетные данные биржи
|
||||
3. Создайте или импортируйте стратегию
|
||||
4. Создайте профиль AI-трейдера
|
||||
5. Запустите, мониторьте и улучшайте через дашборд
|
||||
|
||||
Все настройки доступны в Web UI по адресу **http://127.0.0.1:3000**.
|
||||
|
||||
---
|
||||
|
||||
## Развёртывание на сервере
|
||||
|
||||
**HTTP-развёртывание:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# Доступ через http://YOUR_IP:3000
|
||||
```
|
||||
|
||||
**HTTPS через Cloudflare:**
|
||||
|
||||
1. Добавьте домен в [Cloudflare](https://dash.cloudflare.com) (бесплатный план)
|
||||
2. A-запись → IP вашего сервера (Proxied)
|
||||
3. SSL/TLS → Flexible
|
||||
4. Установите `TRANSPORT_ENCRYPTION=true` в `.env`
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
NOFX
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Trading Terminal │
|
||||
│ React + TypeScript + TradingView │
|
||||
│ US Stocks · Commodities · Forex · Crypto │
|
||||
│ React · TypeScript · TradingView │
|
||||
│ Dashboard · Strategy Studio · Competition │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ API Server (Go) │
|
||||
│ JWT auth · encrypted credential store │
|
||||
├──────────────┬──────────────┬───────────────────┤
|
||||
│ Strategy │ Telegram │ Trader Runtime │
|
||||
│ Engine │ Agent │ Risk Controls │
|
||||
│ Strategy │ Autopilot │ Trader Runtime │
|
||||
│ Engine │ Preflight │ Risk Engine │
|
||||
├──────────────┴──────────────┴───────────────────┤
|
||||
│ AI Model Layer │
|
||||
│ Unified provider access through Claw402 │
|
||||
│ Model routing · payment · execution support │
|
||||
│ DeepSeek · OpenAI · Claude · Qwen · Gemini │
|
||||
│ Grok · Kimi · MiniMax · Claw402 (x402 USDC) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Exchange Connectivity │
|
||||
│ Binance · Bybit · OKX · Hyperliquid · Bitget │
|
||||
@@ -253,50 +159,99 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Установка
|
||||
|
||||
**Linux / macOS**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
**Railway**
|
||||
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
|
||||
**Docker**
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**Windows** — установите [Docker Desktop](https://www.docker.com/products/docker-desktop/), затем:
|
||||
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**Из исходников** — Go 1.21+, Node.js 18+:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx # backend
|
||||
cd web && npm install && npm run dev # frontend, in a second terminal
|
||||
```
|
||||
|
||||
**Обновление** — повторно запустите скрипт установки; он обновит всё на месте.
|
||||
|
||||
<details>
|
||||
<summary>Развёртывание на сервере</summary>
|
||||
|
||||
<br/>
|
||||
|
||||
**HTTP**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# http://YOUR_IP:3000
|
||||
```
|
||||
|
||||
**HTTPS через Cloudflare**
|
||||
|
||||
1. Добавьте домен в [Cloudflare](https://dash.cloudflare.com) (бесплатный план)
|
||||
2. A-запись → IP сервера, с проксированием
|
||||
3. SSL/TLS → Flexible
|
||||
4. `TRANSPORT_ENCRYPTION=true` в `.env`
|
||||
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
|
||||
## Документация
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| [Архитектура](../../architecture/README.md) | Дизайн системы и индекс модулей |
|
||||
| [Модуль стратегий](../../architecture/STRATEGY_MODULE.md) | Выбор инструментов, AI-промпты, исполнение |
|
||||
| [FAQ](../../faq/README.md) | Частые вопросы |
|
||||
| [Начало работы](../../getting-started/README.md) | Гайд по деплою |
|
||||
| | |
|
||||
| :------------------------------------------------------ | :------------------------------------ |
|
||||
| [Начало работы](../../getting-started/README.md) | Гайды по развёртыванию и биржевым API |
|
||||
| [Архитектура](../../architecture/README.md) | Устройство системы и индекс модулей |
|
||||
| [Модуль стратегий](../../architecture/STRATEGY_MODULE.md) | Выбор монет, AI-промпты, исполнение |
|
||||
| [FAQ](../../guides/faq.en.md) | Частые вопросы |
|
||||
| [Устранение неполадок](../../guides/TROUBLESHOOTING.md) | Диагностика типичных проблем |
|
||||
|
||||
---
|
||||
## Сообщество
|
||||
|
||||
## Участие
|
||||
[Telegram](https://t.me/nofx_dev_community) · [Twitter/X](https://x.com/vergex_ai) · [Issues](https://github.com/NoFxAiOS/nofx/issues) · [vergex.trade](https://vergex.trade) · [Живой дашборд](https://vergex.trade/explore)
|
||||
|
||||
См. [Contributing Guide](../../../CONTRIBUTING.md), [Code of Conduct](../../../CODE_OF_CONDUCT.md) и [Security Policy](../../../SECURITY.md).
|
||||
## Участие в разработке
|
||||
|
||||
### Программа для контрибьюторов
|
||||
Код, документация, переводы и сообщения об ошибках — всё это приветствуется; см. [руководство для контрибьюторов](../../../CONTRIBUTING.md), [кодекс поведения](../../../CODE_OF_CONDUCT.md) и [политику безопасности](../../../SECURITY.md).
|
||||
|
||||
NOFX отслеживает значимые вклады и планирует вознаграждать контрибьюторов по мере роста экосистемы. Приоритетные issues имеют более высокий вес награды.
|
||||
NOFX отслеживает значимые вклады и намерен вознаграждать контрибьюторов по мере роста экосистемы. Приоритетные issues имеют больший вес.
|
||||
|
||||
| Contribution | Weight |
|
||||
| :--- | :---: |
|
||||
| Pinned Issue PRs | ★★★★★★ |
|
||||
| Code (Merged PRs) | ★★★★★ |
|
||||
| Bug Fixes | ★★★★ |
|
||||
| Feature Ideas | ★★★ |
|
||||
| Bug Reports | ★★ |
|
||||
| Documentation | ★★ |
|
||||
| Вклад | Вес |
|
||||
| :---------------- | :----: |
|
||||
| PR по закреплённым issues | ★★★★★★ |
|
||||
| Код (принятые PR) | ★★★★★ |
|
||||
| Исправления ошибок | ★★★★ |
|
||||
| Идеи новых функций | ★★★ |
|
||||
| Сообщения об ошибках | ★★ |
|
||||
| Документация | ★★ |
|
||||
|
||||
---
|
||||
|
||||
## Ссылки
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| Website | [vergex.trade](https://vergex.trade) |
|
||||
| Dashboard | [vergex.trade/explore](https://vergex.trade/explore) |
|
||||
| Telegram | [nofx_dev_community](https://t.me/nofx_dev_community) |
|
||||
| Twitter | [@vergex_ai](https://x.com/vergex_ai) |
|
||||
|
||||
> **Предупреждение о рисках**: автоматическая торговля связана со значительным риском. Контролируйте размер позиции, понимайте устройство каждой площадки и не торгуйте средствами, потерю которых не можете себе позволить.
|
||||
|
||||
---
|
||||
<a href="https://github.com/NoFxAiOS/nofx/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=NoFxAiOS/nofx" alt="Contributors"/>
|
||||
</a>
|
||||
|
||||
## Спонсоры
|
||||
|
||||
@@ -314,8 +269,14 @@ NOFX отслеживает значимые вклады и планирует
|
||||
|
||||
[Стать спонсором](https://github.com/sponsors/NoFxAiOS)
|
||||
|
||||
## License
|
||||
<br/>
|
||||
|
||||
Если NOFX вам полезен, звезда помогает другим трейдерам его найти.
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
|
||||
## Лицензия
|
||||
|
||||
[AGPL-3.0](../../../LICENSE)
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
<sub>Автоматическая торговля сопряжена со значительным риском. Стратегии на основе AI экспериментальны и могут терять деньги. Разумно выбирайте размер позиций, разбирайтесь в устройстве каждой площадки и никогда не торгуйте средствами, потерю которых не можете себе позволить. Полный [дисклеймер](../../../DISCLAIMER.md).</sub>
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
<p align="center"><strong>За підтримки <a href="https://vergex.trade">vergex.trade</a></strong></p>
|
||||
|
||||
<h1 align="center">NOFX</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>AI-термінал для глобальних ринків.</strong><br/>
|
||||
<strong>Дослідження, генерація стратегій, виконання та моніторинг для акцій США, сировинних товарів, FX і криптоактивів.</strong>
|
||||
<img src="../../assets/nofx-banner.svg" alt="NOFX — AI trading terminal" width="100%"/>
|
||||
</p>
|
||||
|
||||
<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://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-E0483B?style=flat-square&labelColor=1A1813" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/telegram-community-E0483B?style=flat-square&labelColor=1A1813&logo=telegram&logoColor=white" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -29,223 +21,137 @@
|
||||
<a href="../vi/README.md">Tiếng Việt</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
NOFX — open-source AI-термінал для активних трейдерів, яким потрібен єдиний робочий простір для аналізу ринків, розробки стратегій, виконання угод і моніторингу портфеля.
|
||||
NOFX — торговий термінал з відкритим кодом, у якому стратегією є мовна модель. Кожен трейдер працює в безперервному циклі — читає структуру ринку, приймає рішення, виконує його, фіксує хід міркувань — а рантайм на Go обмежує кожен ордер жорсткими ризик-лімітами, які модель не може обійти.
|
||||
|
||||
Продукт орієнтований на ліквідні глобальні ринки: акції США, товарні контракти, валютні пари й цифрові активи. AI-шар перетворює торговий намір на watchlists, сигнали, логіку стратегії, ризик-контроль і робочі процеси виконання.
|
||||
Трейдери компонуються вільно: будь-яка модель, будь-яка з дев'яти бірж, будь-яка стратегія. Запускайте кілька одночасно та порівнюйте їх у публічній таблиці лідерів за реалізованою дохідністю. Усе працює на вашій власній машині; облікові дані бірж зашифровані на диску й ніколи її не покидають.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
**http://127.0.0.1:3000** 을 엽니다.
|
||||
Термінал відкривається за адресою `http://127.0.0.1:3000`.
|
||||
|
||||
---
|
||||
**Перший запуск**
|
||||
|
||||
1. Зареєструйтеся — перший акаунт стає власником інстансу.
|
||||
2. Пройдіть покроковий запуск: покладіть **$1+ USDC** (мережа Base) у гаманець для оплати AI, який він створить для вас, потім підключіть Hyperliquid і внесіть **$12+ USDC** для торгівлі.
|
||||
3. Запустіть **Autopilot**. AI сканує ринок кожні кілька хвилин і торгує самостійно; кожне рішення з'являється на дашборді в момент ухвалення. Зупинити його можна будь-коли одним кліком.
|
||||
|
||||
<br/>
|
||||
|
||||
## Реєстрація на біржах
|
||||
|
||||
Скористайтеся посиланнями нижче, щоб відкрити торгові акаунти для крипторинків і підтримуваних деривативів на акції США, FX та сировинні товари. Ці маршрути належать до партнерських програм NOFX і можуть містити знижки на комісії або реферальні переваги.
|
||||
NOFX безкоштовний і має відкритий код. Відкриття акаунта за партнерськими посиланнями нижче дає знижку на торгові комісії та фінансує подальшу розробку.
|
||||
|
||||
| Біржа | Статус | Реєстрація зі знижкою |
|
||||
| :--- | :---: | :--- |
|
||||
| <img src="../../../web/public/exchange-icons/binance.jpg" width="20" height="20" style="vertical-align: middle;"/> **Binance** | ✅ | [Реєстрація](https://www.binance.com/join?ref=NOFXENG) |
|
||||
| <img src="../../../web/public/exchange-icons/bybit.png" width="20" height="20" style="vertical-align: middle;"/> **Bybit** | ✅ | [Реєстрація](https://partner.bybit.com/b/83856) |
|
||||
| <img src="../../../web/public/exchange-icons/okx.svg" width="20" height="20" style="vertical-align: middle;"/> **OKX** | ✅ | [Реєстрація](https://www.okx.com/join/1865360) |
|
||||
| <img src="../../../web/public/exchange-icons/hyperliquid.png" width="20" height="20" style="vertical-align: middle;"/> **Hyperliquid** | ✅ | [Реєстрація](https://app.hyperliquid.xyz/join/AITRADING) |
|
||||
| <img src="../../../web/public/exchange-icons/bitget.svg" width="20" height="20" style="vertical-align: middle;"/> **Bitget** | ✅ | [Реєстрація](https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172) |
|
||||
| <img src="../../../web/public/exchange-icons/kucoin.svg" width="20" height="20" style="vertical-align: middle;"/> **KuCoin** | ✅ | [Реєстрація](https://www.kucoin.com/r/broker/CXEV7XKK) |
|
||||
| <img src="../../../web/public/exchange-icons/gate.svg" width="20" height="20" style="vertical-align: middle;"/> **Gate** | ✅ | [Реєстрація](https://www.gatenode.xyz/share/VQBGUAxY) |
|
||||
| <img src="../../../web/public/exchange-icons/aster.svg" width="20" height="20" style="vertical-align: middle;"/> **Aster** | ✅ | [Реєстрація](https://www.asterdex.com/en/referral/fdfc0e) |
|
||||
| <img src="../../../web/public/exchange-icons/lighter.png" width="20" height="20" style="vertical-align: middle;"/> **Lighter** | ✅ | [Реєстрація](https://app.lighter.xyz/?referral=68151432) |
|
||||
| Біржа | Статус | Реєстрація зі знижкою на комісії |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------- | :----: | :---------------------------------------------------------------------------------- |
|
||||
| <img src="../../../web/public/exchange-icons/binance.jpg" width="20" height="20" style="vertical-align: middle;"/> **Binance** | ✅ | [Реєстрація](https://www.binance.com/join?ref=NOFXENG) |
|
||||
| <img src="../../../web/public/exchange-icons/bybit.png" width="20" height="20" style="vertical-align: middle;"/> **Bybit** | ✅ | [Реєстрація](https://partner.bybit.com/b/83856) |
|
||||
| <img src="../../../web/public/exchange-icons/okx.svg" width="20" height="20" style="vertical-align: middle;"/> **OKX** | ✅ | [Реєстрація](https://www.okx.com/join/1865360) |
|
||||
| <img src="../../../web/public/exchange-icons/hyperliquid.png" width="20" height="20" style="vertical-align: middle;"/> **Hyperliquid** | ✅ | [Реєстрація](https://app.hyperliquid.xyz/join/AITRADING) |
|
||||
| <img src="../../../web/public/exchange-icons/bitget.svg" width="20" height="20" style="vertical-align: middle;"/> **Bitget** | ✅ | [Реєстрація](https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172) |
|
||||
| <img src="../../../web/public/exchange-icons/kucoin.svg" width="20" height="20" style="vertical-align: middle;"/> **KuCoin** | ✅ | [Реєстрація](https://www.kucoin.com/r/broker/CXEV7XKK) |
|
||||
| <img src="../../../web/public/exchange-icons/gate.svg" width="20" height="20" style="vertical-align: middle;"/> **Gate** | ✅ | [Реєстрація](https://www.gatenode.xyz/share/VQBGUAxY) |
|
||||
| <img src="../../../web/public/exchange-icons/aster.svg" width="20" height="20" style="vertical-align: middle;"/> **Aster** | ✅ | [Реєстрація](https://www.asterdex.com/en/referral/fdfc0e) |
|
||||
| <img src="../../../web/public/exchange-icons/lighter.png" width="20" height="20" style="vertical-align: middle;"/> **Lighter** | ✅ | [Реєстрація](https://app.lighter.xyz/?referral=68151432) |
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Коротка демонстрація
|
||||
## Демо
|
||||
|
||||
https://github.com/user-attachments/assets/3310f495-14c5-4586-a1cc-3d32e44aa505
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Ринки
|
||||
## Модель пропонує. Рантайм вирішує.
|
||||
|
||||
**Акції США · Сировинні товари · FX · Криптоактиви**
|
||||
Рішення приймає мовна модель, що читає стек даних [Claw402.ai](https://claw402.ai) · Vergex: живу сигнальну панель, яка ранжує кожен ринок за напрямком і силою сигналу, глибокі сигнали Signal Lab по кожному інструменту, теплові карти собівартості та ліквідацій, що показують, де в натовпу «паливо» і «стіни», та чистий потік коштів у реальному часі — усе це звіряється із сирими свічками та власним живим трек-рекордом трейдера. Виконання — ні.
|
||||
|
||||
NOFX организует исследования, построение стратегий, исполнение и мониторинг вокруг мультиактивных рабочих процессов, а не вокруг одного торгового экрана.
|
||||
Кожен ордер проходить через ліміти, зашиті в код поза досяжністю моделі:
|
||||
|
||||
---
|
||||
| | |
|
||||
| :----------------------- | :--------------------------------------------------------------------------------- |
|
||||
| Ліміти позицій | Максимум одночасних позицій, номінал обмежено часткою від капіталу, одна позиція на символ |
|
||||
| Обмеження плеча | Жорсткі стелі застосовуються під час розрахунку розміру ордера, незалежно від запиту моделі |
|
||||
| Захист на боці біржі | Стоп-лос і тейк-профіт розміщуються на біржі одразу після кожного входу |
|
||||
| Автозакриття за просадкою | Прибуткові позиції, що віддають надто багато від свого піку, закриваються |
|
||||
| Обмеження частоти угод | Мінімальний час утримання, кулдауни на повторний вхід за символом, ліміти входів на цикл і на годину |
|
||||
| Безпечний режим | Повторювані збої моделі блокують нові входи, доки модель не відновиться |
|
||||
| Передстартова перевірка | Доступ до моделі, кошти в гаманці, стратегія та баланси бірж перевіряються, перш ніж трейдер зможе стартувати |
|
||||
|
||||
## Доступ до AI-моделей
|
||||
Кожне рішення зберігається разом із повним ходом міркувань моделі. Жодної позиції без документального сліду.
|
||||
|
||||
NOFX автоматично маршрутизує AI inference через [Claw402](https://claw402.ai). Користувачам не потрібно налаштовувати провайдерів моделей, керувати API-ключами або підтримувати окремі AI-акаунти. Термінал звертається до підтримуваних моделей на вимогу через pay-as-you-go інфраструктуру Claw402 та офіційний канал зі знижкою.
|
||||
<br/>
|
||||
|
||||
| Провайдер | Доступ |
|
||||
## Термінал
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| **Claw402** | [Доступ до AI-моделей pay-as-you-go з офіційною знижкою](https://claw402.ai) |
|
||||
|
||||
---
|
||||
|
||||
## Можливості
|
||||
|
||||
| Возможность | Описание |
|
||||
| :--- | :--- |
|
||||
| **AI trading terminal** | Единое рабочее пространство для акций США, сырья, FX и криптоактивов |
|
||||
| **Доступ к AI-моделям** | Автоматический доступ к поддерживаемым провайдерам через Claw402 |
|
||||
| **Подключение бирж** | Binance, Bybit, OKX, Hyperliquid, Bitget, KuCoin, Gate, Aster, Lighter |
|
||||
| **Strategy Studio** | Рыночные универсумы, индикаторы, риск-контроль и логика стратегий |
|
||||
| **Соревнование моделей** | Сравнение AI-трейдеров по live-результатам и таблице лидеров |
|
||||
| **Telegram Agent** | Управление и мониторинг ассистента через чат |
|
||||
| **Портфельный дашборд** | Позиции, P/L, история исполнения и логи решений модели |
|
||||
|
||||
---
|
||||
|
||||
## Скріншоти
|
||||
| **Autopilot** | Покроковий запуск: поповнення, підключення, депозит, старт — із серверною передстартовою перевіркою на кожному кроці |
|
||||
| **Strategy Studio** | Пресети стилів, набори монет, індикатори, плече, впевненість входу, власні промпти |
|
||||
| **Змагання** | Публічна таблиця лідерів за реалізованою дохідністю; кожен запис прив'язаний до своєї моделі |
|
||||
| **Дашборд** | Живі позиції, ордери, статистика та обґрунтування кожного рішення |
|
||||
|
||||
<details>
|
||||
<summary><b>Сторінка налаштувань</b></summary>
|
||||
<summary>Скріншоти</summary>
|
||||
|
||||
| Конфігурація | Список трейдерів |
|
||||
| :----------------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/config-ai-exchanges.png" width="400"/> | <img src="../../../screenshots/config-traders-list.png" width="400"/> |
|
||||
<br/>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Дашборд</b></summary>
|
||||
|
||||
| Огляд | Графік ринку |
|
||||
| Огляд | Графік ринку |
|
||||
| :-----------------------------------------------------: | :-------------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-page.png" width="400"/> | <img src="../../../screenshots/dashboard-market-chart.png" width="400"/> |
|
||||
|
||||
| Статистика торгів | Історія позицій |
|
||||
| Статистика торгівлі | Історія позицій |
|
||||
| :--------------------------------------------------------------: | :-----------------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-trading-stats.png" width="400"/> | <img src="../../../screenshots/dashboard-position-history.png" width="400"/> |
|
||||
|
||||
| Позиції | Деталі трейдера |
|
||||
| :----------------------------------------------------------: | :---------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-positions.png" width="400"/> | <img src="../../../screenshots/details-page.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Strategy Studio</b></summary>
|
||||
|
||||
| Редактор стратегій | Налаштування індикаторів |
|
||||
| Редактор стратегій | Налаштування індикаторів |
|
||||
| :------------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/strategy-studio.png" width="400"/> | <img src="../../../screenshots/strategy-indicators.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Змагання</b></summary>
|
||||
|
||||
| Режим змагання |
|
||||
| :-------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/competition-page.png" width="400"/> |
|
||||
| Змагання | Конфігурація |
|
||||
| :-------------------------------------------------------: | :-----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/competition-page.png" width="400"/> | <img src="../../../screenshots/config-ai-exchanges.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Установка
|
||||
## Моделі
|
||||
|
||||
### Linux / macOS
|
||||
Вісім провайдерів із вашими власними ключами — DeepSeek, OpenAI, Claude, Qwen, Gemini, Grok, Kimi, MiniMax — включно з власними ендпоінтами та назвами моделей.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
Або взагалі без ключів: [Claw402](https://claw402.ai) тарифікує використання моделей за кожен виклик у USDC через протокол x402. Гаманець у мережі Base замінює всі API-ключі.
|
||||
|
||||
### Railway (облако)
|
||||
| Провайдер | Доступ |
|
||||
| :------- | :----- |
|
||||
| **Claw402** | [AI-моделі з оплатою за використання та офіційною знижкою](https://claw402.ai) |
|
||||
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
## Ринки
|
||||
|
||||
### Docker
|
||||
Криптовалютні безстрокові контракти на всіх дев'яти біржах. На Hyperliquid той самий рантайм також торгує токенізованими акціями США, сировинними товарами, індексами, валютами та pre-IPO перпетуалами — TSLA, NVDA, GOLD, SPX, EUR, OPENAI — поряд із криптою.
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Установите [Docker Desktop](https://www.docker.com/products/docker-desktop/), затем:
|
||||
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Сборка из исходников
|
||||
|
||||
```bash
|
||||
# Prerequisites: Go 1.21+, Node.js 18+, TA-Lib
|
||||
# macOS: brew install ta-lib
|
||||
# Ubuntu: sudo apt-get install libta-lib0-dev
|
||||
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx
|
||||
cd web && npm install && npm run dev
|
||||
```
|
||||
|
||||
### Обновление
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Настройка
|
||||
|
||||
**Режим для новичков**: пошаговый onboarding проводит через доступ к моделям, подключение биржи, настройку стратегии и первый запуск.
|
||||
|
||||
**Продвинутый режим**:
|
||||
|
||||
1. Настройте доступ к AI-моделям
|
||||
2. Подключите учетные данные биржи
|
||||
3. Создайте или импортируйте стратегию
|
||||
4. Создайте профиль AI-трейдера
|
||||
5. Запустите, мониторьте и улучшайте через дашборд
|
||||
|
||||
Все настройки доступны в Web UI по адресу **http://127.0.0.1:3000**.
|
||||
|
||||
---
|
||||
|
||||
## Розгортання на сервері
|
||||
|
||||
**HTTP-розгортання:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# Доступ через http://YOUR_IP:3000
|
||||
```
|
||||
|
||||
**HTTPS через Cloudflare:**
|
||||
|
||||
1. Додайте домен у [Cloudflare](https://dash.cloudflare.com) (безкоштовний план)
|
||||
2. A-запис → IP вашого сервера (Proxied)
|
||||
3. SSL/TLS → Flexible
|
||||
4. Встановіть `TRANSPORT_ENCRYPTION=true` у `.env`
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Архітектура
|
||||
|
||||
```
|
||||
NOFX
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Trading Terminal │
|
||||
│ React + TypeScript + TradingView │
|
||||
│ US Stocks · Commodities · Forex · Crypto │
|
||||
│ React · TypeScript · TradingView │
|
||||
│ Dashboard · Strategy Studio · Competition │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ API Server (Go) │
|
||||
│ JWT auth · encrypted credential store │
|
||||
├──────────────┬──────────────┬───────────────────┤
|
||||
│ Strategy │ Telegram │ Trader Runtime │
|
||||
│ Engine │ Agent │ Risk Controls │
|
||||
│ Strategy │ Autopilot │ Trader Runtime │
|
||||
│ Engine │ Preflight │ Risk Engine │
|
||||
├──────────────┴──────────────┴───────────────────┤
|
||||
│ AI Model Layer │
|
||||
│ Unified provider access through Claw402 │
|
||||
│ Model routing · payment · execution support │
|
||||
│ DeepSeek · OpenAI · Claude · Qwen · Gemini │
|
||||
│ Grok · Kimi · MiniMax · Claw402 (x402 USDC) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Exchange Connectivity │
|
||||
│ Binance · Bybit · OKX · Hyperliquid · Bitget │
|
||||
@@ -253,50 +159,99 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Документация
|
||||
## Встановлення
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| [Архитектура](../../architecture/README.md) | Дизайн системы и индекс модулей |
|
||||
| [Модуль стратегий](../../architecture/STRATEGY_MODULE.md) | Выбор инструментов, AI-промпты, исполнение |
|
||||
| [FAQ](../../faq/README.md) | Частые вопросы |
|
||||
| [Начало работы](../../getting-started/README.md) | Гайд по деплою |
|
||||
**Linux / macOS**
|
||||
|
||||
---
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
## Участие
|
||||
**Railway**
|
||||
|
||||
См. [Contributing Guide](../../../CONTRIBUTING.md), [Code of Conduct](../../../CODE_OF_CONDUCT.md) и [Security Policy](../../../SECURITY.md).
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
|
||||
### Программа для контрибьюторов
|
||||
**Docker**
|
||||
|
||||
NOFX отслеживает значимые вклады и планирует вознаграждать контрибьюторов по мере роста экосистемы. Приоритетные issues имеют более высокий вес награды.
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
| Contribution | Weight |
|
||||
| :--- | :---: |
|
||||
| Pinned Issue PRs | ★★★★★★ |
|
||||
| Code (Merged PRs) | ★★★★★ |
|
||||
| Bug Fixes | ★★★★ |
|
||||
| Feature Ideas | ★★★ |
|
||||
| Bug Reports | ★★ |
|
||||
| Documentation | ★★ |
|
||||
**Windows** — встановіть [Docker Desktop](https://www.docker.com/products/docker-desktop/), потім:
|
||||
|
||||
---
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
## Посилання
|
||||
**З вихідного коду** — Go 1.21+, Node.js 18+:
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| Website | [vergex.trade](https://vergex.trade) |
|
||||
| Dashboard | [vergex.trade/explore](https://vergex.trade/explore) |
|
||||
| Telegram | [nofx_dev_community](https://t.me/nofx_dev_community) |
|
||||
| Twitter | [@vergex_ai](https://x.com/vergex_ai) |
|
||||
```bash
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx # backend
|
||||
cd web && npm install && npm run dev # frontend, in a second terminal
|
||||
```
|
||||
|
||||
> **Попередження про ризики**: автоматизована торгівля має значний ризик. Контролюйте розмір позиції, розумійте механіку кожного майданчика і не торгуйте коштами, втрату яких не можете собі дозволити.
|
||||
**Оновлення** — запустіть інсталяційний скрипт ще раз; він оновить усе на місці.
|
||||
|
||||
---
|
||||
<details>
|
||||
<summary>Розгортання на сервері</summary>
|
||||
|
||||
<br/>
|
||||
|
||||
**HTTP**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# http://YOUR_IP:3000
|
||||
```
|
||||
|
||||
**HTTPS через Cloudflare**
|
||||
|
||||
1. Додайте домен у [Cloudflare](https://dash.cloudflare.com) (безкоштовний план)
|
||||
2. A-запис → IP сервера, проксійований
|
||||
3. SSL/TLS → Flexible
|
||||
4. `TRANSPORT_ENCRYPTION=true` у `.env`
|
||||
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
|
||||
## Документація
|
||||
|
||||
| | |
|
||||
| :------------------------------------------------------ | :------------------------------------ |
|
||||
| [Початок роботи](../../getting-started/README.md) | Посібники з розгортання та біржових API |
|
||||
| [Архітектура](../../architecture/README.md) | Дизайн системи та індекс модулів |
|
||||
| [Модуль стратегій](../../architecture/STRATEGY_MODULE.md) | Вибір монет, AI-промпти, виконання |
|
||||
| [FAQ](../../guides/faq.en.md) | Поширені запитання |
|
||||
| [Усунення несправностей](../../guides/TROUBLESHOOTING.md) | Діагностика типових проблем |
|
||||
|
||||
## Спільнота
|
||||
|
||||
[Telegram](https://t.me/nofx_dev_community) · [Twitter/X](https://x.com/vergex_ai) · [Issues](https://github.com/NoFxAiOS/nofx/issues) · [vergex.trade](https://vergex.trade) · [Живий дашборд](https://vergex.trade/explore)
|
||||
|
||||
## Участь у розробці
|
||||
|
||||
Код, документація, переклади та звіти про помилки — усе це вітається; див. [Посібник для контриб'юторів](../../../CONTRIBUTING.md), [Кодекс поведінки](../../../CODE_OF_CONDUCT.md) і [Політику безпеки](../../../SECURITY.md).
|
||||
|
||||
NOFX відстежує значущі внески й має намір винагороджувати контриб'юторів у міру зростання екосистеми. Пріоритетні issues мають більшу вагу.
|
||||
|
||||
| Внесок | Вага |
|
||||
| :---------------- | :----: |
|
||||
| PR до закріплених issues | ★★★★★★ |
|
||||
| Код (змерджені PR) | ★★★★★ |
|
||||
| Виправлення багів | ★★★★ |
|
||||
| Ідеї функцій | ★★★ |
|
||||
| Звіти про баги | ★★ |
|
||||
| Документація | ★★ |
|
||||
|
||||
<a href="https://github.com/NoFxAiOS/nofx/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=NoFxAiOS/nofx" alt="Contributors"/>
|
||||
</a>
|
||||
|
||||
## Спонсори
|
||||
|
||||
@@ -314,8 +269,14 @@ NOFX отслеживает значимые вклады и планирует
|
||||
|
||||
[Стати спонсором](https://github.com/sponsors/NoFxAiOS)
|
||||
|
||||
## License
|
||||
<br/>
|
||||
|
||||
Якщо NOFX вам корисний, зірка допоможе іншим трейдерам його знайти.
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
|
||||
## Ліцензія
|
||||
|
||||
[AGPL-3.0](../../../LICENSE)
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
<sub>Автоматизована торгівля пов'язана зі значним ризиком. Стратегії на основі AI експериментальні й можуть втрачати гроші. Обирайте розмір позицій розважливо, розумійте кожен майданчик і ніколи не торгуйте коштами, втрату яких не можете собі дозволити. Повний [дисклеймер](../../../DISCLAIMER.md).</sub>
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
<p align="center"><strong>Được hỗ trợ bởi <a href="https://vergex.trade">vergex.trade</a></strong></p>
|
||||
|
||||
<h1 align="center">NOFX</h1>
|
||||
<p align="center"><strong>Được hậu thuẫn bởi <a href="https://vergex.trade">vergex.trade</a></strong></p>
|
||||
|
||||
<p align="center">
|
||||
<strong>Thiết bị đầu cuối giao dịch AI cho thị trường toàn cầu.</strong><br/>
|
||||
<strong>Nghiên cứu, tạo chiến lược, thực thi và giám sát cho cổ phiếu Mỹ, hàng hóa, ngoại hối và crypto.</strong>
|
||||
<img src="../../assets/nofx-banner.svg" alt="NOFX — AI trading terminal" width="100%"/>
|
||||
</p>
|
||||
|
||||
<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://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-E0483B?style=flat-square&labelColor=1A1813" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/telegram-community-E0483B?style=flat-square&labelColor=1A1813&logo=telegram&logoColor=white" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -29,89 +21,83 @@
|
||||
<a href="README.md">Tiếng Việt</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
NOFX là thiết bị đầu cuối giao dịch AI mã nguồn mở cho các trader cần một không gian làm việc thống nhất để nghiên cứu thị trường, phát triển chiến lược, thực thi giao dịch và giám sát danh mục.
|
||||
NOFX là một terminal giao dịch mã nguồn mở, nơi chiến lược chính là một mô hình ngôn ngữ. Mỗi trader chạy một vòng lặp liên tục — đọc cấu trúc thị trường, ra quyết định, thực thi, ghi lại lập luận — trong khi một runtime viết bằng Go ép mọi lệnh vào các giới hạn rủi ro cứng mà mô hình không thể vượt qua.
|
||||
|
||||
Sản phẩm tập trung vào các thị trường thanh khoản toàn cầu: cổ phiếu Mỹ, hợp đồng hàng hóa, cặp FX và tài sản số. Lớp AI chuyển ý định giao dịch thành watchlist, tín hiệu, logic chiến lược, kiểm soát rủi ro và workflow thực thi.
|
||||
Trader được kết hợp tự do: bất kỳ mô hình nào, bất kỳ sàn nào trong chín sàn, bất kỳ chiến lược nào. Chạy nhiều trader song song và so sánh chúng trên bảng xếp hạng công khai theo lợi nhuận đã thực hiện. Mọi thứ chạy trên máy của chính bạn; thông tin xác thực sàn được mã hóa khi lưu trữ và không bao giờ rời khỏi máy.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
**http://127.0.0.1:3000** 을 엽니다.
|
||||
Terminal mở tại `http://127.0.0.1:3000`.
|
||||
|
||||
---
|
||||
**Lần chạy đầu tiên**
|
||||
|
||||
1. Đăng ký — tài khoản đầu tiên trở thành chủ sở hữu của instance.
|
||||
2. Làm theo quy trình khởi chạy có hướng dẫn: nạp **$1+ USDC** (mạng Base) vào ví phí AI được tạo sẵn cho bạn, sau đó kết nối Hyperliquid và nạp **$12+ USDC** để giao dịch.
|
||||
3. Khởi động **Autopilot**. AI quét thị trường vài phút một lần và tự mình giao dịch; mọi quyết định hiện lên dashboard ngay khi diễn ra. Dừng lại bất cứ lúc nào chỉ với một cú nhấp.
|
||||
|
||||
<br/>
|
||||
|
||||
## Đăng ký sàn giao dịch
|
||||
|
||||
Sử dụng các liên kết bên dưới để mở tài khoản giao dịch cho crypto và các thị trường phái sinh cổ phiếu Mỹ, FX, hàng hóa được hỗ trợ. Các tuyến này thuộc chương trình đối tác NOFX và có thể bao gồm ưu đãi phí hoặc quyền lợi giới thiệu.
|
||||
NOFX miễn phí và mã nguồn mở. Mở tài khoản qua các liên kết đối tác bên dưới sẽ được giảm phí giao dịch và góp phần tài trợ cho việc phát triển lâu dài.
|
||||
|
||||
| Sàn | Trạng thái | Đăng ký kèm ưu đãi phí |
|
||||
| :--- | :---: | :--- |
|
||||
| <img src="../../../web/public/exchange-icons/binance.jpg" width="20" height="20" style="vertical-align: middle;"/> **Binance** | ✅ | [Đăng ký](https://www.binance.com/join?ref=NOFXENG) |
|
||||
| <img src="../../../web/public/exchange-icons/bybit.png" width="20" height="20" style="vertical-align: middle;"/> **Bybit** | ✅ | [Đăng ký](https://partner.bybit.com/b/83856) |
|
||||
| <img src="../../../web/public/exchange-icons/okx.svg" width="20" height="20" style="vertical-align: middle;"/> **OKX** | ✅ | [Đăng ký](https://www.okx.com/join/1865360) |
|
||||
| <img src="../../../web/public/exchange-icons/hyperliquid.png" width="20" height="20" style="vertical-align: middle;"/> **Hyperliquid** | ✅ | [Đăng ký](https://app.hyperliquid.xyz/join/AITRADING) |
|
||||
| <img src="../../../web/public/exchange-icons/bitget.svg" width="20" height="20" style="vertical-align: middle;"/> **Bitget** | ✅ | [Đăng ký](https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172) |
|
||||
| <img src="../../../web/public/exchange-icons/kucoin.svg" width="20" height="20" style="vertical-align: middle;"/> **KuCoin** | ✅ | [Đăng ký](https://www.kucoin.com/r/broker/CXEV7XKK) |
|
||||
| <img src="../../../web/public/exchange-icons/gate.svg" width="20" height="20" style="vertical-align: middle;"/> **Gate** | ✅ | [Đăng ký](https://www.gatenode.xyz/share/VQBGUAxY) |
|
||||
| <img src="../../../web/public/exchange-icons/aster.svg" width="20" height="20" style="vertical-align: middle;"/> **Aster** | ✅ | [Đăng ký](https://www.asterdex.com/en/referral/fdfc0e) |
|
||||
| <img src="../../../web/public/exchange-icons/lighter.png" width="20" height="20" style="vertical-align: middle;"/> **Lighter** | ✅ | [Đăng ký](https://app.lighter.xyz/?referral=68151432) |
|
||||
| Sàn giao dịch | Trạng thái | Đăng ký kèm ưu đãi phí |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------- | :----: | :---------------------------------------------------------------------------------- |
|
||||
| <img src="../../../web/public/exchange-icons/binance.jpg" width="20" height="20" style="vertical-align: middle;"/> **Binance** | ✅ | [Đăng ký](https://www.binance.com/join?ref=NOFXENG) |
|
||||
| <img src="../../../web/public/exchange-icons/bybit.png" width="20" height="20" style="vertical-align: middle;"/> **Bybit** | ✅ | [Đăng ký](https://partner.bybit.com/b/83856) |
|
||||
| <img src="../../../web/public/exchange-icons/okx.svg" width="20" height="20" style="vertical-align: middle;"/> **OKX** | ✅ | [Đăng ký](https://www.okx.com/join/1865360) |
|
||||
| <img src="../../../web/public/exchange-icons/hyperliquid.png" width="20" height="20" style="vertical-align: middle;"/> **Hyperliquid** | ✅ | [Đăng ký](https://app.hyperliquid.xyz/join/AITRADING) |
|
||||
| <img src="../../../web/public/exchange-icons/bitget.svg" width="20" height="20" style="vertical-align: middle;"/> **Bitget** | ✅ | [Đăng ký](https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172) |
|
||||
| <img src="../../../web/public/exchange-icons/kucoin.svg" width="20" height="20" style="vertical-align: middle;"/> **KuCoin** | ✅ | [Đăng ký](https://www.kucoin.com/r/broker/CXEV7XKK) |
|
||||
| <img src="../../../web/public/exchange-icons/gate.svg" width="20" height="20" style="vertical-align: middle;"/> **Gate** | ✅ | [Đăng ký](https://www.gatenode.xyz/share/VQBGUAxY) |
|
||||
| <img src="../../../web/public/exchange-icons/aster.svg" width="20" height="20" style="vertical-align: middle;"/> **Aster** | ✅ | [Đăng ký](https://www.asterdex.com/en/referral/fdfc0e) |
|
||||
| <img src="../../../web/public/exchange-icons/lighter.png" width="20" height="20" style="vertical-align: middle;"/> **Lighter** | ✅ | [Đăng ký](https://app.lighter.xyz/?referral=68151432) |
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Demo nhanh
|
||||
## Demo
|
||||
|
||||
https://github.com/user-attachments/assets/3310f495-14c5-4586-a1cc-3d32e44aa505
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Thị trường
|
||||
## Mô hình đề xuất. Runtime định đoạt.
|
||||
|
||||
**Cổ phiếu Mỹ · Hàng hóa · Ngoại hối · Crypto**
|
||||
Các quyết định đến từ một mô hình ngôn ngữ đọc bộ dữ liệu [Claw402.ai](https://claw402.ai) · Vergex: bảng tín hiệu trực tiếp xếp hạng mọi thị trường theo thiên hướng và độ mạnh tín hiệu, tín hiệu chuyên sâu Signal Lab cho từng mã, bản đồ nhiệt chi phí vốn và thanh lý cho thấy "nhiên liệu" và "bức tường" của đám đông nằm ở đâu, cùng dòng tiền ròng theo thời gian thực — tất cả được đối chiếu với nến thô và thành tích thực chiến của chính trader. Việc thực thi thì không.
|
||||
|
||||
NOFX tổ chức nghiên cứu, xây dựng chiến lược, thực thi và giám sát theo workflow đa tài sản thay vì một màn hình sàn đơn lẻ.
|
||||
Mọi lệnh đều phải đi qua các giới hạn được thực thi bằng code, nằm ngoài tầm với của mô hình:
|
||||
|
||||
---
|
||||
| | |
|
||||
| :----------------------- | :--------------------------------------------------------------------------------- |
|
||||
| Giới hạn vị thế | Số vị thế đồng thời tối đa, giá trị danh nghĩa bị chặn theo tỷ lệ vốn, mỗi symbol chỉ một vị thế |
|
||||
| Kẹp đòn bẩy | Trần cứng áp dụng ngay khi tính khối lượng lệnh, bất kể mô hình yêu cầu gì |
|
||||
| Bảo vệ phía sàn | Stop-loss và take-profit được đặt trên sàn ngay sau mỗi lần vào lệnh |
|
||||
| Tự đóng khi rút vốn từ đỉnh | Vị thế đang lãi nhưng để mất quá nhiều so với đỉnh sẽ bị đóng |
|
||||
| Điều tiết giao dịch | Thời gian giữ tối thiểu, thời gian chờ vào lại theo từng symbol, giới hạn số lần vào lệnh theo chu kỳ và theo giờ |
|
||||
| Chế độ an toàn | Mô hình lỗi liên tiếp sẽ bị chặn vào lệnh mới cho đến khi mô hình hồi phục |
|
||||
| Kiểm tra tiền khởi chạy | Quyền truy cập mô hình, tiền trong ví, chiến lược và số dư sàn được xác minh trước khi trader được phép khởi động |
|
||||
|
||||
## Truy cập mô hình AI
|
||||
Mỗi quyết định được lưu kèm toàn bộ lập luận của mô hình. Không có vị thế nào thiếu hồ sơ lưu vết.
|
||||
|
||||
NOFX tự động định tuyến AI inference qua [Claw402](https://claw402.ai). Người dùng không cần cấu hình nhà cung cấp mô hình, quản lý API key hoặc duy trì tài khoản AI riêng. Terminal truy cập các mô hình được hỗ trợ theo nhu cầu qua hạ tầng pay-as-you-go của Claw402 và định tuyến qua kênh ưu đãi chính thức.
|
||||
<br/>
|
||||
|
||||
| Nhà cung cấp | Truy cập |
|
||||
## Terminal
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| **Claw402** | [Truy cập mô hình AI pay-as-you-go với ưu đãi chính thức](https://claw402.ai) |
|
||||
|
||||
---
|
||||
|
||||
## Năng lực
|
||||
|
||||
| Năng lực | Mô tả |
|
||||
| :--- | :--- |
|
||||
| **AI trading terminal** | Không gian làm việc thống nhất cho cổ phiếu Mỹ, hàng hóa, ngoại hối và crypto |
|
||||
| **Truy cập mô hình AI** | Tự động kết nối nhà cung cấp được hỗ trợ qua Claw402 |
|
||||
| **Kết nối sàn** | Binance, Bybit, OKX, Hyperliquid, Bitget, KuCoin, Gate, Aster, Lighter |
|
||||
| **Strategy Studio** | Universe thị trường, chỉ báo, kiểm soát rủi ro và logic chiến lược |
|
||||
| **Cạnh tranh mô hình** | So sánh AI trader bằng hiệu suất live và bảng xếp hạng |
|
||||
| **Telegram Agent** | Điều khiển và giám sát trợ lý giao dịch qua chat |
|
||||
| **Dashboard danh mục** | Vị thế, P/L, lịch sử thực thi và log quyết định của mô hình |
|
||||
|
||||
---
|
||||
|
||||
## Ảnh chụp màn hình
|
||||
| **Autopilot** | Khởi chạy có hướng dẫn: nạp phí, kết nối, nạp tiền, khởi động — với kiểm tra preflight phía máy chủ xuyên suốt |
|
||||
| **Strategy Studio** | Preset phong cách, tập coin, chỉ báo, đòn bẩy, độ tin cậy vào lệnh, prompt tùy chỉnh |
|
||||
| **Cạnh tranh** | Bảng xếp hạng công khai theo lợi nhuận đã thực hiện, mỗi mục được ghi nhận cho mô hình tạo ra nó |
|
||||
| **Dashboard** | Vị thế trực tiếp, lệnh, thống kê và lập luận đằng sau mỗi quyết định |
|
||||
|
||||
<details>
|
||||
<summary><b>Trang cấu hình</b></summary>
|
||||
<summary>Ảnh chụp màn hình</summary>
|
||||
|
||||
| Cấu hình | Danh sách trader |
|
||||
| :----------------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/config-ai-exchanges.png" width="400"/> | <img src="../../../screenshots/config-traders-list.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Dashboard</b></summary>
|
||||
<br/>
|
||||
|
||||
| Tổng quan | Biểu đồ thị trường |
|
||||
| :-----------------------------------------------------: | :-------------------------------------------------------------: |
|
||||
@@ -121,131 +107,51 @@ NOFX tự động định tuyến AI inference qua [Claw402](https://claw402.ai)
|
||||
| :--------------------------------------------------------------: | :-----------------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-trading-stats.png" width="400"/> | <img src="../../../screenshots/dashboard-position-history.png" width="400"/> |
|
||||
|
||||
| Vị thế | Chi tiết trader |
|
||||
| :----------------------------------------------------------: | :---------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-positions.png" width="400"/> | <img src="../../../screenshots/details-page.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Strategy Studio</b></summary>
|
||||
|
||||
| Trình soạn chiến lược | Cấu hình chỉ báo |
|
||||
| :------------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/strategy-studio.png" width="400"/> | <img src="../../../screenshots/strategy-indicators.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Cạnh tranh</b></summary>
|
||||
|
||||
| Chế độ cạnh tranh |
|
||||
| :-------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/competition-page.png" width="400"/> |
|
||||
| Cạnh tranh | Cấu hình |
|
||||
| :-------------------------------------------------------: | :-----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/competition-page.png" width="400"/> | <img src="../../../screenshots/config-ai-exchanges.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Cài đặt
|
||||
## Mô hình
|
||||
|
||||
### Linux / macOS
|
||||
Tám nhà cung cấp với key của riêng bạn — DeepSeek, OpenAI, Claude, Qwen, Gemini, Grok, Kimi, MiniMax — bao gồm cả endpoint và tên mô hình tùy chỉnh.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
Hoặc không cần key nào cả: [Claw402](https://claw402.ai) tính phí sử dụng mô hình theo từng lần gọi bằng USDC qua giao thức x402. Một chiếc ví trên Base thay thế mọi API key.
|
||||
|
||||
### Railway (Cloud)
|
||||
| Nhà cung cấp | Truy cập |
|
||||
| :------- | :----- |
|
||||
| **Claw402** | [Mô hình AI trả theo mức dùng với ưu đãi chính thức](https://claw402.ai) |
|
||||
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
## Thị trường
|
||||
|
||||
### Docker
|
||||
Hợp đồng vĩnh cửu crypto trên cả chín sàn. Trên Hyperliquid, cùng một runtime còn giao dịch cổ phiếu Mỹ token hóa, hàng hóa, chỉ số, ngoại hối và hợp đồng vĩnh cửu pre-IPO — TSLA, NVDA, GOLD, SPX, EUR, OPENAI — song song với crypto.
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Cài [Docker Desktop](https://www.docker.com/products/docker-desktop/), sau đó:
|
||||
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Build từ source
|
||||
|
||||
```bash
|
||||
# Prerequisites: Go 1.21+, Node.js 18+, TA-Lib
|
||||
# macOS: brew install ta-lib
|
||||
# Ubuntu: sudo apt-get install libta-lib0-dev
|
||||
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx
|
||||
cd web && npm install && npm run dev
|
||||
```
|
||||
|
||||
### Cập nhật
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Thiết lập
|
||||
|
||||
**Chế độ người mới**: onboarding có hướng dẫn giúp người dùng mới hoàn tất truy cập mô hình, kết nối sàn, cấu hình chiến lược và lần triển khai đầu tiên.
|
||||
|
||||
**Chế độ nâng cao**:
|
||||
|
||||
1. Cấu hình truy cập mô hình AI
|
||||
2. Kết nối thông tin xác thực sàn
|
||||
3. Xây dựng hoặc import chiến lược
|
||||
4. Tạo hồ sơ AI trader
|
||||
5. Khởi chạy, giám sát và cải thiện từ dashboard
|
||||
|
||||
Tất cả cấu hình có trong Web UI tại **http://127.0.0.1:3000**.
|
||||
|
||||
---
|
||||
|
||||
## Triển khai lên máy chủ
|
||||
|
||||
**Triển khai HTTP:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# Truy cập qua http://YOUR_IP:3000
|
||||
```
|
||||
|
||||
**HTTPS qua Cloudflare:**
|
||||
|
||||
1. Thêm domain vào [Cloudflare](https://dash.cloudflare.com) (gói miễn phí)
|
||||
2. Bản ghi A → IP máy chủ của bạn (Proxied)
|
||||
3. SSL/TLS → Flexible
|
||||
4. Đặt `TRANSPORT_ENCRYPTION=true` trong `.env`
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Kiến trúc
|
||||
|
||||
```
|
||||
NOFX
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Trading Terminal │
|
||||
│ React + TypeScript + TradingView │
|
||||
│ US Stocks · Commodities · Forex · Crypto │
|
||||
│ React · TypeScript · TradingView │
|
||||
│ Dashboard · Strategy Studio · Competition │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ API Server (Go) │
|
||||
│ JWT auth · encrypted credential store │
|
||||
├──────────────┬──────────────┬───────────────────┤
|
||||
│ Strategy │ Telegram │ Trader Runtime │
|
||||
│ Engine │ Agent │ Risk Controls │
|
||||
│ Strategy │ Autopilot │ Trader Runtime │
|
||||
│ Engine │ Preflight │ Risk Engine │
|
||||
├──────────────┴──────────────┴───────────────────┤
|
||||
│ AI Model Layer │
|
||||
│ Unified provider access through Claw402 │
|
||||
│ Model routing · payment · execution support │
|
||||
│ DeepSeek · OpenAI · Claude · Qwen · Gemini │
|
||||
│ Grok · Kimi · MiniMax · Claw402 (x402 USDC) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Exchange Connectivity │
|
||||
│ Binance · Bybit · OKX · Hyperliquid · Bitget │
|
||||
@@ -253,50 +159,99 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## Cài đặt
|
||||
|
||||
**Linux / macOS**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
**Railway**
|
||||
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
|
||||
**Docker**
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**Windows** — cài [Docker Desktop](https://www.docker.com/products/docker-desktop/), sau đó:
|
||||
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**Từ mã nguồn** — Go 1.21+, Node.js 18+:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx # backend
|
||||
cd web && npm install && npm run dev # frontend, in a second terminal
|
||||
```
|
||||
|
||||
**Cập nhật** — chạy lại script cài đặt; nó tự nâng cấp tại chỗ.
|
||||
|
||||
<details>
|
||||
<summary>Triển khai lên máy chủ</summary>
|
||||
|
||||
<br/>
|
||||
|
||||
**HTTP**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# http://YOUR_IP:3000
|
||||
```
|
||||
|
||||
**HTTPS qua Cloudflare**
|
||||
|
||||
1. Thêm domain vào [Cloudflare](https://dash.cloudflare.com) (gói miễn phí)
|
||||
2. Bản ghi A → IP máy chủ, bật proxy
|
||||
3. SSL/TLS → Flexible
|
||||
4. `TRANSPORT_ENCRYPTION=true` trong `.env`
|
||||
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
|
||||
## Tài liệu
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| [Kiến trúc](../../architecture/README.md) | Thiết kế hệ thống và chỉ mục module |
|
||||
| [Module chiến lược](../../architecture/STRATEGY_MODULE.md) | Chọn công cụ, prompt AI, thực thi |
|
||||
| [FAQ](../../faq/README.md) | Câu hỏi thường gặp |
|
||||
| [Bắt đầu](../../getting-started/README.md) | Hướng dẫn triển khai |
|
||||
| | |
|
||||
| :------------------------------------------------------ | :------------------------------------ |
|
||||
| [Bắt đầu](../../getting-started/README.md) | Hướng dẫn triển khai và API sàn |
|
||||
| [Kiến trúc](../../architecture/README.md) | Thiết kế hệ thống và chỉ mục module |
|
||||
| [Module chiến lược](../../architecture/STRATEGY_MODULE.md) | Chọn coin, prompt AI, thực thi |
|
||||
| [FAQ](../../guides/faq.en.md) | Câu hỏi thường gặp |
|
||||
| [Khắc phục sự cố](../../guides/TROUBLESHOOTING.md) | Chẩn đoán các sự cố thường gặp |
|
||||
|
||||
---
|
||||
## Cộng đồng
|
||||
|
||||
[Telegram](https://t.me/nofx_dev_community) · [Twitter/X](https://x.com/vergex_ai) · [Issues](https://github.com/NoFxAiOS/nofx/issues) · [vergex.trade](https://vergex.trade) · [Dashboard trực tiếp](https://vergex.trade/explore)
|
||||
|
||||
## Đóng góp
|
||||
|
||||
Xem [Contributing Guide](../../../CONTRIBUTING.md), [Code of Conduct](../../../CODE_OF_CONDUCT.md), và [Security Policy](../../../SECURITY.md).
|
||||
Code, tài liệu, bản dịch và báo cáo lỗi đều được hoan nghênh — xem [Hướng dẫn đóng góp](../../../CONTRIBUTING.md), [Quy tắc ứng xử](../../../CODE_OF_CONDUCT.md) và [Chính sách bảo mật](../../../SECURITY.md).
|
||||
|
||||
### Chương trình contributor
|
||||
NOFX theo dõi các đóng góp có ý nghĩa và dự định thưởng cho người đóng góp khi hệ sinh thái phát triển. Issue ưu tiên có trọng số cao hơn.
|
||||
|
||||
NOFX ghi nhận các đóng góp có ý nghĩa và dự định thưởng cho contributor khi hệ sinh thái phát triển. Issue ưu tiên có trọng số thưởng cao hơn.
|
||||
| Đóng góp | Trọng số |
|
||||
| :---------------- | :----: |
|
||||
| PR cho issue được ghim | ★★★★★★ |
|
||||
| Code (PR đã merge) | ★★★★★ |
|
||||
| Sửa lỗi | ★★★★ |
|
||||
| Ý tưởng tính năng | ★★★ |
|
||||
| Báo cáo lỗi | ★★ |
|
||||
| Tài liệu | ★★ |
|
||||
|
||||
| Contribution | Weight |
|
||||
| :--- | :---: |
|
||||
| Pinned Issue PRs | ★★★★★★ |
|
||||
| Code (Merged PRs) | ★★★★★ |
|
||||
| Bug Fixes | ★★★★ |
|
||||
| Feature Ideas | ★★★ |
|
||||
| Bug Reports | ★★ |
|
||||
| Documentation | ★★ |
|
||||
|
||||
---
|
||||
|
||||
## Liên kết
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| Website | [vergex.trade](https://vergex.trade) |
|
||||
| Dashboard | [vergex.trade/explore](https://vergex.trade/explore) |
|
||||
| Telegram | [nofx_dev_community](https://t.me/nofx_dev_community) |
|
||||
| Twitter | [@vergex_ai](https://x.com/vergex_ai) |
|
||||
|
||||
> **Cảnh báo rủi ro**: Giao dịch tự động có rủi ro đáng kể. Hãy dùng kích thước vị thế phù hợp, hiểu từng venue và không giao dịch số vốn bạn không thể mất.
|
||||
|
||||
---
|
||||
<a href="https://github.com/NoFxAiOS/nofx/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=NoFxAiOS/nofx" alt="Contributors"/>
|
||||
</a>
|
||||
|
||||
## Nhà tài trợ
|
||||
|
||||
@@ -314,8 +269,14 @@ NOFX ghi nhận các đóng góp có ý nghĩa và dự định thưởng cho co
|
||||
|
||||
[Trở thành nhà tài trợ](https://github.com/sponsors/NoFxAiOS)
|
||||
|
||||
## License
|
||||
<br/>
|
||||
|
||||
Nếu NOFX hữu ích với bạn, một ngôi sao sẽ giúp các trader khác tìm thấy nó.
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
|
||||
## Giấy phép
|
||||
|
||||
[AGPL-3.0](../../../LICENSE)
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
<sub>Giao dịch tự động tiềm ẩn rủi ro đáng kể. Các chiến lược do AI điều khiển mang tính thử nghiệm và có thể thua lỗ. Hãy đặt kích thước vị thế hợp lý, hiểu rõ từng sàn giao dịch, và đừng bao giờ giao dịch bằng số tiền mà bạn không thể để mất. Xem đầy đủ [tuyên bố miễn trừ trách nhiệm](../../../DISCLAIMER.md).</sub>
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
<p align="center"><strong>由 <a href="https://vergex.trade">vergex.trade</a> 支持</strong></p>
|
||||
|
||||
<h1 align="center">NOFX</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>面向全球市场的 AI 交易终端。</strong><br/>
|
||||
<strong>覆盖美股、大宗商品、外汇与加密市场的研究、策略生成、执行与监控。</strong>
|
||||
<img src="../../assets/nofx-banner.svg" alt="NOFX — AI trading terminal" width="100%"/>
|
||||
</p>
|
||||
|
||||
<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://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=flat-square&labelColor=1A1813&color=E0483B" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-E0483B?style=flat-square&labelColor=1A1813" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/telegram-community-E0483B?style=flat-square&labelColor=1A1813&logo=telegram&logoColor=white" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -29,91 +21,83 @@
|
||||
<a href="../vi/README.md">Tiếng Việt</a>
|
||||
</p>
|
||||
|
||||
> **语言声明:** 本中文版本文档仅为方便海外华人社区阅读而提供,不代表本软件面向中国大陆、香港、澳门或台湾地区用户开放。如您位于上述地区,请勿使用本软件。
|
||||
<br/>
|
||||
|
||||
---
|
||||
NOFX 是一个开源交易终端,策略本身就是一个语言模型。每个交易员运行一个持续循环——读取市场结构、做出决策、执行、记录推理过程——同时由 Go 运行时把每一笔订单钳制在模型无法越过的硬性风控限制之内。
|
||||
|
||||
NOFX 是一个开源 AI 交易终端,面向需要统一工作区完成市场研究、策略开发、交易执行与组合监控的活跃交易者。
|
||||
|
||||
产品围绕全球高流动性市场设计:美股、大宗商品合约、外汇货币对与数字资产。AI 层将交易意图转化为观察列表、信号、策略逻辑、风控约束与执行工作流。
|
||||
交易员可以自由组合:任意模型、九家交易所任选、任意策略。多个交易员并行运行,在公开排行榜上按已实现收益一较高下。一切都在你自己的机器上运行;交易所凭证加密存储,绝不外传。
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
打开 **http://127.0.0.1:3000**。
|
||||
终端会在 `http://127.0.0.1:3000` 打开。
|
||||
|
||||
---
|
||||
**首次运行**
|
||||
|
||||
1. 注册——第一个账户即成为该实例的所有者。
|
||||
2. 按引导式启动流程操作:向系统为你创建的 AI 费用钱包存入 **$1+ USDC**(Base 网络),然后连接 Hyperliquid 并存入 **$12+ USDC** 作为交易资金。
|
||||
3. 启动 **Autopilot**。AI 每隔几分钟扫描一次市场并自主交易;每一个决策都会实时出现在仪表板上。随时可以一键停止。
|
||||
|
||||
<br/>
|
||||
|
||||
## 注册交易所
|
||||
|
||||
通过以下链接开通交易账户,可交易加密资产以及平台支持的美股、外汇和大宗商品衍生品市场。这些链接来自 NOFX 合作伙伴计划,可能包含手续费折扣或推荐权益。
|
||||
NOFX 免费且开源。通过下方合作伙伴链接开户可享受更低的交易手续费,同时也为项目的持续开发提供支持。
|
||||
|
||||
| 交易所 | 状态 | 享手续费折扣注册 |
|
||||
| :--- | :---: | :--- |
|
||||
| <img src="../../../web/public/exchange-icons/binance.jpg" width="20" height="20" style="vertical-align: middle;"/> **Binance** | ✅ | [注册](https://www.binance.com/join?ref=NOFXENG) |
|
||||
| <img src="../../../web/public/exchange-icons/bybit.png" width="20" height="20" style="vertical-align: middle;"/> **Bybit** | ✅ | [注册](https://partner.bybit.com/b/83856) |
|
||||
| <img src="../../../web/public/exchange-icons/okx.svg" width="20" height="20" style="vertical-align: middle;"/> **OKX** | ✅ | [注册](https://www.okx.com/join/1865360) |
|
||||
| <img src="../../../web/public/exchange-icons/hyperliquid.png" width="20" height="20" style="vertical-align: middle;"/> **Hyperliquid** | ✅ | [注册](https://app.hyperliquid.xyz/join/AITRADING) |
|
||||
| <img src="../../../web/public/exchange-icons/bitget.svg" width="20" height="20" style="vertical-align: middle;"/> **Bitget** | ✅ | [注册](https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172) |
|
||||
| <img src="../../../web/public/exchange-icons/kucoin.svg" width="20" height="20" style="vertical-align: middle;"/> **KuCoin** | ✅ | [注册](https://www.kucoin.com/r/broker/CXEV7XKK) |
|
||||
| <img src="../../../web/public/exchange-icons/gate.svg" width="20" height="20" style="vertical-align: middle;"/> **Gate** | ✅ | [注册](https://www.gatenode.xyz/share/VQBGUAxY) |
|
||||
| <img src="../../../web/public/exchange-icons/aster.svg" width="20" height="20" style="vertical-align: middle;"/> **Aster** | ✅ | [注册](https://www.asterdex.com/en/referral/fdfc0e) |
|
||||
| <img src="../../../web/public/exchange-icons/lighter.png" width="20" height="20" style="vertical-align: middle;"/> **Lighter** | ✅ | [注册](https://app.lighter.xyz/?referral=68151432) |
|
||||
| 交易所 | 状态 | 享手续费折扣注册 |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------- | :----: | :---------------------------------------------------------------------------------- |
|
||||
| <img src="../../../web/public/exchange-icons/binance.jpg" width="20" height="20" style="vertical-align: middle;"/> **Binance** | ✅ | [注册](https://www.binance.com/join?ref=NOFXENG) |
|
||||
| <img src="../../../web/public/exchange-icons/bybit.png" width="20" height="20" style="vertical-align: middle;"/> **Bybit** | ✅ | [注册](https://partner.bybit.com/b/83856) |
|
||||
| <img src="../../../web/public/exchange-icons/okx.svg" width="20" height="20" style="vertical-align: middle;"/> **OKX** | ✅ | [注册](https://www.okx.com/join/1865360) |
|
||||
| <img src="../../../web/public/exchange-icons/hyperliquid.png" width="20" height="20" style="vertical-align: middle;"/> **Hyperliquid** | ✅ | [注册](https://app.hyperliquid.xyz/join/AITRADING) |
|
||||
| <img src="../../../web/public/exchange-icons/bitget.svg" width="20" height="20" style="vertical-align: middle;"/> **Bitget** | ✅ | [注册](https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172) |
|
||||
| <img src="../../../web/public/exchange-icons/kucoin.svg" width="20" height="20" style="vertical-align: middle;"/> **KuCoin** | ✅ | [注册](https://www.kucoin.com/r/broker/CXEV7XKK) |
|
||||
| <img src="../../../web/public/exchange-icons/gate.svg" width="20" height="20" style="vertical-align: middle;"/> **Gate** | ✅ | [注册](https://www.gatenode.xyz/share/VQBGUAxY) |
|
||||
| <img src="../../../web/public/exchange-icons/aster.svg" width="20" height="20" style="vertical-align: middle;"/> **Aster** | ✅ | [注册](https://www.asterdex.com/en/referral/fdfc0e) |
|
||||
| <img src="../../../web/public/exchange-icons/lighter.png" width="20" height="20" style="vertical-align: middle;"/> **Lighter** | ✅ | [注册](https://app.lighter.xyz/?referral=68151432) |
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 快速演示
|
||||
## 演示
|
||||
|
||||
https://github.com/user-attachments/assets/3310f495-14c5-4586-a1cc-3d32e44aa505
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 市场
|
||||
## 模型提议,运行时裁决
|
||||
|
||||
**美股 · 大宗商品 · 外汇 · 加密资产**
|
||||
决策来自一个读取 [Claw402.ai](https://claw402.ai) · Vergex 数据栈的语言模型:实时信号看板为每个市场标注方向偏置与信号强度排名,Signal Lab 提供逐标的深度信号,成本与清算热力图揭示市场的"燃料"与"墙"在哪里,再叠加实时资金净流——并与原始 K 线和交易员自身的实盘战绩交叉验证。但执行不由它说了算。
|
||||
|
||||
NOFX 按多资产工作流组织研究、策略构建、执行与监控,而不是停留在单一交易所界面。
|
||||
每一笔订单都要经过代码层面强制执行的限制,模型无从干预:
|
||||
|
||||
---
|
||||
| | |
|
||||
| :----------------------- | :--------------------------------------------------------------------------------- |
|
||||
| 持仓限制 | 最大并发持仓数、名义价值按权益比例封顶、每个币种仅允许一个持仓 |
|
||||
| 杠杆钳制 | 在订单定量时施加硬上限,与模型请求的杠杆无关 |
|
||||
| 交易所端保护 | 每次开仓后立即在交易所挂出止损和止盈单 |
|
||||
| 回撤自动平仓 | 盈利持仓从峰值回吐过多时会被自动平掉 |
|
||||
| 交易节流 | 最短持仓时间、单币种再入场冷却、单周期与单小时开仓次数限制 |
|
||||
| 安全模式 | 模型连续失败时阻止新开仓,直至模型恢复正常 |
|
||||
| 启动预检 | 交易员启动前须通过模型访问、钱包资金、策略和交易所余额的校验 |
|
||||
|
||||
## AI 模型接入
|
||||
每个决策都连同模型的完整推理一起存档。没有任何持仓是无据可查的。
|
||||
|
||||
NOFX 自动通过 [Claw402](https://claw402.ai) 路由 AI 推理请求。用户无需配置大模型供应商、管理 API Key 或维护独立 AI 账户。终端按需按次调用 Claw402 的 AI 模型基础设施,并通过官方折扣通道完成路由。
|
||||
<br/>
|
||||
|
||||
| 提供商 | 接入 |
|
||||
## 终端
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| **Claw402** | [通过官方折扣通道按需使用 AI 模型](https://claw402.ai) |
|
||||
|
||||
---
|
||||
|
||||
## 能力
|
||||
|
||||
| 能力 | 描述 |
|
||||
| :--- | :--- |
|
||||
| **AI 交易终端** | 面向美股、大宗商品、外汇与加密资产的一体化工作区 |
|
||||
| **AI 模型接入** | 通过 Claw402 自动接入支持的模型供应商 |
|
||||
| **交易所连接** | Binance、Bybit、OKX、Hyperliquid、Bitget、KuCoin、Gate、Aster、Lighter |
|
||||
| **策略工作室** | 市场范围、指标、风控与策略逻辑 |
|
||||
| **模型竞赛** | 比较 AI 交易员的实时表现与排行榜 |
|
||||
| **Telegram Agent** | 通过聊天控制和监控交易助手 |
|
||||
| **组合仪表板** | 持仓、盈亏、执行历史与模型决策日志 |
|
||||
|
||||
---
|
||||
|
||||
## 截图
|
||||
| **Autopilot** | 引导式启动:注资、连接、入金、启动——全程由服务端预检保驾护航 |
|
||||
| **Strategy Studio** | 风格预设、币种池、技术指标、杠杆、开仓置信度、自定义提示词 |
|
||||
| **竞赛** | 按已实现收益排名的公开排行榜,每个条目都标注所用模型 |
|
||||
| **仪表板** | 实时持仓、订单、统计数据,以及每个决策背后的推理 |
|
||||
|
||||
<details>
|
||||
<summary><b>配置页</b></summary>
|
||||
<summary>截图</summary>
|
||||
|
||||
| 配置 | 交易员列表 |
|
||||
| :----------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/config-ai-exchanges.png" width="400"/> | <img src="../../../screenshots/config-traders-list.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>仪表板</b></summary>
|
||||
<br/>
|
||||
|
||||
| 概览 | 行情图表 |
|
||||
| :-----------------------------------------------------: | :-------------------------------------------------------------: |
|
||||
@@ -123,131 +107,51 @@ NOFX 自动通过 [Claw402](https://claw402.ai) 路由 AI 推理请求。用户
|
||||
| :--------------------------------------------------------------: | :-----------------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-trading-stats.png" width="400"/> | <img src="../../../screenshots/dashboard-position-history.png" width="400"/> |
|
||||
|
||||
| 持仓 | 交易员详情 |
|
||||
| :----------------------------------------------------------: | :---------------------------------------------------: |
|
||||
| <img src="../../../screenshots/dashboard-positions.png" width="400"/> | <img src="../../../screenshots/details-page.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>策略工作室</b></summary>
|
||||
|
||||
| 策略编辑器 | 指标配置 |
|
||||
| :------------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/strategy-studio.png" width="400"/> | <img src="../../../screenshots/strategy-indicators.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>竞赛</b></summary>
|
||||
|
||||
| 竞赛模式 |
|
||||
| :-------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/competition-page.png" width="400"/> |
|
||||
| 竞赛 | 配置 |
|
||||
| :-------------------------------------------------------: | :-----------------------------------------------------------: |
|
||||
| <img src="../../../screenshots/competition-page.png" width="400"/> | <img src="../../../screenshots/config-ai-exchanges.png" width="400"/> |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 安装
|
||||
## 模型
|
||||
|
||||
### Linux / macOS
|
||||
八家提供商,使用你自己的密钥——DeepSeek、OpenAI、Claude、Qwen、Gemini、Grok、Kimi、MiniMax——并支持自定义端点和模型名称。
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
或者完全不需要密钥:[Claw402](https://claw402.ai) 通过 x402 协议以 USDC 按次计量模型用量。一个 Base 链上的钱包即可替代所有 API 密钥。
|
||||
|
||||
### Railway(云部署)
|
||||
| 提供商 | 接入方式 |
|
||||
| :------- | :----- |
|
||||
| **Claw402** | [按量付费的 AI 模型,享官方折扣](https://claw402.ai) |
|
||||
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
## 市场
|
||||
|
||||
### Docker
|
||||
九家交易所全部支持加密货币永续合约。在 Hyperliquid 上,同一套运行时还可以交易代币化美股、大宗商品、指数、外汇和 pre-IPO 永续合约——TSLA、NVDA、GOLD、SPX、EUR、OPENAI——与加密资产并行。
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
安装 [Docker Desktop](https://www.docker.com/products/docker-desktop/),然后:
|
||||
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### 从源码构建
|
||||
|
||||
```bash
|
||||
# Prerequisites: Go 1.21+, Node.js 18+, TA-Lib
|
||||
# macOS: brew install ta-lib
|
||||
# Ubuntu: sudo apt-get install libta-lib0-dev
|
||||
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx
|
||||
cd web && npm install && npm run dev
|
||||
```
|
||||
|
||||
### 更新
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
**新手模式**:引导式 onboarding 帮助新用户完成模型访问、交易所连接、策略配置与首次部署。
|
||||
|
||||
**进阶模式**:
|
||||
|
||||
1. 配置 AI 模型访问
|
||||
2. 连接交易所凭证
|
||||
3. 构建或导入策略
|
||||
4. 创建 AI 交易员配置
|
||||
5. 在仪表板启动、监控并迭代
|
||||
|
||||
所有配置均可在 Web UI **http://127.0.0.1:3000** 完成。
|
||||
|
||||
---
|
||||
|
||||
## 部署到服务器
|
||||
|
||||
**HTTP 部署:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# 通过 http://YOUR_IP:3000 访问
|
||||
```
|
||||
|
||||
**通过 Cloudflare 启用 HTTPS:**
|
||||
|
||||
1. 在 [Cloudflare](https://dash.cloudflare.com)(免费套餐)添加域名
|
||||
2. A 记录指向你的服务器 IP(开启代理)
|
||||
3. SSL/TLS 选择 Flexible
|
||||
4. 在 `.env` 中设置 `TRANSPORT_ENCRYPTION=true`
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
NOFX
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Trading Terminal │
|
||||
│ React + TypeScript + TradingView │
|
||||
│ US Stocks · Commodities · Forex · Crypto │
|
||||
│ React · TypeScript · TradingView │
|
||||
│ Dashboard · Strategy Studio · Competition │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ API Server (Go) │
|
||||
│ JWT auth · encrypted credential store │
|
||||
├──────────────┬──────────────┬───────────────────┤
|
||||
│ Strategy │ Telegram │ Trader Runtime │
|
||||
│ Engine │ Agent │ Risk Controls │
|
||||
│ Strategy │ Autopilot │ Trader Runtime │
|
||||
│ Engine │ Preflight │ Risk Engine │
|
||||
├──────────────┴──────────────┴───────────────────┤
|
||||
│ AI Model Layer │
|
||||
│ Unified provider access through Claw402 │
|
||||
│ Model routing · payment · execution support │
|
||||
│ DeepSeek · OpenAI · Claude · Qwen · Gemini │
|
||||
│ Grok · Kimi · MiniMax · Claw402 (x402 USDC) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Exchange Connectivity │
|
||||
│ Binance · Bybit · OKX · Hyperliquid · Bitget │
|
||||
@@ -255,50 +159,99 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 安装
|
||||
|
||||
**Linux / macOS**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
```
|
||||
|
||||
**Railway**
|
||||
|
||||
[](https://railway.com/deploy/nofx?referralCode=nofx)
|
||||
|
||||
**Docker**
|
||||
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**Windows** —— 安装 [Docker Desktop](https://www.docker.com/products/docker-desktop/),然后:
|
||||
|
||||
```powershell
|
||||
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/NoFxAiOS/nofx/main/docker-compose.prod.yml
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**从源码构建** —— 需要 Go 1.21+、Node.js 18+:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/NoFxAiOS/nofx.git && cd nofx
|
||||
go build -o nofx && ./nofx # backend
|
||||
cd web && npm install && npm run dev # frontend, in a second terminal
|
||||
```
|
||||
|
||||
**更新** —— 重新运行安装脚本,即可原地升级。
|
||||
|
||||
<details>
|
||||
<summary>服务器部署</summary>
|
||||
|
||||
<br/>
|
||||
|
||||
**HTTP**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||
# http://YOUR_IP:3000
|
||||
```
|
||||
|
||||
**通过 Cloudflare 启用 HTTPS**
|
||||
|
||||
1. 将域名添加到 [Cloudflare](https://dash.cloudflare.com)(免费套餐即可)
|
||||
2. A 记录 → 服务器 IP,开启代理
|
||||
3. SSL/TLS → Flexible
|
||||
4. 在 `.env` 中设置 `TRANSPORT_ENCRYPTION=true`
|
||||
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
|
||||
## 文档
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| [架构概览](../../architecture/README.md) | 系统设计和模块索引 |
|
||||
| | |
|
||||
| :------------------------------------------------------ | :------------------------------------ |
|
||||
| [快速开始](../../getting-started/README.zh-CN.md) | 部署与交易所 API 指南 |
|
||||
| [架构](../../architecture/README.md) | 系统设计与模块索引 |
|
||||
| [策略模块](../../architecture/STRATEGY_MODULE.md) | 币种选择、AI 提示词、执行 |
|
||||
| [常见问题](../../faq/README.md) | FAQ |
|
||||
| [快速开始](../../getting-started/README.md) | 部署指南 |
|
||||
| [常见问题](../../guides/faq.zh-CN.md) | 常见疑问解答 |
|
||||
| [故障排查](../../guides/TROUBLESHOOTING.zh-CN.md) | 常见问题诊断 |
|
||||
|
||||
---
|
||||
## 社区
|
||||
|
||||
[Telegram](https://t.me/nofx_dev_community) · [Twitter/X](https://x.com/vergex_ai) · [Issues](https://github.com/NoFxAiOS/nofx/issues) · [vergex.trade](https://vergex.trade) · [实时仪表板](https://vergex.trade/explore)
|
||||
|
||||
## 贡献
|
||||
|
||||
查看 [贡献指南](../../../CONTRIBUTING.md)、[行为准则](../../../CODE_OF_CONDUCT.md) 与 [安全政策](../../../SECURITY.md)。
|
||||
代码、文档、翻译和 Bug 报告都欢迎——参见[贡献指南](../../../CONTRIBUTING.md)、[行为准则](../../../CODE_OF_CONDUCT.md)和[安全政策](../../../SECURITY.md)。
|
||||
|
||||
### 贡献者计划
|
||||
NOFX 会记录有价值的贡献,并计划随着生态发展回馈贡献者。优先级 Issue 拥有更高权重。
|
||||
|
||||
NOFX 会记录有价值的贡献,并计划在生态增长后回馈贡献者。优先级 Issue 拥有更高奖励权重。
|
||||
| 贡献类型 | 权重 |
|
||||
| :---------------- | :----: |
|
||||
| 置顶 Issue 的 PR | ★★★★★★ |
|
||||
| 代码(已合并 PR) | ★★★★★ |
|
||||
| Bug 修复 | ★★★★ |
|
||||
| 功能建议 | ★★★ |
|
||||
| Bug 报告 | ★★ |
|
||||
| 文档 | ★★ |
|
||||
|
||||
| Contribution | Weight |
|
||||
| :--- | :---: |
|
||||
| Pinned Issue PRs | ★★★★★★ |
|
||||
| Code (Merged PRs) | ★★★★★ |
|
||||
| Bug Fixes | ★★★★ |
|
||||
| Feature Ideas | ★★★ |
|
||||
| Bug Reports | ★★ |
|
||||
| Documentation | ★★ |
|
||||
|
||||
---
|
||||
|
||||
## 链接
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| 官网 | [vergex.trade](https://vergex.trade) |
|
||||
| Dashboard | [vergex.trade/explore](https://vergex.trade/explore) |
|
||||
| Telegram | [nofx_dev_community](https://t.me/nofx_dev_community) |
|
||||
| Twitter | [@vergex_ai](https://x.com/vergex_ai) |
|
||||
|
||||
> **风险提示**:自动化交易存在重大风险。请控制仓位,理解每个交易场所的机制,不要投入无法承受损失的资金。
|
||||
|
||||
---
|
||||
<a href="https://github.com/NoFxAiOS/nofx/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=NoFxAiOS/nofx" alt="Contributors"/>
|
||||
</a>
|
||||
|
||||
## 赞助者
|
||||
|
||||
@@ -316,8 +269,14 @@ NOFX 会记录有价值的贡献,并计划在生态增长后回馈贡献者。
|
||||
|
||||
[成为赞助者](https://github.com/sponsors/NoFxAiOS)
|
||||
|
||||
## License
|
||||
<br/>
|
||||
|
||||
如果 NOFX 对你有帮助,点个 Star 能让更多交易者发现它。
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
|
||||
## 许可证
|
||||
|
||||
[AGPL-3.0](../../../LICENSE)
|
||||
|
||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||
<sub>自动化交易存在重大风险。AI 驱动的策略尚处实验阶段,可能造成亏损。请合理控制仓位规模,充分了解每个交易场所,切勿投入无法承受损失的资金。完整[免责声明](../../../DISCLAIMER.md)。</sub>
|
||||
|
||||
@@ -877,16 +877,24 @@ func minInt(a, b int) int {
|
||||
return b
|
||||
}
|
||||
|
||||
// DirectionalCandidates returns bullish (long) and bearish (short) candidate
|
||||
// symbols from the most recent Vergex signal ranking, each ordered by upstream
|
||||
// rank (strongest first). Only populated for vergex_signal coin sources, since
|
||||
// that is the only source carrying a per-symbol directional bias.
|
||||
func (e *StrategyEngine) DirectionalCandidates() (bullish []string, bearish []string) {
|
||||
// DirectionalCandidate is a Vergex board candidate with its directional
|
||||
// signal strength (the board z-score; sign follows the bias direction).
|
||||
type DirectionalCandidate struct {
|
||||
Symbol string
|
||||
Score float64
|
||||
}
|
||||
|
||||
// DirectionalCandidates returns bullish (long) and bearish (short) candidates
|
||||
// from the most recent Vergex signal ranking, each ordered by upstream rank
|
||||
// (strongest first) and carrying the signal score so callers can require a
|
||||
// minimum strength. Only populated for vergex_signal coin sources, since that
|
||||
// is the only source carrying a per-symbol directional bias.
|
||||
func (e *StrategyEngine) DirectionalCandidates() (bullish []DirectionalCandidate, bearish []DirectionalCandidate) {
|
||||
if e == nil || len(e.vergexRankingCache) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
type ranked struct {
|
||||
sym string
|
||||
cand DirectionalCandidate
|
||||
rank int
|
||||
}
|
||||
rankKey := func(r int) int {
|
||||
@@ -900,20 +908,21 @@ func (e *StrategyEngine) DirectionalCandidates() (bullish []string, bearish []st
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
entry := ranked{DirectionalCandidate{Symbol: sym, Score: item.Score}, item.Rank}
|
||||
switch strings.ToLower(strings.TrimSpace(item.Bias)) {
|
||||
case "bearish", "short", "sell":
|
||||
br = append(br, ranked{sym, item.Rank})
|
||||
br = append(br, entry)
|
||||
case "bullish", "long", "buy":
|
||||
bl = append(bl, ranked{sym, item.Rank})
|
||||
bl = append(bl, entry)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(bl, func(i, j int) bool { return rankKey(bl[i].rank) < rankKey(bl[j].rank) })
|
||||
sort.SliceStable(br, func(i, j int) bool { return rankKey(br[i].rank) < rankKey(br[j].rank) })
|
||||
for _, r := range bl {
|
||||
bullish = append(bullish, r.sym)
|
||||
bullish = append(bullish, r.cand)
|
||||
}
|
||||
for _, r := range br {
|
||||
bearish = append(bearish, r.sym)
|
||||
bearish = append(bearish, r.cand)
|
||||
}
|
||||
return bullish, bearish
|
||||
}
|
||||
|
||||
@@ -9,22 +9,28 @@ import (
|
||||
func TestDirectionalCandidates(t *testing.T) {
|
||||
e := &StrategyEngine{
|
||||
vergexRankingCache: map[string]*vergex.SignalRankItem{
|
||||
"xyz:NVDA": {Symbol: "xyz:NVDA", Bias: "bullish", Rank: 2},
|
||||
"xyz:AAPL": {Symbol: "xyz:AAPL", Bias: "bullish", Rank: 1},
|
||||
"BTC": {Symbol: "BTC", Bias: "bearish", Rank: 3},
|
||||
"ETH": {Symbol: "ETH", Bias: "bearish", Rank: 1},
|
||||
"SOL": {Symbol: "SOL", Bias: "neutral", Rank: 1},
|
||||
"xyz:NVDA": {Symbol: "xyz:NVDA", Bias: "bullish", Rank: 2, Score: 1.07},
|
||||
"xyz:AAPL": {Symbol: "xyz:AAPL", Bias: "bullish", Rank: 1, Score: 1.76},
|
||||
"BTC": {Symbol: "BTC", Bias: "bearish", Rank: 3, Score: -0.9},
|
||||
"ETH": {Symbol: "ETH", Bias: "bearish", Rank: 1, Score: -0.05},
|
||||
"SOL": {Symbol: "SOL", Bias: "neutral", Rank: 1, Score: 0.4},
|
||||
},
|
||||
}
|
||||
|
||||
bull, bear := e.DirectionalCandidates()
|
||||
|
||||
if len(bull) != 2 || bull[0] != "xyz:AAPL" || bull[1] != "xyz:NVDA" {
|
||||
if len(bull) != 2 || bull[0].Symbol != "xyz:AAPL" || bull[1].Symbol != "xyz:NVDA" {
|
||||
t.Fatalf("bullish should be rank-ordered [xyz:AAPL xyz:NVDA], got %v", bull)
|
||||
}
|
||||
if len(bear) != 2 || bear[0] != "ETH" || bear[1] != "BTC" {
|
||||
if bull[0].Score != 1.76 || bull[1].Score != 1.07 {
|
||||
t.Fatalf("bullish candidates should carry their board scores, got %v", bull)
|
||||
}
|
||||
if len(bear) != 2 || bear[0].Symbol != "ETH" || bear[1].Symbol != "BTC" {
|
||||
t.Fatalf("bearish should be rank-ordered [ETH BTC], got %v", bear)
|
||||
}
|
||||
if bear[0].Score != -0.05 || bear[1].Score != -0.9 {
|
||||
t.Fatalf("bearish candidates should carry their board scores, got %v", bear)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectionalCandidatesEmpty(t *testing.T) {
|
||||
|
||||
@@ -26,6 +26,14 @@ func (e *StrategyEngine) BuildSystemPrompt(accountEquity float64, variant string
|
||||
zh := false
|
||||
singleSymbol, primarySymbol := e.singleSymbolInfo()
|
||||
|
||||
// Configs created in the Chinese-UI era carry legacy stored prompt sections
|
||||
// and custom prompts written for a different contract; ignore them wholesale
|
||||
// and fall back to the canonical built-in English sections.
|
||||
legacyZhConfig := strings.EqualFold(strings.TrimSpace(e.config.Language), "zh")
|
||||
if legacyZhConfig {
|
||||
promptSections = store.PromptSectionsConfig{}
|
||||
}
|
||||
|
||||
if e.usesVergexSignalPrompt() {
|
||||
return e.buildVergexSystemPrompt(accountEquity, variant, lang, zh, singleSymbol, primarySymbol)
|
||||
}
|
||||
@@ -147,6 +155,9 @@ func (e *StrategyEngine) BuildSystemPrompt(accountEquity float64, variant string
|
||||
// 2. It guarantees a stock-specific, US-equity-tuned briefing
|
||||
// regardless of when the strategy was first created.
|
||||
customPrompt := englishOnlyPromptSection(e.config.CustomPrompt)
|
||||
if legacyZhConfig {
|
||||
customPrompt = ""
|
||||
}
|
||||
if singleSymbol && market.IsXyzDexAsset(primarySymbol) {
|
||||
customPrompt = buildXYZStockCustomPrompt(primarySymbol)
|
||||
}
|
||||
@@ -204,8 +215,9 @@ func (e *StrategyEngine) buildVergexSystemPrompt(accountEquity float64, variant
|
||||
sb.WriteString("- Ranking alone is not an entry reason; it only defines the candidate pool.\n")
|
||||
sb.WriteString("- Every symbol in Candidate Coins is part of the allowed trading universe; missing detail can lower confidence or trigger waiting, but does not make the symbol non-tradable.\n")
|
||||
sb.WriteString("- If Signal Lab or heatmap is absent from that symbol's Vergex Claw402 Signals, state it in reasoning; if it is present, never claim the symbol lacks that data.\n")
|
||||
sb.WriteString("- Avoid churn: unless stopping out or taking a strong profit, hold new positions for at least 45 minutes; avoid flat/noise closes until roughly 90 minutes; after closing a symbol, wait 90 minutes before re-entry; open at most 1 new position per hour.\n")
|
||||
sb.WriteString("- Stops must sit beyond invalidation; targets should prefer heatmap resistance/liquidation zones or valid risk/reward levels.\n\n")
|
||||
sb.WriteString("- Hold for BIG moves, do not churn: hold new positions for at least 4 hours; never close inside the -4%% to +6%% noise band before ~8 hours; after closing a symbol wait 3 hours before re-entry; open at most 1-2 new positions per hour. Small in-and-out trades bled this account to death on fees.\n")
|
||||
sb.WriteString("- Fees are the main edge killer: a round trip costs ~0.1%% of notional. Only enter setups where the realistic target is a LARGE move: stop-loss around -5%% and take-profit around +10-12%% (roughly 2:1 or better). Do not aim for 0.5%% scalps — they cannot cover fees. Fewer, high-conviction, wide-target, multi-hour holds only.\n")
|
||||
sb.WriteString("- Set WIDE stops and targets: place the stop well beyond short-term noise (around -5%%) and the target at a distant heatmap resistance/liquidation zone (around +10-12%%). Give the position room to develop; do not exit on small green or small red.\n\n")
|
||||
} else {
|
||||
sb.WriteString("# You are the NOFX Claw402 auto-trader\n\n")
|
||||
sb.WriteString("Trade only Hyperliquid instruments returned by this cycle's Claw402.ai/Vergex board. You may trade only the current candidate symbols and existing positions; never invent tickers or rotate outside the provided universe.\n\n")
|
||||
@@ -220,8 +232,9 @@ func (e *StrategyEngine) buildVergexSystemPrompt(accountEquity float64, variant
|
||||
sb.WriteString("- Ranking alone is not an entry reason; it only defines the candidate pool.\n")
|
||||
sb.WriteString("- Every symbol in Candidate Coins is part of the allowed trading universe; missing detail can lower confidence or trigger waiting, but does not make the symbol non-tradable.\n")
|
||||
sb.WriteString("- If Signal Lab or heatmap is absent from that symbol's Vergex Claw402 Signals, state it in reasoning; if it is present, never claim the symbol lacks that data.\n")
|
||||
sb.WriteString("- Avoid churn: unless stopping out or taking a strong profit, hold new positions for at least 45 minutes; avoid flat/noise closes until roughly 90 minutes; after closing a symbol, wait 90 minutes before re-entry; open at most 1 new position per hour.\n")
|
||||
sb.WriteString("- Stops must sit beyond invalidation; targets should prefer heatmap resistance/liquidation zones or valid risk/reward levels.\n\n")
|
||||
sb.WriteString("- Hold for BIG moves, do not churn: hold new positions for at least 4 hours; never close inside the -4%% to +6%% noise band before ~8 hours; after closing a symbol wait 3 hours before re-entry; open at most 1-2 new positions per hour. Small in-and-out trades bled this account to death on fees.\n")
|
||||
sb.WriteString("- Fees are the main edge killer: a round trip costs ~0.1%% of notional. Only enter setups where the realistic target is a LARGE move: stop-loss around -5%% and take-profit around +10-12%% (roughly 2:1 or better). Do not aim for 0.5%% scalps — they cannot cover fees. Fewer, high-conviction, wide-target, multi-hour holds only.\n")
|
||||
sb.WriteString("- Set WIDE stops and targets: place the stop well beyond short-term noise (around -5%%) and the target at a distant heatmap resistance/liquidation zone (around +10-12%%). Give the position room to develop; do not exit on small green or small red.\n\n")
|
||||
}
|
||||
|
||||
writeModeVariant(&sb, variant, zh)
|
||||
@@ -233,7 +246,7 @@ func (e *StrategyEngine) buildVergexSystemPrompt(accountEquity float64, variant
|
||||
writeVergexHardConstraints(&sb, accountEquity, riskControl, altcoinPosValueRatio, zh)
|
||||
writeVergexOutputFormat(&sb, accountEquity, riskControl, altcoinPosValueRatio, singleSymbol, primarySymbol, zh)
|
||||
|
||||
customPrompt := englishOnlyPromptSection(e.config.CustomPrompt)
|
||||
customPrompt := vergexCustomPromptSection(e.config.CustomPrompt)
|
||||
if customPrompt != "" {
|
||||
sb.WriteString("# User Preference\n\n")
|
||||
sb.WriteString(customPrompt)
|
||||
@@ -243,6 +256,32 @@ func (e *StrategyEngine) buildVergexSystemPrompt(accountEquity float64, variant
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// vergexCustomPromptSection returns the user's custom prompt for the vergex
|
||||
// path, dropping legacy directional overrides ("long only" era) that would
|
||||
// contradict the data-driven direction rule baked into this prompt.
|
||||
func vergexCustomPromptSection(section string) string {
|
||||
trimmed := englishOnlyPromptSection(section)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
lower := strings.ToLower(trimmed)
|
||||
legacyDirectives := []string{
|
||||
"long only",
|
||||
"long-only",
|
||||
"do not short",
|
||||
"no shorts",
|
||||
"must open a long",
|
||||
"short only",
|
||||
"short-only",
|
||||
}
|
||||
for _, directive := range legacyDirectives {
|
||||
if strings.Contains(lower, directive) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func englishOnlyPromptSection(section string) string {
|
||||
trimmed := strings.TrimSpace(section)
|
||||
if trimmed == "" {
|
||||
@@ -335,13 +374,13 @@ func writeVergexOutputFormat(sb *strings.Builder, accountEquity float64, riskCon
|
||||
sb.WriteString("Use XML tags <reasoning> and <decision> to separate concise analysis from the decision JSON.\n\n")
|
||||
sb.WriteString("Direction must be data-driven: use `open_long` for confirmed upside structures and `open_short` for confirmed downside structures; never default to long-only or short-only behavior.\n\n")
|
||||
if !singleSymbol {
|
||||
sb.WriteString("This cycle you MUST include at least one `open_long` (pick the strongest net-inflow / bullish name) AND at least one `open_short` (pick the strongest net-outflow / bearish name); omit a side only if no suitable name exists for it.\n\n")
|
||||
sb.WriteString("Evaluate both directions every cycle, but enter a side only when its own signals independently justify it. Never open a position just to balance the book — an unbalanced book beats a forced trade.\n\n")
|
||||
}
|
||||
} else {
|
||||
sb.WriteString("Use XML tags <reasoning> and <decision> to separate concise analysis from the decision JSON.\n\n")
|
||||
sb.WriteString("Direction must be data-driven: use `open_long` for confirmed upside structures and `open_short` for confirmed downside structures; never default to long-only or short-only behavior.\n\n")
|
||||
if !singleSymbol {
|
||||
sb.WriteString("This cycle you MUST include at least one `open_long` (pick the strongest net-inflow / bullish name) AND at least one `open_short` (pick the strongest net-outflow / bearish name); omit a side only if no suitable name exists for it.\n\n")
|
||||
sb.WriteString("Evaluate both directions every cycle, but enter a side only when its own signals independently justify it. Never open a position just to balance the book — an unbalanced book beats a forced trade.\n\n")
|
||||
}
|
||||
}
|
||||
sb.WriteString("<reasoning>\n")
|
||||
|
||||
@@ -29,8 +29,8 @@ func TestBuildSystemPromptUsesVergexClaw402Prompt(t *testing.T) {
|
||||
if !strings.Contains(prompt, "Direction must be data-driven") {
|
||||
t.Fatalf("prompt should explain that direction is data-driven, not long-only:\n%s", prompt)
|
||||
}
|
||||
if !strings.Contains(prompt, "every open position must use exactly 10x") {
|
||||
t.Fatalf("prompt should force 10x leverage for Claw402 opens:\n%s", prompt)
|
||||
if !strings.Contains(prompt, "every open position must use exactly 5x") {
|
||||
t.Fatalf("prompt should force 5x leverage for Claw402 opens:\n%s", prompt)
|
||||
}
|
||||
if !strings.Contains(prompt, "use the full max notional per position") {
|
||||
t.Fatalf("prompt should force full-size Claw402 opens:\n%s", prompt)
|
||||
|
||||
@@ -658,15 +658,10 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg
|
||||
return fmt.Errorf("failed to parse strategy config for trader %s: %w", traderCfg.Name, err)
|
||||
}
|
||||
strategyConfig.ClampLimits()
|
||||
// Autopilot (vergex_signal/claw402) runs a balanced multi-position book:
|
||||
// hold several instruments with a smaller per-position notional so multiple
|
||||
// long/short positions fit the margin. Applied after ClampLimits so it is
|
||||
// not capped back to the conservative single-position default.
|
||||
if strategyConfig.CoinSource.SourceType == "vergex_signal" {
|
||||
strategyConfig.RiskControl.MaxPositions = 6
|
||||
strategyConfig.RiskControl.BTCETHMaxPositionValueRatio = 1.2
|
||||
strategyConfig.RiskControl.AltcoinMaxPositionValueRatio = 1.2
|
||||
}
|
||||
// Sizing comes from the strategy's own RiskControl (a hardcoded
|
||||
// 6-position × equity×1.2 override used to live here, silently ignoring
|
||||
// the user's configuration). ClampLimits bounds the values and the
|
||||
// margin auto-reduce at order time keeps the book solvent.
|
||||
logger.Infof("✓ Trader %s loaded strategy config: %s (maxPos=%d, posRatio=%.1f)", traderCfg.Name, strategy.Name, strategyConfig.RiskControl.MaxPositions, strategyConfig.RiskControl.AltcoinMaxPositionValueRatio)
|
||||
ensureHyperliquidNativeStrategy(traderCfg.Name, exchangeCfg.ExchangeType, strategyConfig)
|
||||
} else {
|
||||
|
||||
@@ -347,6 +347,27 @@ func (s *PositionStore) GetOpenPositions(traderID string) ([]*TraderPosition, er
|
||||
return positions, nil
|
||||
}
|
||||
|
||||
// GetOpenPositionsByExchange returns every OPEN row on an exchange account,
|
||||
// across all trader IDs. An exchange account is shared by every "NOFX
|
||||
// Autopilot" relaunch (each relaunch mints a fresh trader_id), so reconciling
|
||||
// must be scoped to the exchange — not the current trader_id — or rows left by
|
||||
// prior incarnations become permanent orphans that never close.
|
||||
func (s *PositionStore) GetOpenPositionsByExchange(exchangeID string) ([]*TraderPosition, error) {
|
||||
var positions []*TraderPosition
|
||||
err := s.db.Where("exchange_id = ? AND status = ?", exchangeID, "OPEN").
|
||||
Order("entry_time DESC").
|
||||
Find(&positions).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query open positions by exchange: %w", err)
|
||||
}
|
||||
for _, pos := range positions {
|
||||
if pos.EntryQuantity == 0 {
|
||||
pos.EntryQuantity = pos.Quantity
|
||||
}
|
||||
}
|
||||
return positions, nil
|
||||
}
|
||||
|
||||
// GetOpenPositionBySymbol gets open position for specified symbol and direction
|
||||
func (s *PositionStore) GetOpenPositionBySymbol(traderID, symbol, side string) (*TraderPosition, error) {
|
||||
var pos TraderPosition
|
||||
|
||||
@@ -38,7 +38,9 @@ type HistorySummary struct {
|
||||
func (s *PositionStore) GetHistorySummary(traderID string) (*HistorySummary, error) {
|
||||
summary := &HistorySummary{}
|
||||
|
||||
fullStats, err := s.GetFullStats(traderID)
|
||||
// Baseline 0 is fine here: the summary only reads counts/win-rate/PnL, not
|
||||
// the drawdown that depends on it.
|
||||
fullStats, err := s.GetFullStats(traderID, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -54,14 +54,16 @@ func (s *PositionStore) GetPositionStats(traderID string) (map[string]interface{
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetFullStats gets complete trading statistics
|
||||
func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) {
|
||||
return s.GetFullStatsByTraderFilters([]string{traderID}, nil)
|
||||
// GetFullStats gets complete trading statistics. startingEquity is the real
|
||||
// account baseline used for the drawdown equity curve; pass 0 when unknown.
|
||||
func (s *PositionStore) GetFullStats(traderID string, startingEquity float64) (*TraderStats, error) {
|
||||
return s.GetFullStatsByTraderFilters([]string{traderID}, nil, startingEquity)
|
||||
}
|
||||
|
||||
// GetFullStatsByTraderFilters gets complete trading statistics for explicit
|
||||
// trader IDs plus optional legacy trader ID patterns.
|
||||
func (s *PositionStore) GetFullStatsByTraderFilters(traderIDs []string, traderIDPatterns []string) (*TraderStats, error) {
|
||||
// trader IDs plus optional legacy trader ID patterns. startingEquity is the
|
||||
// real account baseline for the drawdown calculation; pass 0 when unknown.
|
||||
func (s *PositionStore) GetFullStatsByTraderFilters(traderIDs []string, traderIDPatterns []string, startingEquity float64) (*TraderStats, error) {
|
||||
stats := &TraderStats{}
|
||||
|
||||
var positions []TraderPosition
|
||||
@@ -106,7 +108,7 @@ func (s *PositionStore) GetFullStatsByTraderFilters(traderIDs []string, traderID
|
||||
stats.SharpeRatio = calculateSharpeRatioFromPnls(pnls)
|
||||
}
|
||||
if len(pnls) > 0 {
|
||||
stats.MaxDrawdownPct = calculateMaxDrawdownFromPnls(pnls)
|
||||
stats.MaxDrawdownPct = calculateMaxDrawdownFromPnls(pnls, startingEquity)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
@@ -192,13 +194,21 @@ func calculateSharpeRatioFromPnls(pnls []float64) float64 {
|
||||
return mean / stdDev
|
||||
}
|
||||
|
||||
// calculateMaxDrawdownFromPnls calculates maximum drawdown
|
||||
func calculateMaxDrawdownFromPnls(pnls []float64) float64 {
|
||||
// calculateMaxDrawdownFromPnls reconstructs an equity curve from the realized
|
||||
// PnL sequence on top of startingEquity and returns the max peak-to-trough
|
||||
// drawdown as a PERCENT (e.g. 18.5 = -18.5%). The baseline matters: the same
|
||||
// $87 dip is 0.9% of a $10k account but 18% of a $480 one, so callers should
|
||||
// pass the trader's real initial balance. A non-positive baseline falls back
|
||||
// to a neutral $10k so the metric stays defined (but understated) when the
|
||||
// account baseline is unknown.
|
||||
func calculateMaxDrawdownFromPnls(pnls []float64, startingEquity float64) float64 {
|
||||
if len(pnls) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
const startingEquity = 10000.0
|
||||
if startingEquity <= 0 {
|
||||
startingEquity = 10000.0
|
||||
}
|
||||
equity := startingEquity
|
||||
peak := startingEquity
|
||||
var maxDD float64
|
||||
|
||||
111
store/position_reconcile.go
Normal file
111
store/position_reconcile.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"nofx/logger"
|
||||
)
|
||||
|
||||
const reconcileQuantityTolerance = 0.0001
|
||||
|
||||
// LivePositionKey builds the map key used by ReconcileOpenPositionsWithLive.
|
||||
func LivePositionKey(symbol, side string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(symbol)) + "|" + strings.ToUpper(strings.TrimSpace(side))
|
||||
}
|
||||
|
||||
// ReconcileOpenPositionsWithLive force-closes local OPEN position rows that the
|
||||
// exchange no longer holds, and trims rows whose quantity exceeds what is live.
|
||||
//
|
||||
// Why this exists: missed or unmatched fills (position flips, liquidations,
|
||||
// sync gaps) leave "zombie" OPEN rows behind. Every later close on the same
|
||||
// symbol+side then lands as a partial close against the zombie, so the row
|
||||
// never reaches CLOSED and its realized PnL never enters the closed-trade
|
||||
// statistics — the dashboard, the Edge Profile and the AI's own track-record
|
||||
// context all silently under-report. Reconciling against the exchange's live
|
||||
// book is the self-healing fix: local OPEN rows must always be a subset of
|
||||
// what the exchange actually holds.
|
||||
//
|
||||
// liveQty maps LivePositionKey(symbol, side) → live quantity on the exchange.
|
||||
// Rows are matched newest-first so the freshest row survives as the live
|
||||
// position's bookkeeping and older duplicates get closed.
|
||||
//
|
||||
// Scope is by exchange account (all trader IDs), so rows left by prior
|
||||
// autopilot incarnations on the same exchange are reconciled too.
|
||||
func (s *PositionStore) ReconcileOpenPositionsWithLive(exchangeID string, liveQty map[string]float64) (int, error) {
|
||||
openRows, err := s.GetOpenPositionsByExchange(exchangeID)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to list open positions: %w", err)
|
||||
}
|
||||
if len(openRows) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Copy so we can consume quantities without mutating the caller's map.
|
||||
remaining := make(map[string]float64, len(liveQty))
|
||||
for k, v := range liveQty {
|
||||
remaining[strings.ToUpper(k)] = v
|
||||
}
|
||||
|
||||
// Newest first: the most recent row keeps representing the live position.
|
||||
sort.Slice(openRows, func(i, j int) bool {
|
||||
return openRows[i].EntryTime > openRows[j].EntryTime
|
||||
})
|
||||
|
||||
nowMs := time.Now().UTC().UnixMilli()
|
||||
closed := 0
|
||||
|
||||
for _, row := range openRows {
|
||||
key := LivePositionKey(row.Symbol, row.Side)
|
||||
live := remaining[key]
|
||||
|
||||
if live > reconcileQuantityTolerance {
|
||||
// The exchange still holds (part of) this key — this row survives.
|
||||
if row.Quantity > live+reconcileQuantityTolerance {
|
||||
// Trim the row down to what is actually live so the next real
|
||||
// close matches sizes and can fully close it. No PnL is
|
||||
// fabricated: the trimmed residue is stale bookkeeping, not a
|
||||
// real fill.
|
||||
trim := row.Quantity - live
|
||||
exitPrice := row.ExitPrice
|
||||
if exitPrice <= 0 {
|
||||
exitPrice = row.EntryPrice
|
||||
}
|
||||
if err := s.ReducePositionQuantity(row.ID, trim, exitPrice, 0, 0); err != nil {
|
||||
logger.Infof(" ⚠️ Reconcile: failed to trim position %d (%s %s): %v", row.ID, row.Symbol, row.Side, err)
|
||||
} else {
|
||||
logger.Infof(" 🧹 Reconcile: trimmed %s %s row %d by %.6f to match live %.6f", row.Symbol, row.Side, row.ID, trim, live)
|
||||
}
|
||||
remaining[key] = 0
|
||||
} else {
|
||||
remaining[key] = live - row.Quantity
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Nothing (left) on the exchange for this key — the row is a zombie.
|
||||
// Close it with whatever it accumulated; exit info falls back to the
|
||||
// last known bookkeeping on the row.
|
||||
exitPrice := row.ExitPrice
|
||||
if exitPrice <= 0 {
|
||||
exitPrice = row.EntryPrice
|
||||
}
|
||||
exitTime := row.UpdatedAt
|
||||
if exitTime <= 0 {
|
||||
exitTime = nowMs
|
||||
}
|
||||
if err := s.ClosePositionFully(row.ID, exitPrice, row.ExitOrderID, exitTime, row.RealizedPnL, row.Fee, "reconcile"); err != nil {
|
||||
logger.Infof(" ⚠️ Reconcile: failed to close zombie position %d (%s %s): %v", row.ID, row.Symbol, row.Side, err)
|
||||
continue
|
||||
}
|
||||
closed++
|
||||
logger.Infof(" 🧹 Reconcile: closed zombie %s %s row %d (qty %.6f, accumulated PnL %.2f) — not held on exchange", row.Symbol, row.Side, row.ID, row.Quantity, row.RealizedPnL)
|
||||
}
|
||||
|
||||
if closed > 0 {
|
||||
logger.Infof("✅ Position reconcile: closed %d zombie row(s) on exchange %s", closed, exchangeID)
|
||||
}
|
||||
return closed, nil
|
||||
}
|
||||
113
store/position_reconcile_test.go
Normal file
113
store/position_reconcile_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newReconcileTestStore(t *testing.T) *Store {
|
||||
t.Helper()
|
||||
st, err := New(t.TempDir() + "/nofx.db")
|
||||
if err != nil {
|
||||
t.Fatalf("store.New failed: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = st.Close() })
|
||||
return st
|
||||
}
|
||||
|
||||
func openRow(t *testing.T, st *Store, traderID, exchangeID, symbol, side string, qty, pnl float64, entryMs int64) int64 {
|
||||
t.Helper()
|
||||
pos := &TraderPosition{
|
||||
TraderID: traderID,
|
||||
ExchangeID: exchangeID,
|
||||
ExchangeType: "hyperliquid",
|
||||
Symbol: symbol,
|
||||
Side: side,
|
||||
Quantity: qty,
|
||||
EntryQuantity: qty,
|
||||
EntryPrice: 100,
|
||||
EntryTime: entryMs,
|
||||
RealizedPnL: pnl,
|
||||
Status: "OPEN",
|
||||
Source: "sync",
|
||||
CreatedAt: entryMs,
|
||||
UpdatedAt: entryMs,
|
||||
}
|
||||
if err := st.Position().CreateOpenPosition(pos); err != nil {
|
||||
t.Fatalf("create open position: %v", err)
|
||||
}
|
||||
return pos.ID
|
||||
}
|
||||
|
||||
func TestReconcileClosesZombiesKeepsLiveAndTrims(t *testing.T) {
|
||||
st := newReconcileTestStore(t)
|
||||
const exch = "ex-hl"
|
||||
base := time.Now().Add(-48 * time.Hour).UnixMilli()
|
||||
|
||||
// Zombie under the CURRENT trader id: exchange holds nothing for DRAM.
|
||||
zombieID := openRow(t, st, "trader-now", exch, "xyz:DRAM", "LONG", 6.8, -20.34, base)
|
||||
|
||||
// Zombie left by a PRIOR autopilot incarnation on the SAME exchange —
|
||||
// this is the case a per-trader-id reconcile would miss.
|
||||
legacyID := openRow(t, st, "trader-old", exch, "SOLUSDT", "SHORT", 6.94, -3.5, base+500)
|
||||
|
||||
// Duplicates for SP500 (different incarnations): newest survives trimmed,
|
||||
// older closes.
|
||||
oldSP := openRow(t, st, "trader-old", exch, "xyz:SP500", "LONG", 0.07, -1.5, base+1000)
|
||||
newSP := openRow(t, st, "trader-now", exch, "xyz:SP500", "LONG", 0.124, 2.5, base+2000)
|
||||
|
||||
// Healthy row exactly matching live — untouched.
|
||||
healthy := openRow(t, st, "trader-now", exch, "BTCUSDT", "LONG", 0.01, 0, base+3000)
|
||||
|
||||
// Row on a DIFFERENT exchange account — must be out of scope.
|
||||
otherExch := openRow(t, st, "trader-now", "ex-other", "ETHUSDT", "LONG", 2.0, 0, base+4000)
|
||||
|
||||
live := map[string]float64{
|
||||
LivePositionKey("xyz:SP500", "long"): 0.057,
|
||||
LivePositionKey("BTCUSDT", "long"): 0.01,
|
||||
}
|
||||
|
||||
closed, err := st.Position().ReconcileOpenPositionsWithLive(exch, live)
|
||||
if err != nil {
|
||||
t.Fatalf("reconcile failed: %v", err)
|
||||
}
|
||||
if closed != 3 {
|
||||
t.Fatalf("expected 3 zombies closed (DRAM + legacy SOL + old SP500), got %d", closed)
|
||||
}
|
||||
|
||||
get := func(id int64) *TraderPosition {
|
||||
t.Helper()
|
||||
var pos TraderPosition
|
||||
if err := st.Position().db.First(&pos, id).Error; err != nil {
|
||||
t.Fatalf("load position %d: %v", id, err)
|
||||
}
|
||||
return &pos
|
||||
}
|
||||
|
||||
if dram := get(zombieID); dram.Status != "CLOSED" || dram.RealizedPnL != -20.34 || dram.CloseReason != "reconcile" {
|
||||
t.Fatalf("DRAM zombie should close via reconcile keeping PnL, got %+v", dram)
|
||||
}
|
||||
if sol := get(legacyID); sol.Status != "CLOSED" {
|
||||
t.Fatalf("legacy-incarnation SOL zombie on same exchange should close, got %+v", sol)
|
||||
}
|
||||
if sp := get(oldSP); sp.Status != "CLOSED" {
|
||||
t.Fatalf("older duplicate SP500 row should close, got %+v", sp)
|
||||
}
|
||||
if sp := get(newSP); sp.Status != "OPEN" || sp.Quantity > 0.0571 || sp.Quantity < 0.0569 {
|
||||
t.Fatalf("newest SP500 row should stay open trimmed to live 0.057, got status=%s qty=%v", sp.Status, sp.Quantity)
|
||||
}
|
||||
if btc := get(healthy); btc.Status != "OPEN" || btc.Quantity != 0.01 {
|
||||
t.Fatalf("healthy row must be untouched, got %+v", btc)
|
||||
}
|
||||
if eth := get(otherExch); eth.Status != "OPEN" {
|
||||
t.Fatalf("row on a different exchange must be out of scope, got %+v", eth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileNoOpenRowsIsNoop(t *testing.T) {
|
||||
st := newReconcileTestStore(t)
|
||||
closed, err := st.Position().ReconcileOpenPositionsWithLive("ex-empty", map[string]float64{})
|
||||
if err != nil || closed != 0 {
|
||||
t.Fatalf("expected clean noop, got closed=%d err=%v", closed, err)
|
||||
}
|
||||
}
|
||||
@@ -126,6 +126,7 @@ func TestGetClosedPositionsByTraderFiltersIncludesLegacyAutopilotIDs(t *testing.
|
||||
stats, err := positions.GetFullStatsByTraderFilters(
|
||||
[]string{"current-trader"},
|
||||
[]string{"%_user-123_claw402_%"},
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("get stats: %v", err)
|
||||
@@ -134,3 +135,23 @@ func TestGetClosedPositionsByTraderFiltersIncludesLegacyAutopilotIDs(t *testing.
|
||||
t.Fatalf("unexpected stats: trades=%d pnl=%.2f", stats.TotalTrades, stats.TotalPnL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateMaxDrawdownUsesRealBaseline(t *testing.T) {
|
||||
// +50 then -100: peak 550, trough 450 on a 500 account → 100/550 ≈ 18.18%.
|
||||
pnls := []float64{50, -100}
|
||||
|
||||
got := calculateMaxDrawdownFromPnls(pnls, 500)
|
||||
if got < 18.1 || got > 18.3 {
|
||||
t.Fatalf("expected ~18.18%% drawdown on a 500 baseline, got %.2f", got)
|
||||
}
|
||||
|
||||
// Unknown baseline falls back to the neutral 10k curve: 100/10050 ≈ 1%.
|
||||
fallback := calculateMaxDrawdownFromPnls(pnls, 0)
|
||||
if fallback < 0.9 || fallback > 1.1 {
|
||||
t.Fatalf("expected ~1%% drawdown on the 10k fallback, got %.2f", fallback)
|
||||
}
|
||||
|
||||
if calculateMaxDrawdownFromPnls(nil, 500) != 0 {
|
||||
t.Fatalf("no trades must mean zero drawdown")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1014,11 +1014,11 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
|
||||
PriceRankingLimit: 10,
|
||||
},
|
||||
RiskControl: RiskControlConfig{
|
||||
MaxPositions: 6, // Hold up to 6 instruments (≈3 long + 3 short) simultaneously (CODE ENFORCED)
|
||||
BTCETHMaxLeverage: 10, // BTC/ETH exchange leverage (AI guided)
|
||||
AltcoinMaxLeverage: 10, // TradeFi exchange leverage (AI guided)
|
||||
BTCETHMaxPositionValueRatio: 1.2, // Per-position notional = equity × 1.2 so several positions fit the margin
|
||||
AltcoinMaxPositionValueRatio: 1.2, // Per-position notional = equity × 1.2 so several positions fit the margin
|
||||
MaxPositions: 2, // Few, concentrated positions held for big moves (CODE ENFORCED)
|
||||
BTCETHMaxLeverage: 5, // Low leverage so a big (-5%) stop is survivable, not an instant liquidation
|
||||
AltcoinMaxLeverage: 5, // Low leverage so a big (-5%) stop is survivable, not an instant liquidation
|
||||
BTCETHMaxPositionValueRatio: 2.5, // Per-position notional = equity × 2.5; 2 positions = 5x total (full margin at 5x, ~20% liquidation cushion)
|
||||
AltcoinMaxPositionValueRatio: 2.5, // Per-position notional = equity × 2.5; 2 positions = 5x total (full margin at 5x, ~20% liquidation cushion)
|
||||
MaxMarginUsage: 1.0, // Claw402 Autopilot intentionally uses full margin when opening
|
||||
MinPositionSize: 12, // Min 12 USDT per position (CODE ENFORCED)
|
||||
MinRiskRewardRatio: 3.0, // Min 3:1 profit/loss ratio (AI guided)
|
||||
|
||||
@@ -177,8 +177,12 @@ type AutoTrader struct {
|
||||
gridState *GridState // Grid trading state (only used when StrategyType == "grid_trading")
|
||||
claw402WalletAddr string // Claw402 wallet address (derived from private key at start)
|
||||
consecutiveAIFailures int // Consecutive AI call failures
|
||||
runtimeHealthMu sync.RWMutex // Guards safe mode + AI wallet health (loop writes, API reads)
|
||||
safeMode bool // Safe mode: no new positions, protect existing ones
|
||||
safeModeReason string // Why safe mode was activated
|
||||
aiWalletStatus string // "ok"|"low"|"empty"|"unknown" — see runtime_health.go
|
||||
aiWalletBalanceUSDC float64 // Last observed Base USDC balance of the claw402 wallet
|
||||
aiWalletCheckedAt time.Time // When the balance was last observed
|
||||
}
|
||||
|
||||
// NewAutoTrader creates an automatic trader
|
||||
@@ -418,15 +422,11 @@ func (at *AutoTrader) reloadStrategyConfigIfChanged() error {
|
||||
}
|
||||
strategyConfig.ClampLimits()
|
||||
|
||||
// Autopilot (vergex_signal/claw402) runs a balanced multi-position book:
|
||||
// hold several instruments at once with a smaller per-position notional so
|
||||
// multiple long/short positions fit the margin. Applied after ClampLimits so
|
||||
// the book size is not capped back down to the conservative default.
|
||||
if strategyConfig.CoinSource.SourceType == "vergex_signal" {
|
||||
strategyConfig.RiskControl.MaxPositions = 6
|
||||
strategyConfig.RiskControl.BTCETHMaxPositionValueRatio = 1.2
|
||||
strategyConfig.RiskControl.AltcoinMaxPositionValueRatio = 1.2
|
||||
}
|
||||
// NOTE: this used to hardcode the Autopilot book shape (6 positions ×
|
||||
// equity×1.2 notional), silently overriding whatever the user configured in
|
||||
// their strategy. Sizing now comes from the strategy's own RiskControl —
|
||||
// ClampLimits above bounds it (ratio 0.5–10, leverage caps), and the
|
||||
// margin auto-reduce at order time keeps the book solvent.
|
||||
|
||||
claw402Key := at.config.Claw402WalletKey
|
||||
if claw402Key == "" && at.config.AIModel == "claw402" && at.config.CustomAPIKey != "" {
|
||||
@@ -608,6 +608,12 @@ func (at *AutoTrader) GetID() string {
|
||||
return at.id
|
||||
}
|
||||
|
||||
// GetInitialBalance returns the account baseline used for performance metrics
|
||||
// (e.g. the drawdown equity curve).
|
||||
func (at *AutoTrader) GetInitialBalance() float64 {
|
||||
return at.initialBalance
|
||||
}
|
||||
|
||||
// GetUnderlyingTrader returns the underlying Trader interface implementation
|
||||
// This is used by grid trading and other components that need direct exchange access
|
||||
func (at *AutoTrader) GetUnderlyingTrader() Trader {
|
||||
@@ -710,7 +716,9 @@ func (at *AutoTrader) runPreLaunchChecks() {
|
||||
balance, err := wallet.QueryUSDCBalance(addr)
|
||||
if err != nil {
|
||||
logger.Warnf("⚠️ [%s] Could not query USDC balance: %v", at.name, err)
|
||||
at.markAIWalletHealthUnknown()
|
||||
} else {
|
||||
at.setAIWalletHealth(balance)
|
||||
// Estimate runway
|
||||
scanMinutes := int(at.config.ScanInterval.Minutes())
|
||||
modelName := at.config.CustomModelName
|
||||
|
||||
@@ -91,6 +91,19 @@ func (at *AutoTrader) GetStatus() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
// Runtime health: safe mode + AI fee wallet, so the dashboard can show a
|
||||
// persistent banner instead of the user digging through logs.
|
||||
safeMode, safeModeReason := at.safeModeState()
|
||||
result["safe_mode"] = safeMode
|
||||
if safeModeReason != "" {
|
||||
result["safe_mode_reason"] = safeModeReason
|
||||
}
|
||||
if status, balance, checkedAt := at.aiWalletHealth(); status != "" {
|
||||
result["ai_wallet_status"] = status
|
||||
result["ai_wallet_balance_usdc"] = balance
|
||||
result["ai_wallet_checked_at"] = checkedAt.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
package trader
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"nofx/kernel"
|
||||
)
|
||||
|
||||
// ensureLongShortCoverage keeps a balanced book each cycle: it fills toward
|
||||
// roughly half the MaxPositions slots long and half short. The AI still drives
|
||||
// selection/sizing whenever it acts; this is a deterministic top-up — if the
|
||||
// AI's decisions plus existing positions fall short of the per-direction target,
|
||||
// the engine force-opens the strongest unused bullish/bearish candidates to
|
||||
// reach it (never exceeding MaxPositions).
|
||||
// forcedCoverageMinScore is the minimum absolute board z-score a candidate
|
||||
// needs before the engine will force-open it for book balance. Near-neutral
|
||||
// signals (|z| < ~0.3) proved a systematic loser, but a 0.75 floor was too
|
||||
// strict: in a long-leaning tape every bearish candidate scored below it, so
|
||||
// no short ever opened and the book became a one-directional long bet that
|
||||
// drew down hard. 0.4 keeps genuine directional signals while still filtering
|
||||
// pure noise, so the book can actually hedge.
|
||||
const forcedCoverageMinScore = 0.4
|
||||
|
||||
// ensureLongShortCoverage tops the book up toward roughly half the
|
||||
// MaxPositions slots long and half short — but only with candidates whose
|
||||
// directional signal is actually strong (see forcedCoverageMinScore). The AI
|
||||
// still drives selection/sizing whenever it acts; this is a deterministic
|
||||
// top-up, and an unbalanced book is preferred over a forced weak trade.
|
||||
//
|
||||
// Forced opens are sized from account equity via applyAutopilotFullSizeOpen and
|
||||
// run through the same code-enforced risk checks (position-value ratio, minimum
|
||||
// size, margin) as any other open. Guards:
|
||||
// - skipped entirely in safe mode (AI unhealthy),
|
||||
// - scoped to the vergex_signal source (the only one with directional bias),
|
||||
// - requires |signal score| >= forcedCoverageMinScore,
|
||||
// - never exceeds MaxPositions,
|
||||
// - never doubles a base symbol already held or already in the decision set.
|
||||
func (at *AutoTrader) ensureLongShortCoverage(decisions []kernel.Decision, ctx *kernel.Context, equity float64) []kernel.Decision {
|
||||
if at == nil || ctx == nil || at.safeMode {
|
||||
if at == nil || ctx == nil || at.isSafeMode() {
|
||||
return decisions
|
||||
}
|
||||
if at.config.StrategyConfig == nil || at.config.StrategyConfig.CoinSource.SourceType != "vergex_signal" {
|
||||
@@ -65,8 +75,9 @@ func (at *AutoTrader) ensureLongShortCoverage(decisions []kernel.Decision, ctx *
|
||||
bullish, bearish := at.strategyEngine.DirectionalCandidates()
|
||||
|
||||
// fill a direction up to its target, drawing from the strongest unused
|
||||
// candidates, never exceeding MaxPositions.
|
||||
fill := func(action string, cands []string, have, target int) {
|
||||
// candidates that clear the signal-strength floor, never exceeding
|
||||
// MaxPositions.
|
||||
fill := func(action string, cands []kernel.DirectionalCandidate, have, target int) {
|
||||
for _, c := range cands {
|
||||
if have >= target {
|
||||
return
|
||||
@@ -74,13 +85,19 @@ func (at *AutoTrader) ensureLongShortCoverage(decisions []kernel.Decision, ctx *
|
||||
if maxPos > 0 && posCount >= maxPos {
|
||||
return
|
||||
}
|
||||
b := universeBaseKey(c)
|
||||
if math.Abs(c.Score) < forcedCoverageMinScore {
|
||||
// candidates are rank-ordered; weaker ones may still follow,
|
||||
// so keep scanning instead of breaking
|
||||
at.logInfof("⚖️ Skipped forced %s %s: signal score %.2f below %.2f floor", action, c.Symbol, c.Score, forcedCoverageMinScore)
|
||||
continue
|
||||
}
|
||||
b := universeBaseKey(c.Symbol)
|
||||
if b == "" || held[b] {
|
||||
continue
|
||||
}
|
||||
d := kernel.Decision{
|
||||
Action: action,
|
||||
Symbol: c,
|
||||
Symbol: c.Symbol,
|
||||
Confidence: 70,
|
||||
Reasoning: "Forced " + action + " to fill the balanced long/short book (autopilot)",
|
||||
}
|
||||
@@ -89,7 +106,7 @@ func (at *AutoTrader) ensureLongShortCoverage(decisions []kernel.Decision, ctx *
|
||||
held[b] = true
|
||||
have++
|
||||
posCount++
|
||||
at.logInfof("⚖️ Forced %s %s (account-sized %.2f USDT, %dx)", action, c, d.PositionSizeUSD, d.Leverage)
|
||||
at.logInfof("⚖️ Forced %s %s (score %.2f, account-sized %.2f USDT, %dx)", action, c.Symbol, c.Score, d.PositionSizeUSD, d.Leverage)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ package trader
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"nofx/kernel"
|
||||
"nofx/logger"
|
||||
"nofx/market"
|
||||
"nofx/mcp/payment"
|
||||
"nofx/provider/hyperliquid"
|
||||
"nofx/store"
|
||||
"nofx/wallet"
|
||||
@@ -141,10 +143,21 @@ func (at *AutoTrader) runCycle() error {
|
||||
record.Success = false
|
||||
record.ErrorMessage = fmt.Sprintf("Failed to get AI decision: %v", err)
|
||||
|
||||
// Payment-layer rejection means the AI fee wallet is definitively out
|
||||
// of funds — surface it as structured health state (GetStatus), not
|
||||
// just a log line.
|
||||
var insufficientFunds *payment.ErrInsufficientFunds
|
||||
if errors.As(err, &insufficientFunds) {
|
||||
at.markAIWalletEmptyFromPayment(insufficientFunds.Balance)
|
||||
record.ErrorMessage = fmt.Sprintf(
|
||||
"AI fee wallet out of funds: balance $%.2f USDC, next call needs ~$%.2f. Top up the Base USDC wallet.",
|
||||
insufficientFunds.Balance, insufficientFunds.Needed,
|
||||
)
|
||||
}
|
||||
|
||||
// Activate safe mode after 3 consecutive failures
|
||||
if at.consecutiveAIFailures >= 3 && !at.safeMode {
|
||||
at.safeMode = true
|
||||
at.safeModeReason = fmt.Sprintf("AI failed %d consecutive times: %v", at.consecutiveAIFailures, err)
|
||||
if at.consecutiveAIFailures >= 3 && !at.isSafeMode() {
|
||||
at.setSafeMode(true, fmt.Sprintf("AI failed %d consecutive times: %v", at.consecutiveAIFailures, err))
|
||||
at.logErrorf("🛡️ SAFE MODE ACTIVATED — AI failed %d times in a row. No new positions will be opened. Existing positions are protected with current stop-loss settings.", at.consecutiveAIFailures)
|
||||
at.logErrorf("🛡️ Reason: %v", err)
|
||||
at.logErrorf("🛡️ Action: Will keep trying AI each cycle. Safe mode auto-deactivates when AI recovers.")
|
||||
@@ -172,7 +185,7 @@ func (at *AutoTrader) runCycle() error {
|
||||
}
|
||||
|
||||
// In safe mode, don't return error — keep the loop running to retry next cycle
|
||||
if at.safeMode {
|
||||
if at.isSafeMode() {
|
||||
at.logWarnf("🛡️ Safe mode: skipping this cycle, will retry in %v", at.config.ScanInterval)
|
||||
return nil
|
||||
}
|
||||
@@ -185,10 +198,9 @@ func (at *AutoTrader) runCycle() error {
|
||||
at.logInfof("✅ AI recovered after %d consecutive failures", at.consecutiveAIFailures)
|
||||
}
|
||||
at.consecutiveAIFailures = 0
|
||||
if at.safeMode {
|
||||
if at.isSafeMode() {
|
||||
at.logInfof("🛡️ SAFE MODE DEACTIVATED — AI is working again. Resuming normal trading.")
|
||||
at.safeMode = false
|
||||
at.safeModeReason = ""
|
||||
at.setSafeMode(false, "")
|
||||
}
|
||||
|
||||
// // 5. Print system prompt
|
||||
@@ -242,7 +254,7 @@ func (at *AutoTrader) runCycle() error {
|
||||
}
|
||||
|
||||
// Safe mode: filter out open positions, only allow close/hold
|
||||
if at.safeMode {
|
||||
if at.isSafeMode() {
|
||||
filtered := make([]kernel.Decision, 0)
|
||||
for _, d := range sortedDecisions {
|
||||
if d.Action == "open_long" || d.Action == "open_short" {
|
||||
@@ -640,7 +652,7 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) {
|
||||
}
|
||||
}
|
||||
// Get trading statistics for AI context
|
||||
stats, err := at.store.Position().GetFullStats(at.id)
|
||||
stats, err := at.store.Position().GetFullStats(at.id, at.initialBalance)
|
||||
if err != nil {
|
||||
at.logWarnf("⚠️ Failed to get trading stats: %v", err)
|
||||
} else if stats == nil {
|
||||
@@ -770,10 +782,12 @@ func (at *AutoTrader) checkClaw402Balance() {
|
||||
balance, err := wallet.QueryUSDCBalance(at.claw402WalletAddr)
|
||||
if err != nil {
|
||||
at.logWarnf("⚠️ Failed to query USDC balance: %v", err)
|
||||
at.markAIWalletHealthUnknown()
|
||||
return
|
||||
}
|
||||
|
||||
if balance < 1.0 {
|
||||
at.setAIWalletHealth(balance)
|
||||
if balance < aiWalletLowThresholdUSDC {
|
||||
at.logWarnf("⚠️ Low USDC balance: $%.2f — AI may stop soon!", balance)
|
||||
}
|
||||
if balance <= 0 {
|
||||
|
||||
@@ -10,21 +10,25 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
autopilotMinHoldDuration = 45 * time.Minute
|
||||
autopilotNoiseCloseHoldDuration = 90 * time.Minute
|
||||
autopilotReentryCooldown = 30 * time.Minute
|
||||
// Allow one long + one short per cycle. The real exposure/churn limits are
|
||||
// MaxPositions (concurrent) + the 45m min-hold + the 90m per-symbol reentry
|
||||
// cooldown, so the per-hour cap only needs to be high enough not to block the
|
||||
// directional pair from re-establishing after positions close. A tight value
|
||||
// here (e.g. 2) starves the strategy: once a couple opens fire, every later
|
||||
// cycle is blocked and the book drains to flat. Keep it generous.
|
||||
autopilotMaxOpensPerHour = 30
|
||||
autopilotMaxOpensPerCycle = 6
|
||||
earlyCloseStopLossBypassPct = -2.5
|
||||
earlyCloseTakeProfitBypassPct = 5.0
|
||||
noiseCloseLossFloorPct = -1.0
|
||||
noiseCloseProfitCeilingPct = 2.0
|
||||
// "Hold for big moves, don't churn" regime. Live history showed the
|
||||
// account bleeding to death by fees: 0.3-0.5% in/out moves where a ~0.14%
|
||||
// round-trip fee ate 30-50% of every small winner. These values force
|
||||
// positions to be held for hours and to develop meaningful moves before
|
||||
// closing, and cut the trade frequency hard.
|
||||
autopilotMinHoldDuration = 4 * time.Hour
|
||||
autopilotNoiseCloseHoldDuration = 8 * time.Hour
|
||||
autopilotReentryCooldown = 3 * time.Hour
|
||||
// Drastically cut churn: at most a couple of new positions per hour/cycle.
|
||||
autopilotMaxOpensPerHour = 3
|
||||
autopilotMaxOpensPerCycle = 2
|
||||
// Wide, asymmetric exits. Cut a loser only at a real -5% (at 5x leverage
|
||||
// that is -25% of margin — survivable), let a winner run to +12% before
|
||||
// any early take-profit. The noise band (-4%..+6%) blocks closing on the
|
||||
// small moves that were grinding the account to nothing.
|
||||
earlyCloseStopLossBypassPct = -5.0
|
||||
earlyCloseTakeProfitBypassPct = 12.0
|
||||
noiseCloseLossFloorPct = -4.0
|
||||
noiseCloseProfitCeilingPct = 6.0
|
||||
)
|
||||
|
||||
func isOpenAction(action string) bool {
|
||||
|
||||
@@ -32,7 +32,8 @@ func TestTradeThrottleBlocksEarlyNoiseClose(t *testing.T) {
|
||||
|
||||
func TestTradeThrottleAllowsEarlyHardStop(t *testing.T) {
|
||||
at := &AutoTrader{}
|
||||
ctx := throttleContext("xyz:INTC", "long", 20*time.Minute, -3.0)
|
||||
// Only a real -5% stop bypasses the min hold now.
|
||||
ctx := throttleContext("xyz:INTC", "long", 20*time.Minute, -6.0)
|
||||
|
||||
reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "close_long"}, ctx, 0)
|
||||
if reason != "" {
|
||||
@@ -42,7 +43,8 @@ func TestTradeThrottleAllowsEarlyHardStop(t *testing.T) {
|
||||
|
||||
func TestTradeThrottleBlocksFlatCloseInsideNoiseWindow(t *testing.T) {
|
||||
at := &AutoTrader{}
|
||||
ctx := throttleContext("xyz:INTC", "long", 60*time.Minute, 0.4)
|
||||
// Held past the 4h min hold but still inside the wide -4%..+6% noise band.
|
||||
ctx := throttleContext("xyz:INTC", "long", 5*time.Hour, 0.4)
|
||||
|
||||
reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "close_long"}, ctx, 0)
|
||||
if !strings.Contains(reason, "noise band") {
|
||||
@@ -52,7 +54,8 @@ func TestTradeThrottleBlocksFlatCloseInsideNoiseWindow(t *testing.T) {
|
||||
|
||||
func TestTradeThrottleAllowsConfirmedLossAfterMinimumHold(t *testing.T) {
|
||||
at := &AutoTrader{}
|
||||
ctx := throttleContext("xyz:INTC", "long", 60*time.Minute, -1.2)
|
||||
// Past the 4h min hold, loss beyond the -4% noise floor → close allowed.
|
||||
ctx := throttleContext("xyz:INTC", "long", 5*time.Hour, -4.5)
|
||||
|
||||
reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "close_long"}, ctx, 0)
|
||||
if reason != "" {
|
||||
@@ -76,13 +79,13 @@ func TestTradeThrottleBlocksOpensOverCycleCap(t *testing.T) {
|
||||
at := &AutoTrader{}
|
||||
ctx := &kernel.Context{}
|
||||
|
||||
// under the 6-per-cycle cap, a further open is allowed
|
||||
if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 5); reason != "" {
|
||||
t.Fatalf("expected open within the 6-per-cycle cap to be allowed, got %q", reason)
|
||||
// under the 2-per-cycle cap, a further open is allowed
|
||||
if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 1); reason != "" {
|
||||
t.Fatalf("expected open within the 2-per-cycle cap to be allowed, got %q", reason)
|
||||
}
|
||||
// at the cap, the next open is blocked
|
||||
if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 6); !strings.Contains(reason, "6 new position") {
|
||||
t.Fatalf("expected open beyond the 6-per-cycle cap to be blocked, got %q", reason)
|
||||
if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 2); !strings.Contains(reason, "2 new position") {
|
||||
t.Fatalf("expected open beyond the 2-per-cycle cap to be blocked, got %q", reason)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,16 @@ func (t *HyperliquidTrader) SyncOrdersFromHyperliquid(traderID string, exchangeI
|
||||
return fmt.Errorf("store is nil")
|
||||
}
|
||||
|
||||
// Get recent trades (last 24 hours)
|
||||
startTime := time.Now().Add(-24 * time.Hour)
|
||||
// Look back 7 days. GetTrades now pulls up to 2000 recent fills (UserFills)
|
||||
// and filters to this window, so a wide lookback backfills any fills missed
|
||||
// during past outages/gaps without dropping recent ones. Dedup by trade ID
|
||||
// keeps re-processing idempotent.
|
||||
startTime := time.Now().Add(-7 * 24 * time.Hour)
|
||||
|
||||
logger.Infof("🔄 Syncing Hyperliquid trades from: %s", startTime.Format(time.RFC3339))
|
||||
|
||||
// Use GetTrades method to fetch trade records
|
||||
trades, err := t.GetTrades(startTime, 1000)
|
||||
trades, err := t.GetTrades(startTime, 2000)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get trades: %w", err)
|
||||
}
|
||||
@@ -133,9 +136,43 @@ func (t *HyperliquidTrader) SyncOrdersFromHyperliquid(traderID string, exchangeI
|
||||
}
|
||||
|
||||
logger.Infof("✅ Order sync completed: %d new trades synced", syncedCount)
|
||||
|
||||
// Reconcile local OPEN rows against the exchange's live book. Without
|
||||
// this, any missed/unmatched fill leaves a zombie OPEN row that swallows
|
||||
// every later close as a "partial close" — its realized PnL then never
|
||||
// reaches the closed-trade statistics. Scoped by exchange account so rows
|
||||
// left by prior autopilot incarnations are healed too.
|
||||
if err := t.reconcilePositions(exchangeID, positionStore); err != nil {
|
||||
logger.Infof("⚠️ Position reconcile skipped: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reconcilePositions builds the live (symbol, side) → quantity map from the
|
||||
// exchange (core perps + xyz dex) and lets the store close/trim any local
|
||||
// OPEN rows on this exchange account the exchange no longer backs.
|
||||
func (t *HyperliquidTrader) reconcilePositions(exchangeID string, positionStore *store.PositionStore) error {
|
||||
livePositions, err := t.GetPositions()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get live positions: %w", err)
|
||||
}
|
||||
|
||||
liveQty := make(map[string]float64, len(livePositions))
|
||||
for _, pos := range livePositions {
|
||||
symbol, _ := pos["symbol"].(string)
|
||||
side, _ := pos["side"].(string)
|
||||
qty, _ := pos["positionAmt"].(float64)
|
||||
if symbol == "" || qty <= 0 {
|
||||
continue
|
||||
}
|
||||
liveQty[store.LivePositionKey(market.Normalize(symbol), side)] += qty
|
||||
}
|
||||
|
||||
_, err = positionStore.ReconcileOpenPositionsWithLive(exchangeID, liveQty)
|
||||
return err
|
||||
}
|
||||
|
||||
// StartOrderSync starts background order sync task
|
||||
func (t *HyperliquidTrader) StartOrderSync(traderID string, exchangeID string, exchangeType string, st *store.Store, interval time.Duration, stop <-chan struct{}) {
|
||||
syncloop.Run(stop, interval, "Hyperliquid", func() error {
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
hl "github.com/sonirico/go-hyperliquid"
|
||||
)
|
||||
|
||||
// GetBalance gets account balance
|
||||
@@ -548,15 +550,23 @@ func (t *HyperliquidTrader) GetClosedPnL(startTime time.Time, limit int) ([]type
|
||||
|
||||
// GetTrades retrieves trade history from Hyperliquid
|
||||
func (t *HyperliquidTrader) GetTrades(startTime time.Time, limit int) ([]types.TradeRecord, error) {
|
||||
// Use UserFillsByTime API
|
||||
// Use UserFills (returns up to 2000 recent fills) rather than
|
||||
// UserFillsByTime, which is hard-capped at 100 fills per response. At
|
||||
// this trading frequency the account exceeds 100 fills/24h, so
|
||||
// UserFillsByTime silently dropped ~20% of fills — skewing recorded PnL
|
||||
// and fees away from the exchange truth. 2000 recent fills covers many
|
||||
// days of history; we filter to startTime client-side.
|
||||
startTimeMs := startTime.UnixMilli()
|
||||
fills, err := t.exchange.Info().UserFillsByTime(t.ctx, t.walletAddr, startTimeMs, nil, nil)
|
||||
fills, err := t.exchange.Info().UserFills(t.ctx, hl.UserFillsParams{Address: t.walletAddr})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user fills: %w", err)
|
||||
}
|
||||
|
||||
var trades []types.TradeRecord
|
||||
for _, fill := range fills {
|
||||
if fill.Time < startTimeMs {
|
||||
continue
|
||||
}
|
||||
price, _ := strconv.ParseFloat(fill.Price, 64)
|
||||
qty, _ := strconv.ParseFloat(fill.Size, 64)
|
||||
fee, _ := strconv.ParseFloat(fill.Fee, 64)
|
||||
|
||||
82
trader/runtime_health.go
Normal file
82
trader/runtime_health.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package trader
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Runtime health state written by the run-loop goroutine and read by the API
|
||||
// layer (GetStatus). Everything here goes through runtimeHealthMu so the
|
||||
// dashboard can poll without racing the trading loop.
|
||||
|
||||
// AI fee wallet health statuses exposed via GetStatus as "ai_wallet_status".
|
||||
const (
|
||||
AIWalletStatusOK = "ok"
|
||||
AIWalletStatusLow = "low"
|
||||
AIWalletStatusEmpty = "empty"
|
||||
AIWalletStatusUnknown = "unknown"
|
||||
)
|
||||
|
||||
// aiWalletLowThresholdUSDC mirrors api.MinAIFeeUSDC: below this the wallet
|
||||
// cannot reliably pay for the next AI/data calls.
|
||||
const aiWalletLowThresholdUSDC = 1.0
|
||||
|
||||
func (at *AutoTrader) setSafeMode(active bool, reason string) {
|
||||
at.runtimeHealthMu.Lock()
|
||||
at.safeMode = active
|
||||
at.safeModeReason = reason
|
||||
at.runtimeHealthMu.Unlock()
|
||||
}
|
||||
|
||||
func (at *AutoTrader) isSafeMode() bool {
|
||||
at.runtimeHealthMu.RLock()
|
||||
defer at.runtimeHealthMu.RUnlock()
|
||||
return at.safeMode
|
||||
}
|
||||
|
||||
func (at *AutoTrader) safeModeState() (bool, string) {
|
||||
at.runtimeHealthMu.RLock()
|
||||
defer at.runtimeHealthMu.RUnlock()
|
||||
return at.safeMode, at.safeModeReason
|
||||
}
|
||||
|
||||
// setAIWalletHealth records a fresh balance observation for the claw402 wallet.
|
||||
func (at *AutoTrader) setAIWalletHealth(balance float64) {
|
||||
status := AIWalletStatusOK
|
||||
switch {
|
||||
case balance <= 0:
|
||||
status = AIWalletStatusEmpty
|
||||
case balance < aiWalletLowThresholdUSDC:
|
||||
status = AIWalletStatusLow
|
||||
}
|
||||
|
||||
at.runtimeHealthMu.Lock()
|
||||
at.aiWalletStatus = status
|
||||
at.aiWalletBalanceUSDC = balance
|
||||
at.aiWalletCheckedAt = time.Now().UTC()
|
||||
at.runtimeHealthMu.Unlock()
|
||||
}
|
||||
|
||||
// markAIWalletHealthUnknown keeps the last observed balance but flags that the
|
||||
// current reading could not be refreshed (e.g. Base RPC unreachable).
|
||||
func (at *AutoTrader) markAIWalletHealthUnknown() {
|
||||
at.runtimeHealthMu.Lock()
|
||||
at.aiWalletStatus = AIWalletStatusUnknown
|
||||
at.aiWalletCheckedAt = time.Now().UTC()
|
||||
at.runtimeHealthMu.Unlock()
|
||||
}
|
||||
|
||||
// markAIWalletEmptyFromPayment records a payment-layer rejection: the wallet
|
||||
// definitively could not cover an AI call.
|
||||
func (at *AutoTrader) markAIWalletEmptyFromPayment(balance float64) {
|
||||
at.runtimeHealthMu.Lock()
|
||||
at.aiWalletStatus = AIWalletStatusEmpty
|
||||
at.aiWalletBalanceUSDC = balance
|
||||
at.aiWalletCheckedAt = time.Now().UTC()
|
||||
at.runtimeHealthMu.Unlock()
|
||||
}
|
||||
|
||||
func (at *AutoTrader) aiWalletHealth() (status string, balance float64, checkedAt time.Time) {
|
||||
at.runtimeHealthMu.RLock()
|
||||
defer at.runtimeHealthMu.RUnlock()
|
||||
return at.aiWalletStatus, at.aiWalletBalanceUSDC, at.aiWalletCheckedAt
|
||||
}
|
||||
200
web/package-lock.json
generated
200
web/package-lock.json
generated
@@ -98,13 +98,13 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
@@ -113,9 +113,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
|
||||
"integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -123,21 +123,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-module-transforms": "^7.28.3",
|
||||
"@babel/helpers": "^7.28.4",
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/traverse": "^7.28.5",
|
||||
"@babel/types": "^7.28.5",
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-compilation-targets": "^7.29.7",
|
||||
"@babel/helper-module-transforms": "^7.29.7",
|
||||
"@babel/helpers": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
@@ -164,14 +164,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
|
||||
"integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
|
||||
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@babel/types": "^7.28.5",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
@@ -181,14 +181,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
|
||||
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.27.2",
|
||||
"@babel/helper-validator-option": "^7.27.1",
|
||||
"@babel/compat-data": "^7.29.7",
|
||||
"@babel/helper-validator-option": "^7.29.7",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
@@ -208,9 +208,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-globals": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
||||
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
|
||||
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -218,29 +218,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
||||
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
|
||||
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
|
||||
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
|
||||
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/traverse": "^7.28.3"
|
||||
"@babel/helper-module-imports": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -260,9 +260,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -270,9 +270,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -280,9 +280,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
||||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -290,27 +290,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
|
||||
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.4"
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
|
||||
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.5"
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -361,33 +361,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/parser": "^7.27.2",
|
||||
"@babel/types": "^7.27.1"
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
|
||||
"integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
|
||||
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.5",
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-globals": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -395,14 +395,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
|
||||
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -4752,16 +4752,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -5102,9 +5102,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -5748,10 +5748,20 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -8522,9 +8532,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
|
||||
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
|
||||
"version": "6.4.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
|
||||
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useAuth } from '../../contexts/AuthContext'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { DeepVoidBackground } from '../common/DeepVoidBackground'
|
||||
import { LanguageSwitcher } from '../common/LanguageSwitcher'
|
||||
|
||||
export function LoginPage() {
|
||||
const { language } = useLanguage()
|
||||
@@ -64,7 +63,6 @@ export function LoginPage() {
|
||||
|
||||
return (
|
||||
<DeepVoidBackground disableAnimation>
|
||||
<LanguageSwitcher />
|
||||
|
||||
{/* Self-contained centering grid — works regardless of parent flex setup */}
|
||||
<main className="flex-1 grid lg:grid-cols-2">
|
||||
|
||||
@@ -143,10 +143,11 @@ export function RegisterPage() {
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold tracking-tighter text-nofx-text uppercase mb-2">
|
||||
<span className="text-nofx-gold">NEW_USER</span> ONBOARDING
|
||||
<span className="text-nofx-gold">CREATE</span> YOUR ACCOUNT
|
||||
</h1>
|
||||
<p className="text-nofx-text-muted text-xs tracking-[0.2em] uppercase">
|
||||
Initializing Registration Sequence...
|
||||
<p className="text-nofx-text-muted text-sm">
|
||||
This account owns your NOFX instance. Next step: a guided launch —
|
||||
about $13 and five minutes to your first AI trade.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -30,8 +30,7 @@ export default function HeaderBar({
|
||||
isLoggedIn = false,
|
||||
isHomePage = false,
|
||||
currentPage,
|
||||
language = 'zh' as Language,
|
||||
onLanguageChange,
|
||||
language = 'en' as Language,
|
||||
user,
|
||||
onLogout,
|
||||
onPageChange,
|
||||
@@ -40,12 +39,10 @@ export default function HeaderBar({
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
const [languageDropdownOpen, setLanguageDropdownOpen] = useState(false)
|
||||
const [userDropdownOpen, setUserDropdownOpen] = useState(false)
|
||||
const [userMode, setUserModeState] = useState<UserMode>(
|
||||
() => getUserMode() ?? 'advanced'
|
||||
)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const userDropdownRef = useRef<HTMLDivElement>(null)
|
||||
const resolvedCurrentPage =
|
||||
currentPage ?? getCurrentPageForPath(location.pathname)
|
||||
@@ -63,12 +60,6 @@ export default function HeaderBar({
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setLanguageDropdownOpen(false)
|
||||
}
|
||||
if (
|
||||
userDropdownRef.current &&
|
||||
!userDropdownRef.current.contains(event.target as Node)
|
||||
@@ -357,56 +348,7 @@ export default function HeaderBar({
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Language Toggle - Always at the rightmost */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setLanguageDropdownOpen(!languageDropdownOpen)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded transition-colors text-nofx-text-muted hover:bg-white/5"
|
||||
>
|
||||
<span className="text-lg">
|
||||
{language === 'zh' ? '🇨🇳' : language === 'id' ? '🇮🇩' : '🇺🇸'}
|
||||
</span>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{languageDropdownOpen && (
|
||||
<div className="absolute right-0 top-full mt-2 w-32 rounded-lg shadow-lg overflow-hidden z-50 bg-nofx-bg-lighter border border-nofx-gold/20">
|
||||
<button
|
||||
onClick={() => {
|
||||
onLanguageChange?.('zh')
|
||||
setLanguageDropdownOpen(false)
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-nofx-text
|
||||
${language === 'zh' ? 'bg-nofx-gold/10' : 'hover:bg-[rgba(26,24,19,0.06)]'}`}
|
||||
>
|
||||
<span className="text-base">🇨🇳</span>
|
||||
<span className="text-sm">Chinese</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onLanguageChange?.('en')
|
||||
setLanguageDropdownOpen(false)
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-nofx-text
|
||||
${language === 'en' ? 'bg-nofx-gold/10' : 'hover:bg-[rgba(26,24,19,0.06)]'}`}
|
||||
>
|
||||
<span className="text-base">🇺🇸</span>
|
||||
<span className="text-sm">English</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onLanguageChange?.('id')
|
||||
setLanguageDropdownOpen(false)
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-nofx-text
|
||||
${language === 'id' ? 'bg-nofx-gold/10' : 'hover:bg-[rgba(26,24,19,0.06)]'}`}
|
||||
>
|
||||
<span className="text-base">🇮🇩</span>
|
||||
<span className="text-sm">Bahasa</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Language switcher removed — the product UI is English-only. */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -619,28 +561,8 @@ export default function HeaderBar({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Account / Lang */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Lang Switcher */}
|
||||
<div className="flex bg-zinc-900 rounded-lg p-1 border border-zinc-800">
|
||||
{['zh', 'en', 'id'].map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
onClick={() => {
|
||||
onLanguageChange?.(lang as Language)
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className={`flex-1 py-3 text-sm font-bold rounded-md transition-colors ${
|
||||
language === lang
|
||||
? 'bg-zinc-800 text-white shadow-sm'
|
||||
: 'text-zinc-500'
|
||||
}`}
|
||||
>
|
||||
{lang === 'zh' ? 'CN' : lang === 'id' ? 'ID' : 'EN'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Account (language switcher removed — English-only UI) */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{/* Auth Actions */}
|
||||
{isLoggedIn && user ? (
|
||||
<button
|
||||
|
||||
@@ -239,22 +239,31 @@ export function HyperliquidWalletConnect({
|
||||
const [agentInfo, setAgentInfo] = useState<HyperliquidAgentInfo | null>(null)
|
||||
const [agentInfoLoading, setAgentInfoLoading] = useState(false)
|
||||
const [hasWalletProvider, setHasWalletProvider] = useState(false)
|
||||
// Address of a fully-authorized hyperliquid exchange saved on the SERVER.
|
||||
// The local FlowState lives in this browser's localStorage, so a fresh
|
||||
// browser would otherwise show the red "Connect" CTA even though trading
|
||||
// authorization is complete and the bot is running.
|
||||
const [serverExchangeAddr, setServerExchangeAddr] = useState('')
|
||||
const text = useMemo(
|
||||
() => ({
|
||||
title: language === 'zh' ? 'Hyperliquid Wallet' : 'Hyperliquid Wallet',
|
||||
connect: language === 'zh' ? 'Connect Hyperliquid' : 'Connect Hyperliquid',
|
||||
connected: language === 'zh' ? 'Connected' : 'Connected',
|
||||
mainWallet: language === 'zh' ? 'EVM main wallet' : 'EVM main wallet',
|
||||
mainWallet: language === 'zh' ? 'Connect your wallet' : 'Connect your wallet',
|
||||
generateAgent:
|
||||
language === 'zh'
|
||||
? 'Generate NOFX agent wallet'
|
||||
: 'Generate NOFX agent wallet',
|
||||
? 'Create a trading key for NOFX'
|
||||
: 'Create a trading key for NOFX',
|
||||
approveAgent:
|
||||
language === 'zh' ? 'Authorize agent trading' : 'Authorize agent trading',
|
||||
language === 'zh'
|
||||
? 'Approve it in your wallet (trade-only, cannot withdraw)'
|
||||
: 'Approve it in your wallet (trade-only, cannot withdraw)',
|
||||
approveBuilder:
|
||||
language === 'zh' ? 'Finalize trading authorization' : 'Finalize trading authorization',
|
||||
save: language === 'zh' ? 'Save to NOFX' : 'Save to NOFX',
|
||||
done: language === 'zh' ? 'Flow complete' : 'Flow complete',
|
||||
language === 'zh'
|
||||
? 'Approve the small per-trade builder fee'
|
||||
: 'Approve the small per-trade builder fee',
|
||||
save: language === 'zh' ? 'Save to NOFX — done' : 'Save to NOFX — done',
|
||||
done: language === 'zh' ? 'All set — trading authorized' : 'All set — trading authorized',
|
||||
balance: language === 'zh' ? 'Hyperliquid balance' : 'Hyperliquid balance',
|
||||
withdrawable: language === 'zh' ? 'Withdrawable' : 'Withdrawable',
|
||||
equity: language === 'zh' ? 'Equity' : 'Equity',
|
||||
@@ -300,6 +309,31 @@ export function HyperliquidWalletConnect({
|
||||
saveState(state)
|
||||
}, [state])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
setServerExchangeAddr('')
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
api
|
||||
.getExchangeConfigs()
|
||||
.then((configs) => {
|
||||
if (cancelled) return
|
||||
const ready = configs.find(
|
||||
(exchange) =>
|
||||
exchange.exchange_type === 'hyperliquid' &&
|
||||
exchange.enabled &&
|
||||
Boolean(exchange.hyperliquidBuilderApproved) &&
|
||||
(exchange.hyperliquidWalletAddr || '').trim() !== ''
|
||||
)
|
||||
setServerExchangeAddr(ready?.hyperliquidWalletAddr || '')
|
||||
})
|
||||
.catch(() => undefined)
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [isLoggedIn])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn || !state.mainWallet) return
|
||||
let cancelled = false
|
||||
@@ -493,6 +527,11 @@ export function HyperliquidWalletConnect({
|
||||
const complete = Boolean(
|
||||
state.mainWallet && state.savedExchangeId && state.builderApproved
|
||||
)
|
||||
// Trigger shows "connected" when either this browser finished the flow or
|
||||
// the server already holds a fully-authorized exchange.
|
||||
const connectedAddr = complete
|
||||
? state.mainWallet
|
||||
: serverExchangeAddr || undefined
|
||||
|
||||
async function connectWallet() {
|
||||
setError('')
|
||||
@@ -898,14 +937,14 @@ export function HyperliquidWalletConnect({
|
||||
type="button"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-bold transition-all border ${
|
||||
complete
|
||||
connectedAddr
|
||||
? 'bg-nofx-success/10 border-nofx-success/30 text-nofx-success'
|
||||
: 'bg-nofx-gold/10 border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/20'
|
||||
}`}
|
||||
>
|
||||
<Wallet className="w-4 h-4" />
|
||||
<span>
|
||||
{complete ? shortAddress(state.mainWallet) : text.connect}
|
||||
{connectedAddr ? shortAddress(connectedAddr) : text.connect}
|
||||
</span>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
@@ -1,19 +1,96 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
import type { FAQCategory } from '../../data/faqData'
|
||||
// RoadmapWidget: removed dynamic embedding; only show external links on demand
|
||||
import { Fragment, useEffect, useRef } from 'react'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import type { FAQBlock, FAQCategory } from '../../data/faqData'
|
||||
|
||||
interface FAQContentProps {
|
||||
categories: FAQCategory[]
|
||||
language: Language
|
||||
onActiveItemChange: (itemId: string) => void
|
||||
}
|
||||
|
||||
export function FAQContent({
|
||||
categories,
|
||||
language,
|
||||
onActiveItemChange,
|
||||
}: FAQContentProps) {
|
||||
/** Renders text with inline `code` spans (backtick syntax). */
|
||||
function InlineText({ text }: { text: string }) {
|
||||
const parts = text.split(/(`[^`]+`)/g)
|
||||
return (
|
||||
<>
|
||||
{parts.map((part, i) =>
|
||||
part.startsWith('`') && part.endsWith('`') ? (
|
||||
<code
|
||||
key={i}
|
||||
className="rounded bg-nofx-bg-deeper border border-[rgba(26,24,19,0.10)] px-1.5 py-0.5 font-mono text-[0.85em] text-nofx-text break-all"
|
||||
>
|
||||
{part.slice(1, -1)}
|
||||
</code>
|
||||
) : (
|
||||
<Fragment key={i}>{part}</Fragment>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Block({ block }: { block: FAQBlock }) {
|
||||
switch (block.type) {
|
||||
case 'p':
|
||||
return (
|
||||
<p className="text-sm leading-6 text-nofx-text-muted">
|
||||
<InlineText text={block.text} />
|
||||
</p>
|
||||
)
|
||||
case 'list':
|
||||
return (
|
||||
<ul className="space-y-1.5">
|
||||
{block.items.map((item, i) => (
|
||||
<li key={i} className="flex gap-2 text-sm leading-6 text-nofx-text-muted">
|
||||
<span className="mt-[9px] h-1 w-1 shrink-0 rounded-full bg-nofx-gold" />
|
||||
<span>
|
||||
<InlineText text={item} />
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
case 'steps':
|
||||
return (
|
||||
<ol className="space-y-1.5">
|
||||
{block.items.map((item, i) => (
|
||||
<li key={i} className="flex gap-3 text-sm leading-6 text-nofx-text-muted">
|
||||
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded border border-nofx-gold/30 bg-nofx-gold/10 font-mono text-[11px] font-bold text-nofx-gold">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span>
|
||||
<InlineText text={item} />
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)
|
||||
case 'note':
|
||||
return (
|
||||
<div className="border-l-2 border-nofx-gold bg-nofx-gold/10 px-3 py-2 text-sm leading-6 text-nofx-text">
|
||||
<InlineText text={block.text} />
|
||||
</div>
|
||||
)
|
||||
case 'links':
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{block.links.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1.5 rounded border border-nofx-gold/25 bg-nofx-bg-deeper px-2.5 py-1 font-mono text-xs font-semibold text-nofx-gold hover:border-nofx-gold/50 hover:bg-nofx-gold/10"
|
||||
>
|
||||
{link.label}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function FAQContent({ categories, onActiveItemChange }: FAQContentProps) {
|
||||
const sectionRefs = useRef<Map<string, HTMLElement>>(new Map())
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,419 +99,60 @@ export function FAQContent({
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const itemId = entry.target.getAttribute('data-item-id')
|
||||
if (itemId) {
|
||||
onActiveItemChange(itemId)
|
||||
}
|
||||
if (itemId) onActiveItemChange(itemId)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
rootMargin: '-100px 0px -80% 0px',
|
||||
threshold: 0,
|
||||
}
|
||||
{ rootMargin: '-100px 0px -80% 0px', threshold: 0 }
|
||||
)
|
||||
|
||||
sectionRefs.current.forEach((ref) => {
|
||||
if (ref) observer.observe(ref)
|
||||
})
|
||||
|
||||
sectionRefs.current.forEach((ref) => observer.observe(ref))
|
||||
return () => {
|
||||
sectionRefs.current.forEach((ref) => {
|
||||
if (ref) observer.unobserve(ref)
|
||||
})
|
||||
sectionRefs.current.forEach((ref) => observer.unobserve(ref))
|
||||
}
|
||||
}, [onActiveItemChange])
|
||||
}, [onActiveItemChange, categories])
|
||||
|
||||
const setRef = (itemId: string, element: HTMLElement | null) => {
|
||||
if (element) {
|
||||
sectionRefs.current.set(itemId, element)
|
||||
} else {
|
||||
sectionRefs.current.delete(itemId)
|
||||
}
|
||||
if (element) sectionRefs.current.set(itemId, element)
|
||||
else sectionRefs.current.delete(itemId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
<div className="space-y-8">
|
||||
{categories.map((category) => (
|
||||
<div key={category.id} className="nofx-glass p-8 rounded-xl border border-[rgba(26,24,19,0.14)]">
|
||||
{/* Category Header */}
|
||||
<div className="flex items-center gap-3 mb-6 pb-3 border-b border-[rgba(26,24,19,0.14)]">
|
||||
<category.icon className="w-7 h-7 text-nofx-gold" />
|
||||
<h2 className="text-2xl font-bold text-nofx-text">
|
||||
{t(category.titleKey, language)}
|
||||
<div
|
||||
key={category.id}
|
||||
id={category.id}
|
||||
className="overflow-hidden rounded-xl border border-nofx-gold/20 bg-nofx-bg-lighter"
|
||||
>
|
||||
{/* category header — terminal small-caps strip */}
|
||||
<div className="flex items-center gap-2.5 border-b border-nofx-gold/20 bg-nofx-bg px-5 py-3 md:px-6">
|
||||
<category.icon className="h-4 w-4 text-nofx-gold" />
|
||||
<h2 className="font-mono text-xs font-bold uppercase tracking-[0.18em] text-nofx-text">
|
||||
{category.title}
|
||||
</h2>
|
||||
<span className="ml-auto font-mono text-[10px] uppercase tracking-[0.12em] text-nofx-text-muted">
|
||||
{category.items.length} {category.items.length === 1 ? 'entry' : 'entries'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* FAQ Items */}
|
||||
<div className="space-y-8">
|
||||
<div className="divide-y divide-[rgba(26,24,19,0.10)]">
|
||||
{category.items.map((item) => (
|
||||
<section
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
data-item-id={item.id}
|
||||
ref={(el) => setRef(item.id, el)}
|
||||
className="scroll-mt-24"
|
||||
className="scroll-mt-24 px-5 py-5 md:px-6"
|
||||
>
|
||||
{/* Question */}
|
||||
<h3 className="text-xl font-semibold mb-3 text-nofx-text">
|
||||
{t(item.questionKey, language)}
|
||||
<h3 className="mb-3 text-[15px] font-semibold leading-6 text-nofx-text">
|
||||
{item.question}
|
||||
</h3>
|
||||
|
||||
{/* Answer */}
|
||||
<div className="prose prose-invert max-w-none text-nofx-text-muted leading-relaxed">
|
||||
{item.id === 'github-projects-tasks' ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-base">
|
||||
{language === 'zh' ? 'Links:' : 'Links:'}{' '}
|
||||
<a
|
||||
href="https://github.com/orgs/NoFxAiOS/projects/3"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#E0483B' }}
|
||||
>
|
||||
{language === 'zh' ? 'Roadmap' : 'Roadmap'}
|
||||
</a>
|
||||
{' | '}
|
||||
<a
|
||||
href="https://github.com/orgs/NoFxAiOS/projects/5"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#E0483B' }}
|
||||
>
|
||||
{language === 'zh' ? 'Task Dashboard' : 'Task Dashboard'}
|
||||
</a>
|
||||
</div>
|
||||
<ol className="list-decimal pl-5 space-y-1 text-base">
|
||||
{language === 'zh' ? (
|
||||
<>
|
||||
<li>
|
||||
Open the links above and filter by labels (good
|
||||
first issue / help wanted / frontend / backend).
|
||||
</li>
|
||||
<li>
|
||||
Open the task and read the Description &
|
||||
Acceptance Criteria.
|
||||
</li>
|
||||
<li>
|
||||
Comment "assign me" or self-assign (if permitted).
|
||||
</li>
|
||||
<li>Fork the repository to your GitHub account.</li>
|
||||
<li>
|
||||
Sync your fork's <code>dev</code> with upstream:
|
||||
<code className="ml-2">
|
||||
git remote add upstream
|
||||
https://github.com/NoFxAiOS/nofx.git
|
||||
</code>
|
||||
<br />
|
||||
<code>git fetch upstream</code>
|
||||
<br />
|
||||
<code>git checkout dev</code>
|
||||
<br />
|
||||
<code>git rebase upstream/dev</code>
|
||||
<br />
|
||||
<code>git push origin dev</code>
|
||||
</li>
|
||||
<li>
|
||||
Create a feature branch from your fork's{' '}
|
||||
<code>dev</code>:
|
||||
<code className="ml-2">
|
||||
git checkout -b feat/your-topic
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
Push to your fork:
|
||||
<code className="ml-2">
|
||||
git push origin feat/your-topic
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
Open a PR: base <code>NoFxAiOS/nofx:dev</code> ←
|
||||
compare{' '}
|
||||
<code>your-username/nofx:feat/your-topic</code>.
|
||||
</li>
|
||||
<li>
|
||||
In PR, reference the Issue (e.g.,{' '}
|
||||
<code className="ml-1">Closes #123</code>) and
|
||||
choose the proper PR template; rebase onto{' '}
|
||||
<code>upstream/dev</code> as needed.
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li>
|
||||
Open the links above and filter by labels (good
|
||||
first issue / help wanted / frontend / backend).
|
||||
</li>
|
||||
<li>
|
||||
Open the task and read the Description &
|
||||
Acceptance Criteria.
|
||||
</li>
|
||||
<li>
|
||||
Comment "assign me" or self-assign (if permitted).
|
||||
</li>
|
||||
<li>Fork the repository to your GitHub account.</li>
|
||||
<li>
|
||||
Sync your fork's <code>dev</code> with upstream:
|
||||
<code className="ml-2">
|
||||
git remote add upstream
|
||||
https://github.com/NoFxAiOS/nofx.git
|
||||
</code>
|
||||
<br />
|
||||
<code>git fetch upstream</code>
|
||||
<br />
|
||||
<code>git checkout dev</code>
|
||||
<br />
|
||||
<code>git rebase upstream/dev</code>
|
||||
<br />
|
||||
<code>git push origin dev</code>
|
||||
</li>
|
||||
<li>
|
||||
Create a feature branch from your fork's{' '}
|
||||
<code>dev</code>:
|
||||
<code className="ml-2">
|
||||
git checkout -b feat/your-topic
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
Push to your fork:
|
||||
<code className="ml-2">
|
||||
git push origin feat/your-topic
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
Open a PR: base <code>NoFxAiOS/nofx:dev</code> ←
|
||||
compare{' '}
|
||||
<code>your-username/nofx:feat/your-topic</code>.
|
||||
</li>
|
||||
<li>
|
||||
In PR, reference the Issue (e.g.,{' '}
|
||||
<code className="ml-1">Closes #123</code>) and
|
||||
choose the proper PR template; rebase onto{' '}
|
||||
<code>upstream/dev</code> as needed.
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ol>
|
||||
|
||||
<div
|
||||
className="rounded p-3 mt-3"
|
||||
style={{
|
||||
background: 'rgba(224, 72, 59, 0.08)',
|
||||
border: '1px solid rgba(224, 72, 59, 0.25)',
|
||||
}}
|
||||
>
|
||||
{language === 'zh' ? (
|
||||
<div className="text-sm">
|
||||
<strong style={{ color: '#E0483B' }}>Note:</strong>{' '}
|
||||
Contribution incentives are available (e.g., cash
|
||||
bounties, badges & shout-outs, priority
|
||||
review/merge, beta access). Prefer tasks with
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#E0483B' }}
|
||||
>
|
||||
bounty label
|
||||
</a>
|
||||
, or file a
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#E0483B' }}
|
||||
>
|
||||
Bounty Claim
|
||||
</a>
|
||||
after completion.
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm">
|
||||
<strong style={{ color: '#E0483B' }}>Note:</strong>{' '}
|
||||
Contribution incentives are available (e.g., cash
|
||||
bounties, badges & shout-outs, priority
|
||||
review/merge, beta access). Prefer tasks with
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#E0483B' }}
|
||||
>
|
||||
bounty label
|
||||
</a>
|
||||
, or file a
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#E0483B' }}
|
||||
>
|
||||
Bounty Claim
|
||||
</a>
|
||||
after completion.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : item.id === 'contribute-pr-guidelines' ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-base">
|
||||
{language === 'zh' ? 'References:' : 'References:'}{' '}
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/CONTRIBUTING.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-nofx-gold hover:underline"
|
||||
>
|
||||
CONTRIBUTING.md
|
||||
</a>
|
||||
{' | '}
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/PR_TITLE_GUIDE.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-nofx-gold hover:underline"
|
||||
>
|
||||
PR_TITLE_GUIDE.md
|
||||
</a>
|
||||
</div>
|
||||
<ol className="list-decimal pl-5 space-y-1 text-base">
|
||||
{language === 'zh' ? (
|
||||
<>
|
||||
<li>
|
||||
After forking, branch from your fork's{' '}
|
||||
<code>dev</code>; avoid direct commits to upstream{' '}
|
||||
<code>main</code>.
|
||||
</li>
|
||||
<li>
|
||||
Branch naming: feat/…, fix/…, docs/…; commit
|
||||
messages follow Conventional Commits.
|
||||
</li>
|
||||
<li>
|
||||
Run checks before PR:
|
||||
<code className="ml-2">
|
||||
npm --prefix web run lint && npm --prefix web
|
||||
run build
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
For UI changes, attach screenshots or a short
|
||||
video.
|
||||
</li>
|
||||
<li>
|
||||
Choose the proper PR template
|
||||
(frontend/backend/docs/general).
|
||||
</li>
|
||||
<li>
|
||||
Link the Issue in PR (e.g.,{' '}
|
||||
<code className="ml-1">Closes #123</code>) and
|
||||
target <code>NoFxAiOS/nofx:dev</code>.
|
||||
</li>
|
||||
<li>
|
||||
Keep rebasing onto <code>upstream/dev</code>,
|
||||
ensure CI passes; prefer small and focused PRs.
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li>
|
||||
After forking, branch from your fork's{' '}
|
||||
<code>dev</code>; avoid direct commits to upstream{' '}
|
||||
<code>main</code>.
|
||||
</li>
|
||||
<li>
|
||||
Branch naming: feat/…, fix/…, docs/…; commit
|
||||
messages follow Conventional Commits.
|
||||
</li>
|
||||
<li>
|
||||
Run checks before PR:
|
||||
<code className="ml-2">
|
||||
npm --prefix web run lint && npm --prefix web
|
||||
run build
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
For UI changes, attach screenshots or a short
|
||||
video.
|
||||
</li>
|
||||
<li>
|
||||
Choose the proper PR template
|
||||
(frontend/backend/docs/general).
|
||||
</li>
|
||||
<li>
|
||||
Link the Issue in PR (e.g.,{' '}
|
||||
<code className="ml-1">Closes #123</code>) and
|
||||
target <code>NoFxAiOS/nofx:dev</code>.
|
||||
</li>
|
||||
<li>
|
||||
Keep rebasing onto <code>upstream/dev</code>,
|
||||
ensure CI passes; prefer small and focused PRs.
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ol>
|
||||
|
||||
<div className="rounded p-3 mt-3 bg-nofx-gold/10 border border-nofx-gold/25">
|
||||
{language === 'zh' ? (
|
||||
<div className="text-sm">
|
||||
<strong className="text-nofx-gold">Note:</strong>{' '}
|
||||
We offer contribution incentives (bounties, badges,
|
||||
shout-outs, priority review/merge, beta access).
|
||||
Look for tasks with
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#E0483B' }}
|
||||
>
|
||||
bounty label
|
||||
</a>
|
||||
, or submit a
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#E0483B' }}
|
||||
>
|
||||
Bounty Claim template
|
||||
</a>
|
||||
when ready.
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm">
|
||||
<strong style={{ color: '#E0483B' }}>Note:</strong>{' '}
|
||||
We offer contribution incentives (bounties, badges,
|
||||
shout-outs, priority review/merge, beta access).
|
||||
Look for tasks with
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#E0483B' }}
|
||||
>
|
||||
bounty label
|
||||
</a>
|
||||
, or submit a
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#E0483B' }}
|
||||
>
|
||||
Bounty Claim
|
||||
</a>
|
||||
when ready.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-base">{t(item.answerKey, language)}</p>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
{item.blocks.map((block, i) => (
|
||||
<Block key={i} block={block} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="mt-6 h-px bg-[rgba(26,24,19,0.14)]" />
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,158 +1,121 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { HelpCircle } from 'lucide-react'
|
||||
import { DeepVoidBackground } from '../common/DeepVoidBackground'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
import { FAQSearchBar } from './FAQSearchBar'
|
||||
import { FAQSidebar } from './FAQSidebar'
|
||||
import { FAQContent } from './FAQContent'
|
||||
import { faqCategories } from '../../data/faqData'
|
||||
import { faqCategories, faqItemSearchText } from '../../data/faqData'
|
||||
import type { FAQCategory } from '../../data/faqData'
|
||||
|
||||
interface FAQLayoutProps {
|
||||
language: Language
|
||||
}
|
||||
|
||||
export function FAQLayout({ language }: FAQLayoutProps) {
|
||||
export function FAQLayout() {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [activeItemId, setActiveItemId] = useState<string | null>(null)
|
||||
|
||||
// Filter categories based on search term
|
||||
const filteredCategories = useMemo(() => {
|
||||
if (!searchTerm.trim()) {
|
||||
return faqCategories
|
||||
}
|
||||
if (!searchTerm.trim()) return faqCategories
|
||||
|
||||
const term = searchTerm.toLowerCase()
|
||||
const filtered: FAQCategory[] = []
|
||||
|
||||
faqCategories.forEach((category) => {
|
||||
const matchingItems = category.items.filter((item) => {
|
||||
const question = t(item.questionKey, language).toLowerCase()
|
||||
const answer = t(item.answerKey, language).toLowerCase()
|
||||
return question.includes(term) || answer.includes(term)
|
||||
})
|
||||
|
||||
const matchingItems = category.items.filter((item) =>
|
||||
faqItemSearchText(item).includes(term)
|
||||
)
|
||||
if (matchingItems.length > 0) {
|
||||
filtered.push({
|
||||
...category,
|
||||
items: matchingItems,
|
||||
})
|
||||
filtered.push({ ...category, items: matchingItems })
|
||||
}
|
||||
})
|
||||
|
||||
return filtered
|
||||
}, [searchTerm, language])
|
||||
}, [searchTerm])
|
||||
|
||||
const totalItems = useMemo(
|
||||
() => faqCategories.reduce((sum, category) => sum + category.items.length, 0),
|
||||
[]
|
||||
)
|
||||
|
||||
const handleItemClick = (_categoryId: string, itemId: string) => {
|
||||
const element = document.getElementById(itemId)
|
||||
if (element) {
|
||||
const offset = 100
|
||||
const elementPosition = element.getBoundingClientRect().top
|
||||
const offsetPosition = elementPosition + window.pageYOffset - offset
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
if (!element) return
|
||||
const offset = 100
|
||||
const top =
|
||||
element.getBoundingClientRect().top + window.pageYOffset - offset
|
||||
window.scrollTo({ top, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
return (
|
||||
<DeepVoidBackground className="py-6 pt-24" disableAnimation>
|
||||
<div className="w-full px-4 md:px-8">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="flex items-center justify-center gap-3 mb-4">
|
||||
<div className="w-16 h-16 rounded-full flex items-center justify-center bg-nofx-gold shadow-lg">
|
||||
<HelpCircle className="w-8 h-8 text-nofx-bg" />
|
||||
<DeepVoidBackground className="py-8 pt-24" disableAnimation>
|
||||
<div className="mx-auto w-full max-w-6xl px-4 md:px-8">
|
||||
{/* page header — same strip language as the other terminal pages */}
|
||||
<div className="mb-8 border-b border-nofx-gold/20 pb-6">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl border border-nofx-gold/30 bg-nofx-bg-lighter text-nofx-gold md:h-14 md:w-14">
|
||||
<HelpCircle className="h-6 w-6 md:h-7 md:w-7" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-mono text-2xl font-bold tracking-tight text-nofx-text md:text-3xl">
|
||||
FAQ
|
||||
</h1>
|
||||
<p className="mt-1 font-mono text-xs uppercase tracking-[0.14em] text-nofx-text-muted">
|
||||
{totalItems} answers · wallets · launch · trading · self-hosting
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full md:w-80">
|
||||
<FAQSearchBar
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4 text-nofx-text">
|
||||
{t('faqTitle', language)}
|
||||
</h1>
|
||||
<p className="text-lg mb-8 text-nofx-text-muted">
|
||||
{t('faqSubtitle', language)}
|
||||
</p>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<FAQSearchBar
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
placeholder={
|
||||
language === 'zh' ? 'Search FAQ...' : 'Search FAQ...'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
{/* content */}
|
||||
<div className="flex gap-8">
|
||||
{/* Sidebar - Hidden on mobile, visible on desktop */}
|
||||
<aside className="hidden lg:block w-64 flex-shrink-0">
|
||||
<aside className="hidden w-64 flex-shrink-0 lg:block">
|
||||
<FAQSidebar
|
||||
categories={filteredCategories}
|
||||
activeItemId={activeItemId}
|
||||
language={language}
|
||||
onItemClick={handleItemClick}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
{/* Content Area */}
|
||||
<main className="flex-1 min-w-0">
|
||||
<main className="min-w-0 flex-1">
|
||||
{filteredCategories.length > 0 ? (
|
||||
<FAQContent
|
||||
categories={filteredCategories}
|
||||
language={language}
|
||||
onActiveItemChange={setActiveItemId}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-lg" style={{ color: '#8A8478' }}>
|
||||
{language === 'zh'
|
||||
? 'No matching questions found'
|
||||
: 'No matching questions found'}
|
||||
<div className="rounded-xl border border-nofx-gold/20 bg-nofx-bg-lighter py-16 text-center">
|
||||
<p className="font-mono text-sm text-nofx-text-muted">
|
||||
No matching questions for “{searchTerm}”.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setSearchTerm('')}
|
||||
className="mt-4 px-6 py-2 rounded-lg font-semibold transition-all hover:opacity-90"
|
||||
style={{
|
||||
background: '#E0483B',
|
||||
color: '#F1ECE2',
|
||||
}}
|
||||
className="mt-4 rounded-lg border border-nofx-gold/30 bg-nofx-gold/10 px-5 py-2 font-mono text-xs font-bold uppercase tracking-[0.12em] text-nofx-gold hover:bg-nofx-gold/20"
|
||||
>
|
||||
{language === 'zh' ? 'Clear Search' : 'Clear Search'}
|
||||
Clear search
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Contact Section */}
|
||||
<div
|
||||
className="mt-16 p-8 rounded-lg text-center"
|
||||
style={{
|
||||
background: 'rgba(224, 72, 59, 0.08)',
|
||||
border: '1px solid rgba(224, 72, 59, 0.2)',
|
||||
}}
|
||||
>
|
||||
<h3 className="text-xl font-bold mb-3" style={{ color: '#1A1813' }}>
|
||||
{t('faqStillHaveQuestions', language)}
|
||||
{/* still stuck */}
|
||||
<div className="mt-12 rounded-xl border border-nofx-gold/20 bg-nofx-bg-lighter p-6 text-center md:p-8">
|
||||
<h3 className="font-mono text-sm font-bold uppercase tracking-[0.16em] text-nofx-text">
|
||||
Still have questions?
|
||||
</h3>
|
||||
<p className="mb-6" style={{ color: '#8A8478' }}>
|
||||
{t('faqContactUs', language)}
|
||||
<p className="mt-2 text-sm text-nofx-text-muted">
|
||||
Ask in the community or open an issue — both are answered by the
|
||||
people building NOFX.
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<div className="mt-5 flex items-center justify-center gap-3">
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: '#F7F4EC',
|
||||
color: '#1A1813',
|
||||
border: '1px solid rgba(26,24,19,0.14)',
|
||||
}}
|
||||
className="rounded-lg border border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper px-5 py-2.5 font-mono text-xs font-bold uppercase tracking-[0.12em] text-nofx-text hover:border-nofx-gold/40"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
@@ -160,13 +123,9 @@ export function FAQLayout({ language }: FAQLayoutProps) {
|
||||
href="https://t.me/nofx_dev_community"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: '#E0483B',
|
||||
color: '#F1ECE2',
|
||||
}}
|
||||
className="rounded-lg bg-nofx-gold px-5 py-2.5 font-mono text-xs font-bold uppercase tracking-[0.12em] text-white hover:bg-nofx-accent"
|
||||
>
|
||||
{t('community', language)}
|
||||
Telegram community
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,52 +1,44 @@
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
import type { FAQCategory } from '../../data/faqData'
|
||||
|
||||
interface FAQSidebarProps {
|
||||
categories: FAQCategory[]
|
||||
activeItemId: string | null
|
||||
language: Language
|
||||
onItemClick: (categoryId: string, itemId: string) => void
|
||||
}
|
||||
|
||||
export function FAQSidebar({
|
||||
categories,
|
||||
activeItemId,
|
||||
language,
|
||||
onItemClick,
|
||||
}: FAQSidebarProps) {
|
||||
return (
|
||||
<nav
|
||||
className="sticky top-24 h-[calc(100vh-120px)] overflow-y-auto pr-4"
|
||||
style={{
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: '#E8E2D5 #F7F4EC',
|
||||
}}
|
||||
className="sticky top-24 h-[calc(100vh-120px)] overflow-y-auto pr-2"
|
||||
style={{ scrollbarWidth: 'thin', scrollbarColor: '#E8E2D5 transparent' }}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-5">
|
||||
{categories.map((category) => (
|
||||
<div key={category.id} className="nofx-glass p-4 rounded-xl border border-[rgba(26,24,19,0.14)]">
|
||||
{/* Category Title */}
|
||||
<div className="flex items-center gap-2 mb-3 px-3">
|
||||
<category.icon className="w-5 h-5 text-nofx-gold" />
|
||||
<h3 className="text-sm font-bold uppercase tracking-wide text-nofx-gold">
|
||||
{t(category.titleKey, language)}
|
||||
<div key={category.id}>
|
||||
<div className="mb-2 flex items-center gap-2 px-2">
|
||||
<category.icon className="h-3.5 w-3.5 text-nofx-gold" />
|
||||
<h3 className="font-mono text-[11px] font-bold uppercase tracking-[0.16em] text-nofx-gold">
|
||||
{category.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Category Items */}
|
||||
<ul className="space-y-1">
|
||||
<ul className="space-y-0.5 border-l border-[rgba(26,24,19,0.12)]">
|
||||
{category.items.map((item) => {
|
||||
const isActive = activeItemId === item.id
|
||||
return (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
onClick={() => onItemClick(category.id, item.id)}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-all border-l-[3px] ${isActive
|
||||
? 'bg-nofx-gold/10 text-nofx-gold border-nofx-gold pl-[9px]'
|
||||
: 'bg-transparent text-nofx-text-muted border-transparent pl-3 hover:bg-nofx-gold/5 hover:text-nofx-text'
|
||||
}`}
|
||||
className={`-ml-px w-full border-l-2 py-1.5 pl-3 pr-2 text-left text-[13px] leading-5 transition-colors ${
|
||||
isActive
|
||||
? 'border-nofx-gold bg-nofx-gold/5 font-medium text-nofx-text'
|
||||
: 'border-transparent text-nofx-text-muted hover:border-nofx-gold/40 hover:text-nofx-text'
|
||||
}`}
|
||||
>
|
||||
{t(item.questionKey, language)}
|
||||
{item.question}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Terminal, Shield, Cpu, BarChart3 } from 'lucide-react'
|
||||
import { t, Language } from '../../i18n/translations'
|
||||
|
||||
interface AboutSectionProps {
|
||||
language: Language
|
||||
}
|
||||
|
||||
export default function AboutSection({ language }: AboutSectionProps) {
|
||||
const features = [
|
||||
{
|
||||
icon: Shield,
|
||||
title: language === 'zh' ? 'Full Control' : 'Full Control',
|
||||
desc: language === 'zh' ? 'Self-hosted, data secure' : 'Self-hosted, data secure',
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
title: language === 'zh' ? 'Multi-AI Support' : 'Multi-AI Support',
|
||||
desc: language === 'zh' ? 'DeepSeek, GPT, Claude...' : 'DeepSeek, GPT, Claude...',
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: language === 'zh' ? 'Real-time Monitor' : 'Real-time Monitor',
|
||||
desc: language === 'zh' ? 'Visual trading dashboard' : 'Visual trading dashboard',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="py-24 relative overflow-hidden" style={{ background: '#F1ECE2' }}>
|
||||
{/* Background Decoration */}
|
||||
<div
|
||||
className="absolute top-0 right-0 w-96 h-96 rounded-full blur-3xl opacity-30"
|
||||
style={{ background: 'radial-gradient(circle, rgba(224, 72, 59, 0.08) 0%, transparent 70%)' }}
|
||||
/>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||
{/* Left Content */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<motion.div
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full mb-6"
|
||||
style={{
|
||||
background: 'rgba(224, 72, 59, 0.1)',
|
||||
border: '1px solid rgba(224, 72, 59, 0.2)',
|
||||
}}
|
||||
>
|
||||
<Terminal className="w-4 h-4" style={{ color: '#E0483B' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#E0483B' }}>
|
||||
{t('aboutNofx', language)}
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
<h2 className="text-4xl lg:text-5xl font-bold mb-6" style={{ color: '#1A1813' }}>
|
||||
{t('whatIsNofx', language)}
|
||||
</h2>
|
||||
|
||||
<p className="text-lg mb-8 leading-relaxed" style={{ color: '#8A8478' }}>
|
||||
{t('nofxNotAnotherBot', language)} {t('nofxDescription1', language)}
|
||||
</p>
|
||||
|
||||
{/* Feature Pills */}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl"
|
||||
style={{
|
||||
background: '#F7F4EC',
|
||||
border: '1px solid rgba(26, 24, 19, 0.14)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center"
|
||||
style={{ background: 'rgba(224, 72, 59, 0.1)' }}
|
||||
>
|
||||
<feature.icon className="w-5 h-5" style={{ color: '#E0483B' }} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold" style={{ color: '#1A1813' }}>
|
||||
{feature.title}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#8A8478' }}>
|
||||
{feature.desc}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Right - Terminal */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
<div
|
||||
className="rounded-2xl overflow-hidden"
|
||||
style={{
|
||||
background: '#F7F4EC',
|
||||
border: '1px solid rgba(26, 24, 19, 0.14)',
|
||||
boxShadow: '0 6px 24px rgba(26, 24, 19, 0.12)',
|
||||
}}
|
||||
>
|
||||
{/* Terminal Header */}
|
||||
<div
|
||||
className="flex items-center gap-2 px-4 py-3"
|
||||
style={{ background: '#E8E2D5', borderBottom: '1px solid rgba(26, 24, 19, 0.14)' }}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-3 h-3 rounded-full" style={{ background: '#D6433A' }} />
|
||||
<div className="w-3 h-3 rounded-full" style={{ background: '#E0483B' }} />
|
||||
<div className="w-3 h-3 rounded-full" style={{ background: '#2E8B57' }} />
|
||||
</div>
|
||||
<span className="text-xs ml-2" style={{ color: '#8A8478' }}>terminal</span>
|
||||
</div>
|
||||
|
||||
{/* Terminal Content */}
|
||||
<div className="p-6 font-mono text-sm space-y-2">
|
||||
<div style={{ color: '#8A8478' }}>$ git clone https://github.com/NoFxAiOS/nofx.git</div>
|
||||
<div style={{ color: '#8A8478' }}>$ cd nofx && chmod +x start.sh</div>
|
||||
<div style={{ color: '#8A8478' }}>$ ./start.sh start --build</div>
|
||||
<div className="pt-2" style={{ color: '#E0483B' }}>
|
||||
✓ {t('startupMessages1', language)}
|
||||
</div>
|
||||
<div style={{ color: '#2E8B57' }}>
|
||||
✓ {t('startupMessages2', language)}
|
||||
</div>
|
||||
<div style={{ color: '#2E8B57' }}>
|
||||
✓ {t('startupMessages3', language)}
|
||||
</div>
|
||||
<motion.div
|
||||
className="flex items-center gap-2 pt-2"
|
||||
animate={{ opacity: [1, 0.5, 1] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity }}
|
||||
>
|
||||
<span style={{ color: '#E0483B' }}>▸</span>
|
||||
<span style={{ color: '#1A1813' }}>_</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useRef } from 'react'
|
||||
import { motion, useInView } from 'framer-motion'
|
||||
|
||||
export default function AnimatedSection({
|
||||
children,
|
||||
id,
|
||||
backgroundColor = '#F1ECE2',
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
id?: string
|
||||
backgroundColor?: string
|
||||
}) {
|
||||
const ref = useRef(null)
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px' })
|
||||
|
||||
return (
|
||||
<motion.section
|
||||
id={id}
|
||||
ref={ref}
|
||||
className="py-20 px-4"
|
||||
style={{ background: backgroundColor }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={isInView ? { opacity: 1 } : { opacity: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
{children}
|
||||
</motion.section>
|
||||
)
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { MessageCircle, Heart, Repeat2, ExternalLink } from 'lucide-react'
|
||||
import { Language } from '../../i18n/translations'
|
||||
|
||||
interface TweetProps {
|
||||
quote: string
|
||||
authorName: string
|
||||
handle: string
|
||||
avatarUrl: string
|
||||
tweetUrl: string
|
||||
delay: number
|
||||
}
|
||||
|
||||
function TweetCard({ quote, authorName, handle, avatarUrl, tweetUrl, delay }: TweetProps) {
|
||||
return (
|
||||
<motion.a
|
||||
href={tweetUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block p-5 rounded-2xl transition-all duration-300 group"
|
||||
style={{
|
||||
background: '#F7F4EC',
|
||||
border: '1px solid rgba(26, 24, 19, 0.14)',
|
||||
}}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay }}
|
||||
whileHover={{
|
||||
y: -4,
|
||||
borderColor: 'rgba(224, 72, 59, 0.3)',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={authorName}
|
||||
className="w-10 h-10 rounded-full object-cover"
|
||||
style={{ border: '2px solid rgba(26, 24, 19, 0.14)' }}
|
||||
/>
|
||||
<div>
|
||||
<div className="font-semibold text-sm" style={{ color: '#1A1813' }}>
|
||||
{authorName}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#8A8478' }}>
|
||||
{handle}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* X Logo */}
|
||||
<div
|
||||
className="w-6 h-6 flex items-center justify-center opacity-50 group-hover:opacity-100 transition-opacity"
|
||||
style={{ color: '#1A1813' }}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="w-4 h-4" fill="currentColor">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<p
|
||||
className="text-sm leading-relaxed mb-4 line-clamp-4"
|
||||
style={{ color: '#1A1813' }}
|
||||
>
|
||||
{quote}
|
||||
</p>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center gap-6 pt-3" style={{ borderTop: '1px solid rgba(26, 24, 19, 0.14)' }}>
|
||||
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#8A8478' }}>
|
||||
<MessageCircle className="w-3.5 h-3.5" />
|
||||
<span>Reply</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#8A8478' }}>
|
||||
<Repeat2 className="w-3.5 h-3.5" />
|
||||
<span>Repost</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#8A8478' }}>
|
||||
<Heart className="w-3.5 h-3.5" />
|
||||
<span>Like</span>
|
||||
</div>
|
||||
<div className="ml-auto opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<ExternalLink className="w-3.5 h-3.5" style={{ color: '#E0483B' }} />
|
||||
</div>
|
||||
</div>
|
||||
</motion.a>
|
||||
)
|
||||
}
|
||||
|
||||
interface CommunitySectionProps {
|
||||
language?: Language
|
||||
}
|
||||
|
||||
export default function CommunitySection({ language }: CommunitySectionProps) {
|
||||
const tweets: TweetProps[] = []
|
||||
|
||||
return (
|
||||
<section className="py-24 relative" style={{ background: '#F1ECE2' }}>
|
||||
{/* Background Decoration */}
|
||||
<div
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 w-96 h-96 rounded-full blur-3xl opacity-20"
|
||||
style={{ background: 'radial-gradient(circle, rgba(224, 72, 59, 0.08) 0%, transparent 70%)' }}
|
||||
/>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 relative z-10">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
className="text-center mb-12"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#1A1813' }}>
|
||||
{language === 'zh' ? 'Community Voices' : 'Community Voices'}
|
||||
</h2>
|
||||
<p className="text-lg" style={{ color: '#8A8478' }}>
|
||||
{language === 'zh' ? 'See what others are saying' : 'See what others are saying'}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Tweet Grid */}
|
||||
<div className="grid md:grid-cols-3 gap-5">
|
||||
{tweets.map((tweet, idx) => (
|
||||
<TweetCard key={idx} {...tweet} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<motion.div
|
||||
className="text-center mt-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<a
|
||||
href="https://x.com/vergex_ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl font-medium transition-all hover:scale-105"
|
||||
style={{
|
||||
background: 'rgba(224, 72, 59, 0.1)',
|
||||
color: '#E0483B',
|
||||
border: '1px solid rgba(224, 72, 59, 0.3)',
|
||||
}}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="w-5 h-5" fill="currentColor">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
{language === 'zh' ? 'Follow us on X' : 'Follow us on X'}
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Brain, Swords, BarChart3, Shield, Blocks, LineChart } from 'lucide-react'
|
||||
import { t, Language } from '../../i18n/translations'
|
||||
|
||||
interface FeaturesSectionProps {
|
||||
language: Language
|
||||
}
|
||||
|
||||
export default function FeaturesSection({ language }: FeaturesSectionProps) {
|
||||
const features = [
|
||||
{
|
||||
icon: Brain,
|
||||
title: language === 'zh' ? 'AI Strategy Orchestration' : 'AI Strategy Orchestration',
|
||||
desc: language === 'zh'
|
||||
? 'Support DeepSeek, GPT, Claude, Qwen and more. Custom prompts, AI autonomously analyzes markets and makes trading decisions'
|
||||
: 'Support DeepSeek, GPT, Claude, Qwen and more. Custom prompts, AI autonomously analyzes markets and makes trading decisions',
|
||||
highlight: true,
|
||||
badge: language === 'zh' ? 'Core' : 'Core',
|
||||
},
|
||||
{
|
||||
icon: Swords,
|
||||
title: language === 'zh' ? 'Multi-AI Arena' : 'Multi-AI Arena',
|
||||
desc: language === 'zh'
|
||||
? 'Multiple AI traders compete in real-time, live PnL leaderboard, automatic survival of the fittest'
|
||||
: 'Multiple AI traders compete in real-time, live PnL leaderboard, automatic survival of the fittest',
|
||||
highlight: true,
|
||||
badge: language === 'zh' ? 'Unique' : 'Unique',
|
||||
},
|
||||
{
|
||||
icon: LineChart,
|
||||
title: language === 'zh' ? 'Pro Quant Data' : 'Pro Quant Data',
|
||||
desc: language === 'zh'
|
||||
? 'Integrated candlesticks, indicators, order book, funding rates, open interest - comprehensive data for AI decisions'
|
||||
: 'Integrated candlesticks, indicators, order book, funding rates, open interest - comprehensive data for AI decisions',
|
||||
highlight: true,
|
||||
badge: language === 'zh' ? 'Pro' : 'Pro',
|
||||
},
|
||||
{
|
||||
icon: Blocks,
|
||||
title: language === 'zh' ? 'Multi-Exchange Support' : 'Multi-Exchange Support',
|
||||
desc: language === 'zh'
|
||||
? 'Binance, OKX, Bybit, Hyperliquid, Aster DEX - one system, multiple exchanges'
|
||||
: 'Binance, OKX, Bybit, Hyperliquid, Aster DEX - one system, multiple exchanges',
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: language === 'zh' ? 'Real-time Dashboard' : 'Real-time Dashboard',
|
||||
desc: language === 'zh'
|
||||
? 'Trade monitoring, PnL curves, position analysis, AI decision logs at a glance'
|
||||
: 'Trade monitoring, PnL curves, position analysis, AI decision logs at a glance',
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: language === 'zh' ? 'Open Source & Self-Hosted' : 'Open Source & Self-Hosted',
|
||||
desc: language === 'zh'
|
||||
? 'Fully open source, data stored locally, API keys never leave your server'
|
||||
: 'Fully open source, data stored locally, API keys never leave your server',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="py-24 relative" style={{ background: '#F1ECE2' }}>
|
||||
{/* Background */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.04]"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(#E0483B 1px, transparent 1px), linear-gradient(90deg, #E0483B 1px, transparent 1px)`,
|
||||
backgroundSize: '40px 40px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 relative z-10">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
className="text-center mb-16"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#1A1813' }}>
|
||||
{t('whyChooseNofx', language)}
|
||||
</h2>
|
||||
<p className="text-lg max-w-2xl mx-auto" style={{ color: '#8A8478' }}>
|
||||
{language === 'zh'
|
||||
? 'Not just a trading bot, but a complete AI trading operating system'
|
||||
: 'Not just a trading bot, but a complete AI trading operating system'}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className={`
|
||||
relative group rounded-2xl p-6 transition-all duration-300
|
||||
${feature.highlight ? 'md:col-span-1 lg:col-span-1' : ''}
|
||||
`}
|
||||
style={{
|
||||
background: feature.highlight
|
||||
? 'rgba(224, 72, 59, 0.06)'
|
||||
: '#F7F4EC',
|
||||
border: feature.highlight
|
||||
? '1px solid rgba(224, 72, 59, 0.2)'
|
||||
: '1px solid rgba(26, 24, 19, 0.14)',
|
||||
}}
|
||||
>
|
||||
{/* Badge */}
|
||||
{feature.badge && (
|
||||
<div
|
||||
className="absolute top-4 right-4 px-2 py-1 rounded text-xs font-medium"
|
||||
style={{
|
||||
background: 'rgba(224, 72, 59, 0.15)',
|
||||
color: '#E0483B',
|
||||
}}
|
||||
>
|
||||
{feature.badge}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Icon */}
|
||||
<motion.div
|
||||
className="w-12 h-12 rounded-xl flex items-center justify-center mb-4"
|
||||
style={{
|
||||
background: feature.highlight
|
||||
? 'rgba(224, 72, 59, 0.15)'
|
||||
: 'rgba(224, 72, 59, 0.1)',
|
||||
border: '1px solid rgba(224, 72, 59, 0.2)',
|
||||
}}
|
||||
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||
>
|
||||
<feature.icon
|
||||
className="w-6 h-6"
|
||||
style={{ color: '#E0483B' }}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Text */}
|
||||
<h3
|
||||
className="text-xl font-bold mb-3"
|
||||
style={{ color: '#1A1813' }}
|
||||
>
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p
|
||||
className="text-sm leading-relaxed"
|
||||
style={{ color: '#8A8478' }}
|
||||
>
|
||||
{feature.desc}
|
||||
</p>
|
||||
|
||||
{/* Hover Glow */}
|
||||
<div
|
||||
className="absolute -bottom-10 -right-10 w-32 h-32 rounded-full blur-3xl opacity-0 group-hover:opacity-20 transition-opacity duration-500"
|
||||
style={{ background: '#E0483B' }}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bottom Stats */}
|
||||
<motion.div
|
||||
className="mt-16 grid grid-cols-2 md:grid-cols-4 gap-6"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{[
|
||||
{ value: '10+', label: language === 'zh' ? 'AI Models' : 'AI Models' },
|
||||
{ value: '5+', label: language === 'zh' ? 'Exchanges' : 'Exchanges' },
|
||||
{ value: '24/7', label: language === 'zh' ? 'Auto Trading' : 'Auto Trading' },
|
||||
{ value: '100%', label: language === 'zh' ? 'Open Source' : 'Open Source' },
|
||||
].map((stat) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
className="text-center p-4 rounded-xl"
|
||||
style={{
|
||||
background: '#F7F4EC',
|
||||
border: '1px solid rgba(26, 24, 19, 0.14)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="text-2xl font-bold mb-1"
|
||||
style={{
|
||||
color: '#E0483B',
|
||||
}}
|
||||
>
|
||||
{stat.value}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#8A8478' }}>
|
||||
{stat.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { ArrowRight, Play, Github, Zap } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { t, Language } from '../../i18n/translations'
|
||||
import { useGitHubStats } from '../../hooks/useGitHubStats'
|
||||
import { useCounterAnimation } from '../../hooks/useCounterAnimation'
|
||||
import { OFFICIAL_LINKS } from '../../constants/branding'
|
||||
|
||||
interface HeroSectionProps {
|
||||
language: Language
|
||||
}
|
||||
|
||||
export default function HeroSection({ language }: HeroSectionProps) {
|
||||
const { stars, daysOld, isLoading } = useGitHubStats('NoFxAiOS', 'nofx')
|
||||
const animatedStars = useCounterAnimation({
|
||||
start: 0,
|
||||
end: stars,
|
||||
duration: 2000,
|
||||
})
|
||||
|
||||
return (
|
||||
<section className="relative min-h-screen flex items-center justify-center overflow-hidden pt-16">
|
||||
{/* Animated Background */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
{/* Grid Pattern */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(#E0483B 1px, transparent 1px), linear-gradient(90deg, #E0483B 1px, transparent 1px)`,
|
||||
backgroundSize: '60px 60px',
|
||||
}}
|
||||
/>
|
||||
{/* Radial Gradient */}
|
||||
<div
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] rounded-full"
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(circle, rgba(224, 72, 59, 0.08) 0%, transparent 70%)',
|
||||
}}
|
||||
/>
|
||||
{/* Floating Orbs */}
|
||||
<motion.div
|
||||
className="absolute top-20 right-20 w-32 h-32 rounded-full blur-3xl"
|
||||
style={{ background: 'rgba(224, 72, 59, 0.12)' }}
|
||||
animate={{
|
||||
y: [0, 30, 0],
|
||||
scale: [1, 1.1, 1],
|
||||
}}
|
||||
transition={{ duration: 8, repeat: Infinity, ease: 'easeInOut' }}
|
||||
/>
|
||||
<motion.div
|
||||
className="absolute bottom-40 left-20 w-48 h-48 rounded-full blur-3xl"
|
||||
style={{ background: 'rgba(224, 72, 59, 0.08)' }}
|
||||
animate={{
|
||||
y: [0, -40, 0],
|
||||
scale: [1, 1.2, 1],
|
||||
}}
|
||||
transition={{ duration: 10, repeat: Infinity, ease: 'easeInOut' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 max-w-6xl mx-auto px-4 text-center">
|
||||
{/* Badge */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-full mb-8"
|
||||
style={{
|
||||
background: 'rgba(224, 72, 59, 0.1)',
|
||||
border: '1px solid rgba(224, 72, 59, 0.3)',
|
||||
}}
|
||||
>
|
||||
<Zap className="w-4 h-4" style={{ color: '#E0483B' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#E0483B' }}>
|
||||
{isLoading ? (
|
||||
t('githubStarsInDays', language)
|
||||
) : language === 'zh' ? (
|
||||
<>
|
||||
<span className="font-bold tabular-nums">
|
||||
{(animatedStars / 1000).toFixed(1)}K+
|
||||
</span>{' '}
|
||||
GitHub Stars in {daysOld} days
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="font-bold tabular-nums">
|
||||
{(animatedStars / 1000).toFixed(1)}K+
|
||||
</span>{' '}
|
||||
GitHub Stars in {daysOld} days
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
{/* Main Title */}
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
className="text-5xl sm:text-6xl lg:text-7xl font-bold mb-6 leading-tight"
|
||||
>
|
||||
<span style={{ color: '#1A1813' }}>{t('heroTitle1', language)}</span>
|
||||
<br />
|
||||
<span
|
||||
className="relative inline-block"
|
||||
style={{
|
||||
color: '#E0483B',
|
||||
}}
|
||||
>
|
||||
{t('heroTitle2', language)}
|
||||
<motion.span
|
||||
className="absolute -bottom-2 left-0 h-1 rounded-full"
|
||||
style={{ background: '#E0483B' }}
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: '100%' }}
|
||||
transition={{ duration: 0.8, delay: 0.8 }}
|
||||
/>
|
||||
</span>
|
||||
</motion.h1>
|
||||
|
||||
{/* Description */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="text-lg sm:text-xl max-w-3xl mx-auto mb-10 leading-relaxed"
|
||||
style={{ color: '#8A8478' }}
|
||||
>
|
||||
{t('heroDescription', language)}
|
||||
</motion.p>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-12"
|
||||
>
|
||||
<motion.div
|
||||
className="group flex items-center gap-3 px-8 py-4 rounded-xl font-bold text-lg transition-all"
|
||||
style={{
|
||||
background: '#E0483B',
|
||||
color: '#F7F4EC',
|
||||
boxShadow: '0 4px 24px rgba(224, 72, 59, 0.25)',
|
||||
}}
|
||||
whileHover={{
|
||||
scale: 1.02,
|
||||
boxShadow: '0 8px 32px rgba(224, 72, 59, 0.35)',
|
||||
}}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Link to="/competition" className="flex items-center gap-3">
|
||||
<Play className="w-5 h-5" />
|
||||
{t('liveCompetition', language) || 'Live Competition'}
|
||||
<ArrowRight className="w-5 h-5 transition-transform group-hover:translate-x-1" />
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
<motion.a
|
||||
href={OFFICIAL_LINKS.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center gap-3 px-8 py-4 rounded-xl font-bold text-lg transition-all"
|
||||
style={{
|
||||
background: '#F7F4EC',
|
||||
color: '#1A1813',
|
||||
border: '1px solid rgba(26, 24, 19, 0.14)',
|
||||
}}
|
||||
whileHover={{
|
||||
scale: 1.02,
|
||||
background: '#F2EEE4',
|
||||
borderColor: 'rgba(224, 72, 59, 0.3)',
|
||||
}}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Github className="w-5 h-5" />
|
||||
{t('viewSourceCode', language)}
|
||||
</motion.a>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats Row */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
className="flex flex-wrap items-center justify-center gap-8 sm:gap-12"
|
||||
>
|
||||
{[
|
||||
{ label: 'GitHub Stars', value: `${(stars / 1000).toFixed(1)}K+` },
|
||||
{
|
||||
label: language === 'zh' ? 'Exchanges' : 'Exchanges',
|
||||
value: '5+',
|
||||
},
|
||||
{
|
||||
label: language === 'zh' ? 'AI Models' : 'AI Models',
|
||||
value: '10+',
|
||||
},
|
||||
{
|
||||
label: language === 'zh' ? 'Open Source' : 'Open Source',
|
||||
value: '100%',
|
||||
},
|
||||
].map((stat, index) => (
|
||||
<motion.div
|
||||
key={stat.label}
|
||||
className="text-center"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 + index * 0.1 }}
|
||||
>
|
||||
<div
|
||||
className="text-3xl sm:text-4xl font-bold mb-1"
|
||||
style={{
|
||||
color: '#E0483B',
|
||||
}}
|
||||
>
|
||||
{stat.value}
|
||||
</div>
|
||||
<div className="text-sm" style={{ color: '#8A8478' }}>
|
||||
{stat.label}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Powered By */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.8 }}
|
||||
className="mt-16 text-xs"
|
||||
style={{ color: '#8A8478' }}
|
||||
>
|
||||
{t('poweredBy', language)}
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
{/* Scroll Indicator */}
|
||||
<motion.div
|
||||
className="absolute bottom-8 left-1/2 -translate-x-1/2"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1 }}
|
||||
>
|
||||
<motion.div
|
||||
className="w-6 h-10 rounded-full flex justify-center pt-2"
|
||||
style={{ border: '2px solid rgba(224, 72, 59, 0.3)' }}
|
||||
animate={{ y: [0, 8, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
>
|
||||
<motion.div
|
||||
className="w-1.5 h-3 rounded-full"
|
||||
style={{ background: '#E0483B' }}
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Download, Rocket, TrendingUp, AlertTriangle } from 'lucide-react'
|
||||
import { t, Language } from '../../i18n/translations'
|
||||
|
||||
interface HowItWorksSectionProps {
|
||||
language: Language
|
||||
}
|
||||
|
||||
export default function HowItWorksSection({ language }: HowItWorksSectionProps) {
|
||||
const steps = [
|
||||
{
|
||||
icon: Download,
|
||||
number: '01',
|
||||
title: language === 'zh' ? 'One-Click Deploy' : 'One-Click Deploy',
|
||||
desc: language === 'zh'
|
||||
? 'Run a single command on your server to deploy'
|
||||
: 'Run a single command on your server to deploy',
|
||||
code: 'curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash',
|
||||
},
|
||||
{
|
||||
icon: Rocket,
|
||||
number: '02',
|
||||
title: language === 'zh' ? 'Access Dashboard' : 'Access Dashboard',
|
||||
desc: language === 'zh'
|
||||
? 'Access your server via browser'
|
||||
: 'Access your server via browser',
|
||||
code: 'http://YOUR_SERVER_IP:3000',
|
||||
},
|
||||
{
|
||||
icon: TrendingUp,
|
||||
number: '03',
|
||||
title: language === 'zh' ? 'Start Trading' : 'Start Trading',
|
||||
desc: language === 'zh'
|
||||
? 'Create trader, let AI do the work'
|
||||
: 'Create trader, let AI do the work',
|
||||
code: language === 'zh' ? 'Configure Model → Exchange → Create Trader' : 'Configure Model → Exchange → Create Trader',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="py-24 relative overflow-hidden" style={{ background: '#F1ECE2' }}>
|
||||
{/* Background Decoration */}
|
||||
<div
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 w-96 h-96 rounded-full blur-3xl opacity-20"
|
||||
style={{ background: 'radial-gradient(circle, rgba(224, 72, 59, 0.12) 0%, transparent 70%)' }}
|
||||
/>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 relative z-10">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
className="text-center mb-16"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#1A1813' }}>
|
||||
{t('howToStart', language)}
|
||||
</h2>
|
||||
<p className="text-lg" style={{ color: '#8A8478' }}>
|
||||
{t('fourSimpleSteps', language)}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Steps Timeline */}
|
||||
<div className="relative">
|
||||
{/* Connecting Line */}
|
||||
<div
|
||||
className="absolute left-[39px] top-0 bottom-0 w-px hidden lg:block"
|
||||
style={{ background: 'linear-gradient(to bottom, transparent, rgba(224, 72, 59, 0.3), transparent)' }}
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
{steps.map((step, index) => (
|
||||
<motion.div
|
||||
key={step.number}
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.15 }}
|
||||
className="relative"
|
||||
>
|
||||
<div
|
||||
className="flex flex-col lg:flex-row items-start gap-6 p-6 rounded-2xl transition-all duration-300 hover:translate-x-2"
|
||||
style={{
|
||||
background: '#F7F4EC',
|
||||
border: '1px solid rgba(26, 24, 19, 0.14)',
|
||||
}}
|
||||
>
|
||||
{/* Number Circle */}
|
||||
<div className="flex-shrink-0 relative z-10">
|
||||
<motion.div
|
||||
className="w-20 h-20 rounded-2xl flex items-center justify-center"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(224, 72, 59, 0.15) 0%, rgba(224, 72, 59, 0.05) 100%)',
|
||||
border: '1px solid rgba(224, 72, 59, 0.3)',
|
||||
}}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
>
|
||||
<step.icon className="w-8 h-8" style={{ color: '#E0483B' }} />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-grow">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span
|
||||
className="text-sm font-mono font-bold"
|
||||
style={{ color: '#E0483B' }}
|
||||
>
|
||||
{step.number}
|
||||
</span>
|
||||
<h3 className="text-xl font-bold" style={{ color: '#1A1813' }}>
|
||||
{step.title}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-4" style={{ color: '#8A8478' }}>
|
||||
{step.desc}
|
||||
</p>
|
||||
|
||||
{/* Code Block */}
|
||||
<div
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg font-mono text-sm"
|
||||
style={{
|
||||
background: '#E8E2D5',
|
||||
border: '1px solid rgba(26, 24, 19, 0.14)',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: '#8A8478' }}>$</span>
|
||||
<span style={{ color: '#1A1813' }}>{step.code}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Risk Warning */}
|
||||
<motion.div
|
||||
className="mt-12 p-6 rounded-2xl flex items-start gap-4"
|
||||
style={{
|
||||
background: 'rgba(224, 72, 59, 0.05)',
|
||||
border: '1px solid rgba(224, 72, 59, 0.15)',
|
||||
}}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div
|
||||
className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0"
|
||||
style={{ background: 'rgba(224, 72, 59, 0.1)' }}
|
||||
>
|
||||
<AlertTriangle className="w-6 h-6" style={{ color: '#E0483B' }} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold mb-2" style={{ color: '#E0483B' }}>
|
||||
{t('importantRiskWarning', language)}
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed" style={{ color: '#8A8478' }}>
|
||||
{t('riskWarningText', language)}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -31,14 +31,33 @@ export default function DeploymentHub() {
|
||||
</h2>
|
||||
|
||||
<p className="text-nofx-text-muted text-lg leading-relaxed font-light">
|
||||
Initialize your own high-frequency trading node in seconds.
|
||||
Our optimized installer handles all dependencies, bringing the trading system online with a single command.
|
||||
One command on your laptop or any server installs
|
||||
everything. Open the address it prints, create your
|
||||
account, and the guided launch takes you to your
|
||||
first AI trade in about five minutes — around $13
|
||||
is enough to start.
|
||||
</p>
|
||||
|
||||
{/* the first five minutes, in plain words */}
|
||||
<ol className="space-y-2 pt-2 font-mono text-sm text-nofx-text-muted">
|
||||
{[
|
||||
'Register — the first account owns this instance.',
|
||||
'Fund two small wallets: $1+ for AI fees, $12+ to trade with (guided, with QR codes).',
|
||||
'Press Start — the AI trades on its own; stop it anytime.',
|
||||
].map((step, i) => (
|
||||
<li key={i} className="flex gap-3">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded border border-nofx-gold/30 bg-nofx-gold/10 text-[11px] font-bold text-nofx-gold">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span>{step}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-4">
|
||||
{[
|
||||
{ icon: Command, label: "One-Line Install", desc: "No configuration needed" },
|
||||
{ icon: Shield, label: "Secure Core", desc: "Sandboxed execution env" }
|
||||
{ icon: Command, label: "One-Line Install", desc: "Docker handles every dependency" },
|
||||
{ icon: Shield, label: "Your Keys Stay Home", desc: "Runs on your machine, keys encrypted locally" }
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex gap-4 items-start p-4 rounded bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] hover:border-nofx-gold/30 transition-colors group">
|
||||
<div className="p-2 rounded bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] text-nofx-gold group-hover:bg-nofx-gold/10 transition-colors">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { ArrowRight, Shield, Activity, CircuitBoard, Wifi, Globe, Zap, Star, GitFork, Users, MessageCircle } from 'lucide-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useGitHubStats } from '../../../hooks/useGitHubStats'
|
||||
import AgentTerminal from '../brand/AgentTerminal'
|
||||
|
||||
@@ -16,6 +17,7 @@ const tickerLabels: Record<string, string> = {
|
||||
}
|
||||
|
||||
export default function TerminalHero() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
// Real-time price state
|
||||
const [prices, setPrices] = useState<Record<string, string>>({
|
||||
@@ -231,17 +233,29 @@ export default function TerminalHero() {
|
||||
{/* CTA Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 w-full justify-center">
|
||||
<button
|
||||
onClick={() => document.getElementById('market-scanner')?.scrollIntoView({ behavior: 'smooth' })}
|
||||
onClick={() => navigate('/traders')}
|
||||
className="group relative overflow-hidden bg-nofx-gold text-nofx-bg-lighter px-8 py-4 font-bold font-mono tracking-wider hover:scale-105 transition-transform duration-200"
|
||||
style={{ clipPath: 'polygon(10% 0, 100% 0, 100% 70%, 90% 100%, 0 100%, 0 30%)' }}
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
CREATE STOCK TRADER <ArrowRight className="w-4 h-4" />
|
||||
START THE AUTOPILOT <ArrowRight className="w-4 h-4" />
|
||||
</span>
|
||||
<div className="absolute inset-0 bg-nofx-text/10 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => document.getElementById('market-scanner')?.scrollIntoView({ behavior: 'smooth' })}
|
||||
className="px-8 py-4 font-bold font-mono tracking-wider text-nofx-text border border-[rgba(26,24,19,0.2)] rounded hover:border-nofx-gold/50 hover:text-nofx-gold transition-colors"
|
||||
>
|
||||
SEE IT WORK
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Plain-language promise — the anti-jargon line */}
|
||||
<p className="mt-5 text-sm text-nofx-text-muted font-mono">
|
||||
Self-hosted & open source · about $13 is enough to start · guided
|
||||
setup, no API keys — first trade in minutes
|
||||
</p>
|
||||
|
||||
{/* Community Stats Row */}
|
||||
<CommunityStats />
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { invalidateSystemConfig } from '../../lib/config'
|
||||
import { OnboardingModeSelector } from '../auth/OnboardingModeSelector'
|
||||
import type { UserMode } from '../../lib/onboarding'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
import { LanguageSwitcher } from '../common/LanguageSwitcher'
|
||||
|
||||
const labels = {
|
||||
zh: {
|
||||
@@ -127,7 +126,6 @@ export function SetupPage() {
|
||||
{/* Blur overlay */}
|
||||
<div className="absolute inset-0 backdrop-blur-md bg-nofx-bg/60" />
|
||||
|
||||
<LanguageSwitcher />
|
||||
|
||||
{/* Modal card */}
|
||||
<div className="relative z-10 flex min-h-screen items-center justify-center px-4 py-16">
|
||||
|
||||
138
web/src/components/terminal/EdgeProfile.tsx
Normal file
138
web/src/components/terminal/EdgeProfile.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { HistoricalPosition } from '../../types'
|
||||
|
||||
/**
|
||||
* EdgeProfile — where the money actually comes from. Aggregates recent closed
|
||||
* trades into hold-duration buckets plus a long/short split, each with net
|
||||
* PnL, fee load and win rate. This is the panel that made the fee-drag and
|
||||
* churn problems visible, kept on the dashboard so regressions show up
|
||||
* immediately.
|
||||
*/
|
||||
|
||||
interface EdgeProfileProps {
|
||||
positions?: HistoricalPosition[]
|
||||
}
|
||||
|
||||
interface BucketAgg {
|
||||
label: string
|
||||
n: number
|
||||
net: number
|
||||
fees: number
|
||||
wins: number
|
||||
}
|
||||
|
||||
function newBucket(label: string): BucketAgg {
|
||||
return { label, n: 0, net: 0, fees: 0, wins: 0 }
|
||||
}
|
||||
|
||||
function add(bucket: BucketAgg, pos: HistoricalPosition) {
|
||||
bucket.n += 1
|
||||
bucket.net += pos.realized_pnl || 0
|
||||
bucket.fees += pos.fee || 0
|
||||
if ((pos.realized_pnl || 0) > 0) bucket.wins += 1
|
||||
}
|
||||
|
||||
function fmtUsd(n: number): string {
|
||||
const sign = n < 0 ? '-' : '+'
|
||||
return `${sign}$${Math.abs(n).toFixed(2)}`
|
||||
}
|
||||
|
||||
/** Accepts epoch ms (the API's format) or a date string, returns epoch ms. */
|
||||
function toEpochMs(value: number | string): number {
|
||||
if (typeof value === 'number') return value
|
||||
const numeric = Number(value)
|
||||
if (Number.isFinite(numeric) && numeric > 0) return numeric
|
||||
return Date.parse(value)
|
||||
}
|
||||
|
||||
export function EdgeProfile({ positions }: EdgeProfileProps) {
|
||||
const { holdBuckets, sideBuckets, sample } = useMemo(() => {
|
||||
const holds = [
|
||||
newBucket('<15m'),
|
||||
newBucket('15-60m'),
|
||||
newBucket('1-3h'),
|
||||
newBucket('>3h'),
|
||||
]
|
||||
const sides = [newBucket('long'), newBucket('short')]
|
||||
let counted = 0
|
||||
|
||||
for (const pos of positions ?? []) {
|
||||
if ((pos.status || '').toUpperCase() !== 'CLOSED') continue
|
||||
const entry = toEpochMs(pos.entry_time)
|
||||
const exit = toEpochMs(pos.exit_time)
|
||||
if (!Number.isFinite(entry) || !Number.isFinite(exit) || exit <= entry) {
|
||||
continue
|
||||
}
|
||||
counted += 1
|
||||
|
||||
const holdMin = (exit - entry) / 60000
|
||||
const holdBucket =
|
||||
holdMin < 15 ? holds[0] : holdMin < 60 ? holds[1] : holdMin < 180 ? holds[2] : holds[3]
|
||||
add(holdBucket, pos)
|
||||
|
||||
const sideBucket =
|
||||
(pos.side || '').toLowerCase() === 'short' ? sides[1] : sides[0]
|
||||
add(sideBucket, pos)
|
||||
}
|
||||
|
||||
return { holdBuckets: holds, sideBuckets: sides, sample: counted }
|
||||
}, [positions])
|
||||
|
||||
if (sample === 0) {
|
||||
return <div className="tm-sc">No closed trades yet.</div>
|
||||
}
|
||||
|
||||
const maxAbsNet = Math.max(0.01, ...holdBuckets.map((b) => Math.abs(b.net)))
|
||||
|
||||
const row = (bucket: BucketAgg) => {
|
||||
const winPct = bucket.n > 0 ? (100 * bucket.wins) / bucket.n : 0
|
||||
const up = bucket.net >= 0
|
||||
return (
|
||||
<div key={bucket.label} style={{ marginBottom: 7 }}>
|
||||
<div className="tm-mono" style={{ display: 'flex', alignItems: 'baseline', fontSize: 11, marginBottom: 2 }}>
|
||||
<span style={{ fontWeight: 500, minWidth: 52 }}>{bucket.label}</span>
|
||||
<span className="tm-sc">
|
||||
{bucket.n} trades · {bucket.n > 0 ? `${winPct.toFixed(0)}% win` : '—'} · fees ${bucket.fees.toFixed(2)}
|
||||
</span>
|
||||
<span className={up ? 'tm-up' : 'tm-dn'} style={{ marginLeft: 'auto', fontWeight: 600 }}>
|
||||
{bucket.n > 0 ? fmtUsd(bucket.net) : '—'}
|
||||
</span>
|
||||
</div>
|
||||
{/* diverging net bar around a center axis */}
|
||||
<div style={{ display: 'flex', height: 4, background: 'var(--tm-hair)' }}>
|
||||
<div style={{ width: '50%', display: 'flex', justifyContent: 'flex-end' }}>
|
||||
{!up && (
|
||||
<div style={{ height: 4, width: `${(Math.abs(bucket.net) / maxAbsNet) * 100}%`, background: 'var(--tm-dn)' }} />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ width: '50%' }}>
|
||||
{up && bucket.net > 0 && (
|
||||
<div style={{ height: 4, width: `${(bucket.net / maxAbsNet) * 100}%`, background: 'var(--tm-up)' }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// one-line takeaway: does patience pay on this book?
|
||||
const shortHolds = holdBuckets[0].net + holdBuckets[1].net
|
||||
const longHolds = holdBuckets[2].net + holdBuckets[3].net
|
||||
const takeaway =
|
||||
longHolds > shortHolds
|
||||
? `edge concentrates in holds ≥ 1h (${fmtUsd(longHolds)} vs ${fmtUsd(shortHolds)} under 1h)`
|
||||
: `short holds outperform on this sample (${fmtUsd(shortHolds)} vs ${fmtUsd(longHolds)} ≥ 1h)`
|
||||
|
||||
return (
|
||||
<div>
|
||||
{holdBuckets.map(row)}
|
||||
<div style={{ borderTop: '1px solid var(--tm-hair)', margin: '8px 0 7px' }} />
|
||||
{sideBuckets.map(row)}
|
||||
<div className="tm-sc" style={{ marginTop: 6, fontSize: 9 }}>
|
||||
last {sample} closed · {takeaway}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EdgeProfile
|
||||
@@ -3,6 +3,9 @@ import useSWR from 'swr'
|
||||
import { api } from '../../lib/api'
|
||||
import type { Kline } from '../../lib/api/data'
|
||||
import { Candles } from './Candles'
|
||||
import { demoSeedPrice } from '../../lib/demo/demoUniverse'
|
||||
|
||||
const drnd = (a: number, b: number) => a + Math.random() * (b - a)
|
||||
|
||||
/**
|
||||
* KlineChart shows a live candlestick chart. History is seeded from the backend
|
||||
@@ -29,18 +32,87 @@ interface KlineChartProps {
|
||||
height?: number
|
||||
/** stretch the chart to fill the parent's remaining height */
|
||||
fill?: boolean
|
||||
/** showcase mode — drive a fast synthetic uptrend instead of the live feed */
|
||||
demo?: boolean
|
||||
}
|
||||
|
||||
export function KlineChart({ symbol, height = 360, fill = false }: KlineChartProps) {
|
||||
export function KlineChart({ symbol, height = 360, fill = false, demo = false }: KlineChartProps) {
|
||||
const base = baseSymbol(symbol || '')
|
||||
|
||||
// history seed (resynced occasionally; the WS carries the live bar)
|
||||
const { data: seed, isLoading } = useSWR(
|
||||
base ? ['kline', base, INTERVAL] : null,
|
||||
base && !demo ? ['kline', base, INTERVAL] : null,
|
||||
() => api.getKlines(base, INTERVAL, 'hyperliquid', MAX_BARS, true),
|
||||
{ refreshInterval: 60000, revalidateOnFocus: false, shouldRetryOnError: false, keepPreviousData: true },
|
||||
)
|
||||
|
||||
// synthetic showcase candles — a fast, gently rising series
|
||||
const [demoCandles, setDemoCandles] = useState<Kline[]>([])
|
||||
useEffect(() => {
|
||||
if (!demo || !base) return
|
||||
const target = demoSeedPrice(base)
|
||||
// A deliberately clean rising series: linear climb + a couple of gentle waves
|
||||
// (natural pullbacks) + tiny noise and small wicks. The last close lands on
|
||||
// `target` so it stays aligned with the order book + cost/liq mark. No bar
|
||||
// rolling (which degraded into flat noise) — the forming bar ticks live and
|
||||
// the whole series subtly refreshes occasionally.
|
||||
const buildSeries = (): Kline[] => {
|
||||
const now = Date.now()
|
||||
const start = target * (1 - drnd(0.013, 0.02))
|
||||
const span = target - start
|
||||
const phase = drnd(0, Math.PI * 2)
|
||||
const bars: Kline[] = []
|
||||
let prevClose = start
|
||||
for (let i = 0; i < MAX_BARS; i++) {
|
||||
const t01 = i / (MAX_BARS - 1)
|
||||
const close = start + span * t01 + span * 0.13 * Math.sin(t01 * 6.5 + phase) + (Math.random() - 0.5) * target * 0.0008
|
||||
const open = i === 0 ? start : prevClose
|
||||
const wick = target * drnd(0.0003, 0.0009)
|
||||
const tt = now - (MAX_BARS - i) * 60_000
|
||||
bars.push({
|
||||
openTime: tt,
|
||||
closeTime: tt + 60_000,
|
||||
open,
|
||||
high: Math.max(open, close) + wick,
|
||||
low: Math.min(open, close) - wick,
|
||||
close,
|
||||
volume: drnd(60, 360),
|
||||
})
|
||||
prevClose = close
|
||||
}
|
||||
const last = bars[bars.length - 1]
|
||||
last.close = target
|
||||
last.high = Math.max(last.high, target)
|
||||
last.low = Math.min(last.low, target)
|
||||
return bars
|
||||
}
|
||||
setDemoCandles(buildSeries())
|
||||
let frame = 0
|
||||
const id = setInterval(() => {
|
||||
frame++
|
||||
if (frame % 100 === 0) {
|
||||
setDemoCandles(buildSeries()) // subtle full refresh (~25s)
|
||||
return
|
||||
}
|
||||
setDemoCandles((prev) => {
|
||||
if (!prev.length) return prev
|
||||
const arr = prev.slice()
|
||||
const i = arr.length - 1
|
||||
const last = { ...arr[i] }
|
||||
// forming bar ticks gently around the current price; clean fixed-size wick
|
||||
const close = target * (1 + drnd(-0.0009, 0.001))
|
||||
const wick = target * 0.0006
|
||||
last.close = close
|
||||
last.high = Math.max(last.open, close) + wick
|
||||
last.low = Math.min(last.open, close) - wick
|
||||
last.volume += drnd(2, 14)
|
||||
arr[i] = last
|
||||
return arr
|
||||
})
|
||||
}, 250)
|
||||
return () => clearInterval(id)
|
||||
}, [demo, base])
|
||||
|
||||
// resolve the Hyperliquid coin id (xyz: dex membership)
|
||||
const [xyzSet, setXyzSet] = useState<Set<string>>(new Set())
|
||||
useEffect(() => {
|
||||
@@ -65,7 +137,7 @@ export function KlineChart({ symbol, height = 360, fill = false }: KlineChartPro
|
||||
const [wsLive, setWsLive] = useState(false)
|
||||
const pending = useRef<Kline | null>(null)
|
||||
useEffect(() => {
|
||||
if (!coin) return
|
||||
if (!coin || demo) return
|
||||
setLiveBar(null)
|
||||
let ws: WebSocket | null = null
|
||||
let raf: number | null = null
|
||||
@@ -117,7 +189,7 @@ export function KlineChart({ symbol, height = 360, fill = false }: KlineChartPro
|
||||
}, [coin])
|
||||
|
||||
// merge the live bar into the seeded history
|
||||
const candles = useMemo(() => {
|
||||
const realCandles = useMemo(() => {
|
||||
const hist = seed ?? []
|
||||
if (!liveBar) return hist
|
||||
const arr = [...hist]
|
||||
@@ -127,10 +199,11 @@ export function KlineChart({ symbol, height = 360, fill = false }: KlineChartPro
|
||||
return arr.slice(-MAX_BARS)
|
||||
}, [seed, liveBar])
|
||||
|
||||
const candles = demo ? demoCandles : realCandles
|
||||
const last = candles.length ? candles[candles.length - 1].close : 0
|
||||
const first = candles.length ? candles[0].open : 0
|
||||
const chg = first ? ((last - first) / first) * 100 : 0
|
||||
const live = wsLive && candles.length > 0
|
||||
const live = (demo || wsLive) && candles.length > 0
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'var(--tm-mono)', ...(fill ? { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 } : {}) }}>
|
||||
|
||||
@@ -2,6 +2,9 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { api } from '../../lib/api'
|
||||
import type { VergexHeatmapBin } from '../../lib/api/data'
|
||||
import { demoSeedPrice, demoTick } from '../../lib/demo/demoUniverse'
|
||||
|
||||
const lmRnd = (a: number, b: number) => a + Math.random() * (b - a)
|
||||
|
||||
/**
|
||||
* LiquidationMap renders the vergex (claw402) cost / liquidation heatmap as a
|
||||
@@ -44,9 +47,11 @@ interface LiquidationMapProps {
|
||||
marketType?: string
|
||||
/** fixed height of the scrollable ladder (px); auto-centres on the mark */
|
||||
height?: number
|
||||
/** showcase mode — render a synthetic ladder centred on the demo seed price */
|
||||
demo?: boolean
|
||||
}
|
||||
|
||||
export function LiquidationMap({ symbol, marketType = 'hip3_perp', height = 460 }: LiquidationMapProps) {
|
||||
export function LiquidationMap({ symbol, marketType = 'hip3_perp', height = 460, demo = false }: LiquidationMapProps) {
|
||||
// Synthetic markets live under marketType "hip3_perp"; crypto majors under
|
||||
// "perp". We try the caller's guess first and fall back to the other so the
|
||||
// heatmap resolves for ANY symbol that has one.
|
||||
@@ -54,15 +59,68 @@ export function LiquidationMap({ symbol, marketType = 'hip3_perp', height = 460
|
||||
api.getVergexCostLiquidationHeatmap({ marketType: mt, symbol, chain: 'mainnet', liqBand: '15' })
|
||||
const opts = { refreshInterval: 300000, revalidateOnFocus: false, keepPreviousData: true }
|
||||
|
||||
const primary = useSWR(symbol ? ['heatmap', marketType, symbol] : null, () => fetcher(marketType), opts)
|
||||
const primary = useSWR(symbol && !demo ? ['heatmap', marketType, symbol] : null, () => fetcher(marketType), opts)
|
||||
const primaryHasBins = !!primary.data?.data?.bins?.length
|
||||
const altMt = marketType === 'perp' ? 'hip3_perp' : 'perp'
|
||||
const needAlt = !primaryHasBins && !primary.isLoading && primary.data !== undefined
|
||||
const needAlt = !demo && !primaryHasBins && !primary.isLoading && primary.data !== undefined
|
||||
const alt = useSWR(needAlt && symbol ? ['heatmap', altMt, symbol] : null, () => fetcher(altMt), opts)
|
||||
|
||||
const data = primaryHasBins ? primary.data : alt.data
|
||||
const isLoading = primary.isLoading || (needAlt && alt.isLoading)
|
||||
const error = primaryHasBins ? undefined : alt.error || primary.error
|
||||
// showcase mode: drive a slow ticker so the synthetic ladder gently breathes
|
||||
const [demoFrame, setDemoFrame] = useState(0)
|
||||
useEffect(() => {
|
||||
if (!demo) return
|
||||
const id = setInterval(() => setDemoFrame((f) => f + 1), 420)
|
||||
return () => clearInterval(id)
|
||||
}, [demo])
|
||||
|
||||
// stable base ladder for the active symbol (regenerated only on symbol change),
|
||||
// centred on the same seed price the order book / candles use so all three
|
||||
// price panels stay consistent.
|
||||
const demoBase = useMemo(() => {
|
||||
if (!demo) return null
|
||||
const base = (symbol || 'SP500').toUpperCase().replace(/^XYZ:/, '')
|
||||
const mark = demoSeedPrice(base)
|
||||
const tick = demoTick(mark)
|
||||
const N = 44
|
||||
const scale = mark * 1.4e4
|
||||
const bins = [] as { px: number; lc: number; sc: number; ll: number; sl: number }[]
|
||||
for (let i = -N / 2; i <= N / 2; i++) {
|
||||
const px = +(mark + i * tick * 2).toFixed(tick < 1 ? 3 : 1)
|
||||
const dist = Math.abs(i) / (N / 2)
|
||||
const near = Math.max(0, 1 - dist) ** 1.4
|
||||
const far = dist ** 1.3
|
||||
const below = i < 0
|
||||
bins.push({
|
||||
px,
|
||||
lc: (below ? near : near * 0.18) * scale * lmRnd(0.5, 1),
|
||||
sc: (!below ? near : near * 0.18) * scale * lmRnd(0.5, 1),
|
||||
ll: (below ? far : 0) * scale * lmRnd(0.4, 0.9),
|
||||
sl: (!below ? far : 0) * scale * lmRnd(0.4, 0.9),
|
||||
})
|
||||
}
|
||||
return { base, mark, bins, costAddrs: Math.round(lmRnd(22000, 31000)), liqAddrs: Math.round(lmRnd(16000, 23000)) }
|
||||
}, [demo, symbol])
|
||||
|
||||
// per-frame view: each bin breathes on its own phase (gentle ±, not a refresh)
|
||||
const demoData = useMemo(() => {
|
||||
if (!demoBase) return undefined
|
||||
const f = demoFrame
|
||||
const w = (v: number, amp: number, ph: number) => v * (1 + amp * Math.sin(f * 0.5 + ph))
|
||||
const bins: VergexHeatmapBin[] = demoBase.bins.map((b, i) => ({
|
||||
px: b.px,
|
||||
longCost: w(b.lc, 0.12, i * 0.6),
|
||||
shortCost: w(b.sc, 0.12, i * 0.9 + 1.7),
|
||||
longLiq: w(b.ll, 0.16, i * 0.5 + 3.1),
|
||||
shortLiq: w(b.sl, 0.16, i * 0.8 + 4.6),
|
||||
}) as unknown as VergexHeatmapBin)
|
||||
return {
|
||||
data: { bins, markPrice: demoBase.mark, costAddrs: demoBase.costAddrs, liqAddrs: demoBase.liqAddrs, market: { symbol: demoBase.base } },
|
||||
}
|
||||
}, [demoBase, demoFrame])
|
||||
|
||||
const data = demo ? demoData : primaryHasBins ? primary.data : alt.data
|
||||
const isLoading = demo ? false : primary.isLoading || (needAlt && alt.isLoading)
|
||||
const error = demo ? undefined : primaryHasBins ? undefined : alt.error || primary.error
|
||||
|
||||
const [hover, setHover] = useState<number | null>(null)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -45,6 +45,14 @@ function baseSymbol(raw: string): string {
|
||||
return raw.toUpperCase().replace(/^XYZ:/, '').replace(/[-_]/g, '').replace(/(USDT|USDC|USD)$/, '')
|
||||
}
|
||||
|
||||
// stable pseudo-random in [0,1) from a string — gives each beam its own phase so
|
||||
// the funnel flickers randomly (not in lockstep) while staying deterministic.
|
||||
function hash01(s: string): number {
|
||||
let h = 2166136261
|
||||
for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619)
|
||||
return ((h >>> 0) % 10000) / 10000
|
||||
}
|
||||
|
||||
// evenly spread `count` solid items across `capacity` grid cells
|
||||
function scatter(count: number, capacity: number): Map<number, number> {
|
||||
const m = new Map<number, number>()
|
||||
@@ -175,20 +183,30 @@ export function OrchestrationTopology({ layers, className }: OrchestrationTopolo
|
||||
))}
|
||||
</g>
|
||||
|
||||
{engineTargets.map((n, i) => (
|
||||
<circle key={`b0-${i}`} r={1.8} fill={n.dir === 'short' ? SHORT : LONG}>
|
||||
<animate attributeName="cx" values={`${ENGINE_X};${n.x}`} dur={`${0.5 + (i % 5) * 0.08}s`} begin={`${(i % 8) * 0.07}s`} repeatCount="indefinite" />
|
||||
<animate attributeName="cy" values={`${cy};${n.y}`} dur={`${0.5 + (i % 5) * 0.08}s`} begin={`${(i % 8) * 0.07}s`} repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="0.9;0.9;0" dur={`${0.5 + (i % 5) * 0.08}s`} begin={`${(i % 8) * 0.07}s`} repeatCount="indefinite" />
|
||||
</circle>
|
||||
))}
|
||||
{edges.map((e, i) => (
|
||||
<circle key={`be-${e.key}`} r={2.1} fill={e.dir === 'short' ? SHORT : LONG}>
|
||||
<animate attributeName="cx" values={`${e.x1};${e.x2}`} dur={`${0.45 + (i % 4) * 0.08}s`} begin={`${(i % 6) * 0.06}s`} repeatCount="indefinite" />
|
||||
<animate attributeName="cy" values={`${e.y1};${e.y2}`} dur={`${0.45 + (i % 4) * 0.08}s`} begin={`${(i % 6) * 0.06}s`} repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="1;1;0" dur={`${0.45 + (i % 4) * 0.08}s`} begin={`${(i % 6) * 0.06}s`} repeatCount="indefinite" />
|
||||
</circle>
|
||||
))}
|
||||
{engineTargets.map((n, i) => {
|
||||
const r = hash01(`e${i}-${n.x}-${n.y}`)
|
||||
const dur = `${(0.42 + r * 0.55).toFixed(2)}s`
|
||||
const begin = `${(r * 1.4).toFixed(2)}s`
|
||||
return (
|
||||
<circle key={`b0-${i}`} r={1.8} fill={n.dir === 'short' ? SHORT : LONG}>
|
||||
<animate attributeName="cx" values={`${ENGINE_X};${n.x}`} dur={dur} begin={begin} repeatCount="indefinite" />
|
||||
<animate attributeName="cy" values={`${cy};${n.y}`} dur={dur} begin={begin} repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="0.9;0.9;0" dur={dur} begin={begin} repeatCount="indefinite" />
|
||||
</circle>
|
||||
)
|
||||
})}
|
||||
{edges.map((e) => {
|
||||
const r = hash01(e.key)
|
||||
const dur = `${(0.38 + r * 0.6).toFixed(2)}s`
|
||||
const begin = `${(r * 1.6).toFixed(2)}s`
|
||||
return (
|
||||
<circle key={`be-${e.key}`} r={2.1} fill={e.dir === 'short' ? SHORT : LONG}>
|
||||
<animate attributeName="cx" values={`${e.x1};${e.x2}`} dur={dur} begin={begin} repeatCount="indefinite" />
|
||||
<animate attributeName="cy" values={`${e.y1};${e.y2}`} dur={dur} begin={begin} repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="1;1;0" dur={dur} begin={begin} repeatCount="indefinite" />
|
||||
</circle>
|
||||
)
|
||||
})}
|
||||
|
||||
{cellsByLayer.map((layer, li) =>
|
||||
layer.map((cell, ci) => {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { demoSeedPrice, demoTick } from '../../lib/demo/demoUniverse'
|
||||
|
||||
const rnd = (a: number, b: number) => a + Math.random() * (b - a)
|
||||
|
||||
/**
|
||||
* OrderBook renders a live L2 depth ladder for a single instrument, streamed
|
||||
@@ -55,9 +58,11 @@ interface OrderBookProps {
|
||||
symbol: string
|
||||
/** optional entry price to mark the user's position level on the ladder */
|
||||
markPrice?: number
|
||||
/** showcase mode — drive a fast synthetic book instead of the live WS feed */
|
||||
demo?: boolean
|
||||
}
|
||||
|
||||
export function OrderBook({ symbol, markPrice }: OrderBookProps) {
|
||||
export function OrderBook({ symbol, markPrice, demo = false }: OrderBookProps) {
|
||||
const base = useMemo(() => baseSymbol(symbol || ''), [symbol])
|
||||
const [xyzSet, setXyzSet] = useState<Set<string>>(new Set())
|
||||
const [book, setBook] = useState<BookState | null>(null)
|
||||
@@ -88,10 +93,48 @@ export function OrderBook({ symbol, markPrice }: OrderBookProps) {
|
||||
|
||||
const coin = useMemo(() => resolveCoin(base, xyzSet), [base, xyzSet])
|
||||
|
||||
// synthetic showcase feed — keeps price levels stable for stretches (so each
|
||||
// row flashes independently as its size changes) with a gentle upward drift.
|
||||
useEffect(() => {
|
||||
if (!demo || !base) return
|
||||
const seed = demoSeedPrice(base)
|
||||
const tickSz = demoTick(seed)
|
||||
const dp = tickSz < 1 ? 3 : 1
|
||||
let mid = seed
|
||||
let frame = 0
|
||||
const mkSizes = () =>
|
||||
Array.from({ length: DEPTH }, (_, i) => +(rnd(0.4, 6) * (1 + i * 0.12)).toFixed(3))
|
||||
const askSz = mkSizes()
|
||||
const bidSz = mkSizes()
|
||||
const emit = () => {
|
||||
const asks: Level[] = askSz.map((sz, i) => ({ px: +(mid + tickSz * (i + 1)).toFixed(dp), sz }))
|
||||
const bids: Level[] = bidSz.map((sz, i) => ({ px: +(mid - tickSz * (i + 1)).toFixed(dp), sz }))
|
||||
setBook({ coin: `xyz:${base}`, bids, asks })
|
||||
}
|
||||
setStatus('live')
|
||||
emit()
|
||||
const id = setInterval(() => {
|
||||
frame++
|
||||
const n = 2 + Math.floor(Math.random() * 3)
|
||||
for (let k = 0; k < n; k++) {
|
||||
const arr = Math.random() < 0.5 ? askSz : bidSz
|
||||
const i = Math.floor(Math.random() * DEPTH)
|
||||
arr[i] = +Math.max(0.05, arr[i] * rnd(0.6, 1.5)).toFixed(3)
|
||||
}
|
||||
// gentle mean-reverting wiggle around the seed (NO unbounded drift, so the
|
||||
// order book stays aligned with the cost/liq map + candle over a long run)
|
||||
if (frame % 5 === 0) {
|
||||
mid = +(mid + (seed - mid) * 0.3 + (Math.random() - 0.5) * tickSz * 2).toFixed(dp)
|
||||
}
|
||||
emit()
|
||||
}, 130)
|
||||
return () => clearInterval(id)
|
||||
}, [demo, base])
|
||||
|
||||
// live L2 stream
|
||||
const pending = useRef<BookState | null>(null)
|
||||
useEffect(() => {
|
||||
if (!coin) return
|
||||
if (!coin || demo) return
|
||||
let ws: WebSocket | null = null
|
||||
let raf: number | null = null
|
||||
let retry: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
@@ -20,7 +20,9 @@ function fmtUsd(n: number): string {
|
||||
const sign = n < 0 ? '-' : ''
|
||||
if (a >= 1e6) return `${sign}$${(a / 1e6).toFixed(2)}M`
|
||||
if (a >= 1e3) return `${sign}$${(a / 1e3).toFixed(1)}K`
|
||||
return `${sign}$${a.toFixed(0)}`
|
||||
if (a >= 100) return `${sign}$${a.toFixed(0)}`
|
||||
// small PnLs matter here: -$0.22 must not render as -$0
|
||||
return `${sign}$${a.toFixed(2)}`
|
||||
}
|
||||
|
||||
function pct(n: number): string {
|
||||
@@ -40,8 +42,9 @@ function utilColor(p: number): string {
|
||||
|
||||
interface RiskRadarProps {
|
||||
positions?: Position[]
|
||||
account?: { total_equity?: number; total_unrealized_profit?: number; margin_used_pct?: number } | null
|
||||
account?: { total_equity?: number; unrealized_profit?: number; margin_used_pct?: number } | null
|
||||
config?: { btc_eth_leverage?: number; altcoin_leverage?: number; max_positions?: number } | null
|
||||
/** max_drawdown_pct is a percent (18.5 = -18.5%), not a fraction. */
|
||||
fullStats?: { max_drawdown_pct?: number; profit_factor?: number; sharpe_ratio?: number; win_rate?: number } | null
|
||||
}
|
||||
|
||||
@@ -93,14 +96,13 @@ export function RiskRadar({ positions, account, config, fullStats }: RiskRadarPr
|
||||
|
||||
const concentration = totalNotional > 0 ? (topNotional / totalNotional) * 100 : 0
|
||||
|
||||
const ddFrac = fullStats?.max_drawdown_pct ?? 0
|
||||
const drawdown = ddFrac * 100
|
||||
const drawdown = fullStats?.max_drawdown_pct ?? 0
|
||||
|
||||
const count = pos.length
|
||||
const maxPositions = config?.max_positions ?? 0
|
||||
const countUse = maxPositions > 0 ? Math.min(100, (count / maxPositions) * 100) : 0
|
||||
|
||||
const upnl = account?.total_unrealized_profit ?? 0
|
||||
const upnl = account?.unrealized_profit ?? 0
|
||||
|
||||
return {
|
||||
longNotional,
|
||||
|
||||
@@ -17,6 +17,8 @@ import { KlineChart } from './KlineChart'
|
||||
import { ExecutionLog } from './ExecutionLog'
|
||||
import { SignalMatrix } from './SignalMatrix'
|
||||
import { RiskRadar } from './RiskRadar'
|
||||
import { EdgeProfile } from './EdgeProfile'
|
||||
import { useDemoEngine } from '../../lib/demo/useDemoEngine'
|
||||
|
||||
// crypto majors trade on the Hyperliquid main dex (no hip3 cost/liq heatmap);
|
||||
// everything else in the universe is an xyz-dex synthetic market that does.
|
||||
@@ -51,6 +53,12 @@ function fmtPct(n: number | undefined): string {
|
||||
if (n == null || Number.isNaN(n)) return '—'
|
||||
return `${n >= 0 ? '+' : ''}${n.toFixed(2)}%`
|
||||
}
|
||||
/** Price with magnitude-aware precision: 64,416 · 184.2 · 2.3775 · 0.0067 */
|
||||
function fmtPx(n: number | undefined): string {
|
||||
if (n == null || Number.isNaN(n) || n === 0) return '—'
|
||||
const dp = n >= 1000 ? 0 : n >= 100 ? 1 : n >= 1 ? 2 : 4
|
||||
return n.toLocaleString('en-US', { minimumFractionDigits: dp, maximumFractionDigits: dp })
|
||||
}
|
||||
function baseLabel(raw?: string): string {
|
||||
if (!raw) return ''
|
||||
return raw.toUpperCase().replace(/^XYZ:/, '').replace(/[-_]/g, '').replace(/(USDT|USDC|USD)$/, '')
|
||||
@@ -75,6 +83,18 @@ function fmtTime(raw?: string | number): string {
|
||||
return Number.isNaN(d.getTime()) ? '' : d.toLocaleString('en-GB', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false })
|
||||
}
|
||||
|
||||
/** Hold duration from entry/exit epoch-ms as a compact 45m / 2h10 / 1d3h. */
|
||||
function fmtHold(entry?: number, exit?: number): string {
|
||||
if (!entry || !exit || exit <= entry) return '—'
|
||||
const mins = Math.round((exit - entry) / 60000)
|
||||
if (mins < 60) return `${mins}m`
|
||||
const h = Math.floor(mins / 60)
|
||||
const m = mins % 60
|
||||
if (h < 24) return m ? `${h}h${m}` : `${h}h`
|
||||
const d = Math.floor(h / 24)
|
||||
return `${d}d${h % 24}h`
|
||||
}
|
||||
|
||||
function useTick(ms = 1000) {
|
||||
const [, set] = useState(0)
|
||||
useEffect(() => {
|
||||
@@ -88,50 +108,77 @@ export function TerminalDashboard({
|
||||
traders,
|
||||
selectedTraderId,
|
||||
onTraderSelect,
|
||||
status,
|
||||
account,
|
||||
positions,
|
||||
decisions,
|
||||
status: propStatus,
|
||||
account: propAccount,
|
||||
positions: propPositions,
|
||||
decisions: propDecisions,
|
||||
}: TerminalDashboardProps) {
|
||||
const traderId = selectedTrader?.trader_id || selectedTraderId
|
||||
useTick(1000)
|
||||
const clock = new Date().toLocaleTimeString('en-GB', { hour12: false })
|
||||
|
||||
const { data: fullStats } = useSWR(
|
||||
const { data: realFullStats } = useSWR(
|
||||
traderId ? ['full-stats', traderId] : null,
|
||||
() => api.getFullStats(traderId!, true),
|
||||
{ refreshInterval: 30000, shouldRetryOnError: false }
|
||||
)
|
||||
const { data: history } = useSWR(
|
||||
const { data: realHistory } = useSWR(
|
||||
traderId ? ['pos-history', traderId] : null,
|
||||
() => api.getPositionHistory(traderId!, 50, true),
|
||||
{ refreshInterval: 60000, shouldRetryOnError: false }
|
||||
)
|
||||
const { data: config } = useSWR(
|
||||
const { data: realConfig } = useSWR(
|
||||
traderId ? ['trader-config', traderId] : null,
|
||||
() => api.getTraderConfig(traderId!, true),
|
||||
{ refreshInterval: 120000, shouldRetryOnError: false }
|
||||
)
|
||||
|
||||
const latest = decisions && decisions.length > 0 ? decisions[0] : undefined
|
||||
const candidateCoins = latest?.candidate_coins ?? []
|
||||
|
||||
const { data: flow } = useSWR(
|
||||
const { data: realFlow } = useSWR(
|
||||
traderId ? ['flow-markets', traderId] : null,
|
||||
() => api.getFlowMarkets(selectedTrader?.ai_model, 'mainnet', '1h', 50, true),
|
||||
// paid x402 endpoint — poll slowly (5m) to conserve claw402 funds; the
|
||||
// topology beam animation is client-side and stays fast regardless
|
||||
{ refreshInterval: 300000, shouldRetryOnError: false }
|
||||
)
|
||||
const flowItems = flow?.data?.inflow ?? []
|
||||
|
||||
const { data: signalRank } = useSWR(
|
||||
const { data: realSignalRank } = useSWR(
|
||||
traderId ? ['signal-rank', traderId] : null,
|
||||
() => api.getSignalRanking(selectedTrader?.ai_model, 'mainnet', 'all', 30, true),
|
||||
// paid x402 endpoint — poll slowly (5m) to conserve claw402 funds
|
||||
{ refreshInterval: 300000, shouldRetryOnError: false }
|
||||
)
|
||||
|
||||
// Demo / showcase mode for product walkthroughs. Toggle with Shift+D (or the
|
||||
// discreet corner dot). Generates a fast, profitable-looking US-equity dataset
|
||||
// entirely client-side — it never touches the backend or any real account.
|
||||
// When off, real data flows through unchanged.
|
||||
const [demo, setDemo] = useState(false)
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.shiftKey && (e.key === 'D' || e.key === 'd')) {
|
||||
const el = document.activeElement
|
||||
if (el && /^(input|textarea|select)$/i.test(el.tagName)) return
|
||||
setDemo((v) => !v)
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [])
|
||||
const sim = useDemoEngine(demo)
|
||||
const on = demo && !!sim
|
||||
|
||||
const status = on ? sim!.status : propStatus
|
||||
const account = on ? sim!.account : propAccount
|
||||
const positions = on ? sim!.positions : propPositions
|
||||
const decisions = on ? sim!.decisions : propDecisions
|
||||
const fullStats = on ? sim!.fullStats : realFullStats
|
||||
const history = on ? sim!.history : realHistory
|
||||
const config = on ? (sim!.config as unknown as typeof realConfig) : realConfig
|
||||
const flow = on ? sim!.flow : realFlow
|
||||
const signalRank = on ? sim!.signalRank : realSignalRank
|
||||
|
||||
const latest = decisions && decisions.length > 0 ? decisions[0] : undefined
|
||||
const candidateCoins = latest?.candidate_coins ?? []
|
||||
const flowItems = flow?.data?.inflow ?? []
|
||||
|
||||
// Both the cost/liq map and the order book follow this symbol so they stay in
|
||||
// sync. The heatmap only covers hip3_perp synthetic markets, so we pick a
|
||||
// synthetic (non-crypto) the bot trades — preferring the BUSIEST one (most
|
||||
@@ -236,6 +283,26 @@ export function TerminalDashboard({
|
||||
|
||||
return (
|
||||
<div className="nofx-terminal" style={{ minHeight: '100vh', padding: 0 }}>
|
||||
{/* discreet, unlabelled showcase toggle (Shift+D also works) */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDemo((v) => !v)}
|
||||
aria-label="toggle presentation mode"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
zIndex: 9999,
|
||||
width: 12,
|
||||
height: 12,
|
||||
padding: 0,
|
||||
borderRadius: '50%',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
background: on ? 'var(--tm-up)' : 'rgba(26,24,19,0.2)',
|
||||
opacity: on ? 0.55 : 0.18,
|
||||
}}
|
||||
/>
|
||||
{/* centered, capped content column — no border (keeps it from feeling
|
||||
embedded) but bounded so the aspect-ratio SVGs don't balloon on wide screens */}
|
||||
{navSlot &&
|
||||
@@ -255,6 +322,34 @@ export function TerminalDashboard({
|
||||
navSlot,
|
||||
)}
|
||||
<div className="tm-box" style={{ maxWidth: 1280, margin: '0 auto', border: 'none' }}>
|
||||
{/* runtime health banner — AI fee wallet dry / safe mode would otherwise
|
||||
only be visible in server logs while the bot silently idles */}
|
||||
{!on && status && (status.safe_mode || status.ai_wallet_status === 'empty' || status.ai_wallet_status === 'low') && (
|
||||
<div className="tm-mono" style={{ display: 'flex', gap: 10, alignItems: 'center', margin: '8px 14px 0', padding: '8px 12px', fontSize: 11, border: '1px solid var(--tm-down)', color: 'var(--tm-down)', background: 'rgba(200,60,40,0.06)', flexWrap: 'wrap' }}>
|
||||
<span style={{ fontWeight: 600 }}>
|
||||
{status.ai_wallet_status === 'empty'
|
||||
? 'AI fee wallet is out of USDC — decisions are failing.'
|
||||
: status.ai_wallet_status === 'low'
|
||||
? `AI fee wallet is low (${(status.ai_wallet_balance_usdc ?? 0).toFixed(2)} USDC) — top up soon.`
|
||||
: 'Safe mode: AI failed repeatedly, no new positions are being opened.'}
|
||||
</span>
|
||||
<span style={{ color: 'var(--tm-ink-2)' }}>
|
||||
{status.ai_wallet_status === 'empty' || status.ai_wallet_status === 'low'
|
||||
? 'Deposit Base USDC to the Claw402 wallet, the trader recovers automatically.'
|
||||
: status.safe_mode_reason || ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* first-run reassurance — a fresh autopilot looks idle for its first
|
||||
minute (the AI is reading the market); tell newcomers what to expect */}
|
||||
{!on && status?.is_running && (status.call_count ?? 0) <= 1 && !status.safe_mode && (
|
||||
<div className="tm-mono" style={{ display: 'flex', gap: 10, alignItems: 'center', margin: '8px 14px 0', padding: '8px 12px', fontSize: 11, border: '1px solid var(--tm-up)', color: 'var(--tm-ink)', background: 'rgba(40,140,80,0.06)', flexWrap: 'wrap' }}>
|
||||
<span style={{ fontWeight: 600, color: 'var(--tm-up)' }}>Your AI is live.</span>
|
||||
<span style={{ color: 'var(--tm-ink-2)' }}>
|
||||
It reads the whole market before acting — the first decision usually lands within a minute or two and will appear in the Execution Log below. You can stop it anytime from the Config page.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* config / identity strip — first row, flows directly under the global nav */}
|
||||
<div className="tm-mono" style={{ display: 'flex', gap: 16, padding: '6px 14px', fontSize: 11, color: 'var(--tm-ink-2)', flexWrap: 'wrap' }}>
|
||||
<span style={{ fontWeight: 500 }}>{selectedTrader?.trader_name ?? 'NOFX'}</span>
|
||||
@@ -273,14 +368,21 @@ export function TerminalDashboard({
|
||||
</div>
|
||||
<div className="tm-rule" />
|
||||
|
||||
{/* metric row */}
|
||||
{/* metric row — "Total P/L" is equity-based (includes unrealized);
|
||||
"Realized P/L" is closed-trades only and matches PF/win-rate/sharpe,
|
||||
so the two never read as contradicting each other */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)' }}>
|
||||
{[
|
||||
{ l: 'Equity', v: fmtUsd(account?.total_equity), c: 'var(--tm-ink)' },
|
||||
{ l: 'Total P/L', v: `${fmtUsd(pnl, true)} (${fmtPct(pnlPct)})`, c: up ? 'var(--tm-up)' : 'var(--tm-dn)' },
|
||||
{ l: 'Win rate', v: fullStats != null ? `${fullStats.win_rate.toFixed(1)}%` : '—', c: 'var(--tm-ink)' },
|
||||
{ l: 'Total P/L · incl. unrealized', v: `${fmtUsd(pnl, true)} (${fmtPct(pnlPct)})`, c: up ? 'var(--tm-up)' : 'var(--tm-dn)' },
|
||||
{
|
||||
l: 'Realized P/L · closed trades',
|
||||
v: fullStats != null ? fmtUsd(fullStats.total_pnl, true) : '—',
|
||||
c: fullStats != null && fullStats.total_pnl >= 0 ? 'var(--tm-up)' : 'var(--tm-dn)',
|
||||
},
|
||||
{ l: 'Profit factor', v: fullStats != null ? fullStats.profit_factor.toFixed(2) : '—', c: 'var(--tm-ink)' },
|
||||
{ l: 'Max drawdown', v: fullStats != null ? `-${(fullStats.max_drawdown_pct * 100).toFixed(1)}%` : '—', c: 'var(--tm-dn)' },
|
||||
// max_drawdown_pct is already a percent (18.5 = -18.5%)
|
||||
{ l: 'Max drawdown', v: fullStats != null ? `-${fullStats.max_drawdown_pct.toFixed(1)}%` : '—', c: 'var(--tm-dn)' },
|
||||
].map((m, i) => (
|
||||
<div key={m.l} style={{ padding: '12px 14px', borderRight: i < 4 ? cellBorder : 'none' }}>
|
||||
<div className="tm-sc">{m.l}</div>
|
||||
@@ -295,11 +397,14 @@ export function TerminalDashboard({
|
||||
<>
|
||||
<div className="tm-mono" style={{ display: 'flex', gap: 18, padding: '6px 14px', fontSize: 11, color: 'var(--tm-ink-2)', flexWrap: 'wrap' }}>
|
||||
<span className="tm-sc">trades <b style={{ color: 'var(--tm-ink)' }}>{fullStats.total_trades}</b></span>
|
||||
<span className="tm-sc tm-up">win {fullStats.win_trades}</span>
|
||||
<span className="tm-sc tm-up">win {fullStats.win_trades} ({fullStats.win_rate.toFixed(1)}%)</span>
|
||||
<span className="tm-sc tm-dn">loss {fullStats.loss_trades}</span>
|
||||
<span className="tm-sc">sharpe <b style={{ color: 'var(--tm-ink)' }}>{fullStats.sharpe_ratio.toFixed(2)}</b></span>
|
||||
{/* fee-drag chain: gross realized − fees = net realized */}
|
||||
<span className="tm-sc">gross <b style={{ color: 'var(--tm-ink)' }}>{fmtUsd(fullStats.total_pnl + fullStats.total_fee, true)}</b></span>
|
||||
<span className="tm-sc">fees <b style={{ color: 'var(--tm-ink)' }}>-{fmtUsd(fullStats.total_fee)}</b></span>
|
||||
<span className="tm-sc">net <b style={{ color: fullStats.total_pnl >= 0 ? 'var(--tm-up)' : 'var(--tm-dn)' }}>{fmtUsd(fullStats.total_pnl, true)}</b></span>
|
||||
<span className="tm-sc">sharpe/trade <b style={{ color: 'var(--tm-ink)' }}>{fullStats.sharpe_ratio.toFixed(2)}</b></span>
|
||||
<span className="tm-sc">avg win/loss <b style={{ color: 'var(--tm-ink)' }}>{fullStats.avg_win.toFixed(2)}/{fullStats.avg_loss.toFixed(2)}</b></span>
|
||||
<span className="tm-sc">fees <b style={{ color: 'var(--tm-ink)' }}>{fmtUsd(fullStats.total_fee)}</b></span>
|
||||
</div>
|
||||
<div className="tm-rule" />
|
||||
</>
|
||||
@@ -315,19 +420,20 @@ export function TerminalDashboard({
|
||||
back to the other one if the guess is wrong */}
|
||||
<LiquidationMap
|
||||
symbol={activeSym}
|
||||
demo={on}
|
||||
marketType={CRYPTO_MAJORS.has(activeSym) ? 'perp' : 'hip3_perp'}
|
||||
height={ROW1_H - 130}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ ...sc, borderRight: cellBorder, height: ROW1_H, overflow: 'hidden' }}>
|
||||
<OrderBook symbol={activeSym} markPrice={positions?.find((p) => baseLabel(p.symbol) === activeSym)?.entry_price} />
|
||||
<OrderBook symbol={activeSym} demo={on} markPrice={positions?.find((p) => baseLabel(p.symbol) === activeSym)?.entry_price} />
|
||||
</div>
|
||||
<div style={{ ...sc, height: ROW1_H, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||
<SignalMatrix items={signalRank?.items} active={activeSym} onSelect={setSelectedSym} />
|
||||
<SignalMatrix items={signalRank?.items} max={18} active={activeSym} onSelect={setSelectedSym} />
|
||||
{/* the live K-line always sits under the selector and flexes to fill */}
|
||||
<div className="tm-rule" style={{ margin: '10px 0 8px' }} />
|
||||
<div style={{ flex: 1, minHeight: 0 }}>
|
||||
<KlineChart symbol={activeSym} fill />
|
||||
<KlineChart symbol={activeSym} fill demo={on} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -412,8 +518,9 @@ export function TerminalDashboard({
|
||||
<thead>
|
||||
<tr className="tm-sc" style={{ fontSize: 9 }}>
|
||||
<td style={{ padding: '0 0 3px' }}>symbol</td>
|
||||
<td style={{ padding: '0 0 3px' }}>side</td>
|
||||
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>lev</td>
|
||||
<td style={{ padding: '0 0 3px' }}>side·lev</td>
|
||||
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>entry</td>
|
||||
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>size</td>
|
||||
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>PnL</td>
|
||||
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>return%</td>
|
||||
</tr>
|
||||
@@ -422,11 +529,13 @@ export function TerminalDashboard({
|
||||
{positions.map((p, i) => {
|
||||
const long = /long|buy/i.test(p.side)
|
||||
const win = (p.unrealized_pnl ?? 0) >= 0
|
||||
const notional = Math.abs(p.quantity ?? 0) * (p.mark_price || p.entry_price || 0)
|
||||
return (
|
||||
<tr key={`${p.symbol}-${i}`} style={{ borderTop: '1px solid var(--tm-hair)' }}>
|
||||
<td style={{ padding: '5px 0', fontWeight: 500 }}>{baseLabel(p.symbol)}</td>
|
||||
<td style={{ padding: '5px 0' }} className={long ? 'tm-up' : 'tm-dn'}>{long ? 'long' : 'short'}</td>
|
||||
<td style={{ padding: '5px 0', textAlign: 'right', color: 'var(--tm-muted)' }}>{p.leverage}×</td>
|
||||
<td style={{ padding: '5px 0' }} className={long ? 'tm-up' : 'tm-dn'}>{long ? 'long' : 'short'} <span style={{ color: 'var(--tm-muted)' }}>{p.leverage}×</span></td>
|
||||
<td style={{ padding: '5px 0', textAlign: 'right', color: 'var(--tm-ink-2)' }}>{fmtPx(p.entry_price)}</td>
|
||||
<td style={{ padding: '5px 0', textAlign: 'right', color: 'var(--tm-ink-2)' }}>{fmtUsd(notional)}</td>
|
||||
<td style={{ padding: '5px 0', textAlign: 'right' }} className={win ? 'tm-up' : 'tm-dn'}>{fmtUsd(p.unrealized_pnl, true)}</td>
|
||||
<td style={{ padding: '5px 0', textAlign: 'right' }} className={win ? 'tm-up' : 'tm-dn'}>{(p.unrealized_pnl_pct ?? 0).toFixed(2)}%</td>
|
||||
</tr>
|
||||
@@ -440,10 +549,19 @@ export function TerminalDashboard({
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
|
||||
<span className="tm-px" style={{ fontSize: 11 }}>Recent trades</span>
|
||||
<span className="tm-sc">Recent closes · symbol/side/time/pnl</span>
|
||||
<span className="tm-sc">Recent closes · symbol/side/hold/pnl</span>
|
||||
</div>
|
||||
{recentTrades.length > 0 ? (
|
||||
<table className="tm-mono" style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
|
||||
<thead>
|
||||
<tr className="tm-sc" style={{ fontSize: 9 }}>
|
||||
<td style={{ padding: '0 0 3px' }}>symbol</td>
|
||||
<td style={{ padding: '0 0 3px' }}>side</td>
|
||||
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>hold</td>
|
||||
<td style={{ padding: '0 0 3px' }}> closed</td>
|
||||
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>PnL</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recentTrades.map((p) => {
|
||||
const win = p.realized_pnl >= 0
|
||||
@@ -451,7 +569,8 @@ export function TerminalDashboard({
|
||||
<tr key={p.id} style={{ borderTop: '1px solid var(--tm-hair)' }}>
|
||||
<td style={{ padding: '5px 0', fontWeight: 500 }}>{baseLabel(p.symbol)}</td>
|
||||
<td style={{ padding: '5px 0' }} className={p.side === 'long' || p.side === 'LONG' ? 'tm-up' : 'tm-dn'}>{p.side.toLowerCase()}</td>
|
||||
<td style={{ padding: '5px 0', color: 'var(--tm-muted)' }}>{fmtTime(p.exit_time)}</td>
|
||||
<td style={{ padding: '5px 0', textAlign: 'right', color: 'var(--tm-ink-2)' }}>{fmtHold(p.entry_time, p.exit_time)}</td>
|
||||
<td style={{ padding: '5px 0 5px 6px', color: 'var(--tm-muted)' }}>{fmtTime(p.exit_time)}</td>
|
||||
<td style={{ padding: '5px 0', textAlign: 'right' }} className={win ? 'tm-up' : 'tm-dn'}>{fmtUsd(p.realized_pnl, true)}</td>
|
||||
</tr>
|
||||
)
|
||||
@@ -463,8 +582,8 @@ export function TerminalDashboard({
|
||||
</div>
|
||||
<div className="tm-rule" />
|
||||
|
||||
{/* market net inflow (Vergex) · by-symbol history — balanced two-column footer */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1.5fr) minmax(0,1fr)' }}>
|
||||
{/* market net inflow (Vergex) · by-symbol history · edge profile — footer */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1.2fr) minmax(0,0.9fr) minmax(0,0.9fr)' }}>
|
||||
<div style={{ ...sc, borderRight: cellBorder }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 8 }}>
|
||||
<span className="tm-px" style={{ fontSize: 12 }}>Market net inflow</span>
|
||||
@@ -473,7 +592,7 @@ export function TerminalDashboard({
|
||||
</div>
|
||||
<FlowMarkets items={flowItems} window={flow?.data?.window} />
|
||||
</div>
|
||||
<div style={sc}>
|
||||
<div style={{ ...sc, borderRight: cellBorder }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 8 }}>
|
||||
<span className="tm-px" style={{ fontSize: 11 }}>By symbol</span>
|
||||
<span className="tm-sc">By-symbol history · trades/win/pnl</span>
|
||||
@@ -491,6 +610,13 @@ export function TerminalDashboard({
|
||||
</div>
|
||||
)) : <div className="tm-sc">No symbol history.</div>}
|
||||
</div>
|
||||
<div style={sc}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 8 }}>
|
||||
<span className="tm-px" style={{ fontSize: 11 }}>Edge profile</span>
|
||||
<span className="tm-sc">Net by hold time & side · after fees</span>
|
||||
</div>
|
||||
<EdgeProfile positions={history?.positions} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import useSWR from 'swr'
|
||||
import { api } from '../../lib/api'
|
||||
import { ApiError } from '../../lib/httpClient'
|
||||
import { ROUTES } from '../../router/paths'
|
||||
import type {
|
||||
TraderInfo,
|
||||
CreateTraderRequest,
|
||||
@@ -305,7 +307,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
|
||||
await mutateTraders()
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle trader:', error)
|
||||
// Launch preflight rejections carry an actionable reason (e.g. "AI fee
|
||||
// wallet is empty") — show it instead of a generic failure toast.
|
||||
if (
|
||||
error instanceof ApiError &&
|
||||
error.errorKey === 'trader.start.preflight_failed'
|
||||
) {
|
||||
toast.error(error.message)
|
||||
return
|
||||
}
|
||||
toast.error(t('operationFailed', language))
|
||||
}
|
||||
}
|
||||
@@ -658,10 +668,20 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
if (!setupTarget) return
|
||||
|
||||
if (setupTarget === 'claw402') {
|
||||
if (supportedModels.length === 0 && allModels.length === 0) return
|
||||
handleOpenClaw402Config()
|
||||
// The welcome page shows the deposit QR, auto-creates the wallet if
|
||||
// needed, and polls the balance — friendlier than the key-config modal.
|
||||
navigate(ROUTES.welcome)
|
||||
} else if (setupTarget === 'hyperliquid') {
|
||||
handleOpenHyperliquidConfig()
|
||||
} else if (setupTarget === 'hyperliquid-funds') {
|
||||
// Funding shortfall: bring the guided launch panel (with the trading
|
||||
// balance step and live preflight polling) into view.
|
||||
document
|
||||
.getElementById('autopilot-launch-panel')
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
toast.info(
|
||||
'Deposit USDC to your Hyperliquid account, the balance check updates automatically.'
|
||||
)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
@@ -675,6 +695,21 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
await Promise.all([loadConfigs(), mutateTraders()])
|
||||
}
|
||||
|
||||
const hasRunningTrader = (traders || []).some((trader) => trader.is_running)
|
||||
const launchPanel = (
|
||||
<AutopilotLaunchPanel
|
||||
models={allModels}
|
||||
exchanges={allExchanges}
|
||||
exchangeAccountStates={exchangeAccountStates}
|
||||
traders={traders || []}
|
||||
isLoggedIn={Boolean(user && token)}
|
||||
language={language}
|
||||
onRefresh={refreshLaunchState}
|
||||
onOpenClaw402Config={handleOpenClaw402Config}
|
||||
onOpenHyperliquidConfig={handleOpenHyperliquidConfig}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<DeepVoidBackground className="py-8" disableAnimation>
|
||||
<div className="w-full px-4 md:px-8 space-y-8 animate-fade-in">
|
||||
@@ -749,6 +784,10 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Newcomers see the guided launch FIRST — the config grid is detail.
|
||||
Once an autopilot is running, the running bot takes back the top. */}
|
||||
{!hasRunningTrader && launchPanel}
|
||||
|
||||
{/* Configuration Status Grid */}
|
||||
<ConfigStatusGrid
|
||||
configuredModels={configuredModels}
|
||||
@@ -766,17 +805,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
onCopyAddress={handleCopyAddress}
|
||||
/>
|
||||
|
||||
<AutopilotLaunchPanel
|
||||
models={allModels}
|
||||
exchanges={allExchanges}
|
||||
exchangeAccountStates={exchangeAccountStates}
|
||||
traders={traders || []}
|
||||
isLoggedIn={Boolean(user && token)}
|
||||
language={language}
|
||||
onRefresh={refreshLaunchState}
|
||||
onOpenClaw402Config={handleOpenClaw402Config}
|
||||
onOpenHyperliquidConfig={handleOpenHyperliquidConfig}
|
||||
/>
|
||||
{hasRunningTrader && launchPanel}
|
||||
|
||||
{/* Traders List */}
|
||||
<TradersList
|
||||
|
||||
@@ -18,6 +18,12 @@ import {
|
||||
import { toast } from 'sonner'
|
||||
import { api } from '../../lib/api'
|
||||
import { buildDashboardPath, ROUTES } from '../../router/paths'
|
||||
import {
|
||||
ensureClaw402Strategy,
|
||||
launchAutopilot,
|
||||
} from '../../lib/launch/launchAutopilot'
|
||||
import { runLaunchPreflight } from '../../lib/launch/preflight'
|
||||
import type { LaunchPreflightResult } from '../../lib/launch/types'
|
||||
import type {
|
||||
AIModel,
|
||||
CurrentBeginnerWalletResponse,
|
||||
@@ -219,13 +225,6 @@ export function AutopilotLaunchPanel({
|
||||
[models]
|
||||
)
|
||||
|
||||
const feeWalletAddress = claw402Model?.walletAddress || wallet?.address || ''
|
||||
const feeWalletBalance = parseNumber(
|
||||
claw402Model?.balanceUsdc || wallet?.balance_usdc
|
||||
)
|
||||
const feeReady =
|
||||
Boolean(feeWalletAddress) && feeWalletBalance >= MIN_AI_FEE_USDC
|
||||
|
||||
const hyperliquidExchange = useMemo(
|
||||
() =>
|
||||
exchanges.find(
|
||||
@@ -238,17 +237,80 @@ export function AutopilotLaunchPanel({
|
||||
[exchanges]
|
||||
)
|
||||
|
||||
// Any hyperliquid account (even partially configured) is enough for the
|
||||
// server preflight — it reports exactly which prerequisite is missing.
|
||||
const preflightExchange = useMemo(
|
||||
() =>
|
||||
hyperliquidExchange ||
|
||||
exchanges.find((exchange) => exchange.exchange_type === 'hyperliquid') ||
|
||||
null,
|
||||
[exchanges, hyperliquidExchange]
|
||||
)
|
||||
|
||||
// Server-side preflight is the source of truth for balances: it queries the
|
||||
// chain / exchange live (30s server cache) instead of trusting the balance
|
||||
// snapshot cached in the model object. Poll while the panel is visible so
|
||||
// deposits show up without a manual refresh.
|
||||
const [preflight, setPreflight] = useState<LaunchPreflightResult | null>(null)
|
||||
const claw402ModelId = claw402Model?.id
|
||||
const preflightExchangeId = preflightExchange?.id
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn || !claw402ModelId || !preflightExchangeId) {
|
||||
setPreflight(null)
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
const check = async () => {
|
||||
try {
|
||||
const result = await runLaunchPreflight({
|
||||
ai_model_id: claw402ModelId,
|
||||
exchange_id: preflightExchangeId,
|
||||
})
|
||||
if (!cancelled) setPreflight(result)
|
||||
} catch {
|
||||
// keep the last known result; client-derived fallbacks still render
|
||||
}
|
||||
}
|
||||
void check()
|
||||
const timer = setInterval(() => void check(), 20000)
|
||||
return () => {
|
||||
cancelled = true
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, [isLoggedIn, claw402ModelId, preflightExchangeId])
|
||||
|
||||
const preflightCheck = (id: string) =>
|
||||
preflight?.checks.find((check) => check.id === id)
|
||||
|
||||
const feeWalletAddress =
|
||||
claw402Model?.walletAddress ||
|
||||
wallet?.address ||
|
||||
preflightCheck('ai_wallet')?.address ||
|
||||
''
|
||||
const feeFundsCheck = preflightCheck('ai_wallet_funds')
|
||||
const feeWalletBalance =
|
||||
feeFundsCheck?.actual ??
|
||||
parseNumber(claw402Model?.balanceUsdc || wallet?.balance_usdc)
|
||||
const minAIFeeUSDC = preflight?.min_ai_fee_usdc ?? MIN_AI_FEE_USDC
|
||||
const feeReady = feeFundsCheck
|
||||
? feeFundsCheck.status !== 'failed' && Boolean(feeWalletAddress)
|
||||
: Boolean(feeWalletAddress) && feeWalletBalance >= minAIFeeUSDC
|
||||
|
||||
const hyperliquidConnected = Boolean(hyperliquidExchange)
|
||||
const exchangeState = hyperliquidExchange
|
||||
? exchangeAccountStates[hyperliquidExchange.id]
|
||||
: undefined
|
||||
const tradingBalance = parseNumber(
|
||||
exchangeState?.available_balance ?? exchangeState?.total_equity
|
||||
)
|
||||
const accountCheck = preflightCheck('exchange_account')
|
||||
const tradingFundsCheck = preflightCheck('exchange_funds')
|
||||
const tradingBalance =
|
||||
tradingFundsCheck?.actual ??
|
||||
parseNumber(exchangeState?.available_balance ?? exchangeState?.total_equity)
|
||||
const minTradingUSDC = preflight?.min_trading_usdc ?? MIN_TRADING_USDC
|
||||
const tradingBalanceReady =
|
||||
hyperliquidConnected &&
|
||||
exchangeState?.status === 'ok' &&
|
||||
tradingBalance >= MIN_TRADING_USDC
|
||||
(accountCheck && tradingFundsCheck
|
||||
? accountCheck.status === 'ok' && tradingFundsCheck.status !== 'failed'
|
||||
: exchangeState?.status === 'ok' && tradingBalance >= minTradingUSDC)
|
||||
|
||||
const autopilotTrader = useMemo(
|
||||
() =>
|
||||
@@ -286,90 +348,39 @@ export function AutopilotLaunchPanel({
|
||||
}
|
||||
}
|
||||
|
||||
const ensureClaw402Strategy = async () => {
|
||||
const strategies = await api.getStrategies()
|
||||
const existing =
|
||||
strategies.find(
|
||||
(strategy) =>
|
||||
strategy.is_active &&
|
||||
strategy.config?.ai_config?.coin_source?.source_type ===
|
||||
'vergex_signal'
|
||||
) ||
|
||||
strategies.find((strategy) =>
|
||||
strategy.name.toLowerCase().includes('claw402')
|
||||
)
|
||||
|
||||
if (existing) {
|
||||
if (!existing.is_active) {
|
||||
await api.activateStrategy(existing.id)
|
||||
}
|
||||
return existing.id
|
||||
}
|
||||
|
||||
const config = await api.getDefaultStrategyConfig()
|
||||
const created = await api.createStrategy({
|
||||
name: 'NOFX Claw402 Auto Strategy',
|
||||
description:
|
||||
'Single built-in strategy: Claw402 board, per-symbol details, raw candles, then execution.',
|
||||
config,
|
||||
})
|
||||
if (created?.id) {
|
||||
await api.activateStrategy(created.id)
|
||||
return created.id
|
||||
}
|
||||
|
||||
const refreshed = await api.getStrategies()
|
||||
const fallback = refreshed.find((strategy) =>
|
||||
strategy.name.toLowerCase().includes('claw402')
|
||||
)
|
||||
if (!fallback) throw new Error('Failed to create Claw402 strategy')
|
||||
await api.activateStrategy(fallback.id)
|
||||
return fallback.id
|
||||
}
|
||||
|
||||
const launchAutopilot = async () => {
|
||||
const handleLaunch = async () => {
|
||||
if (!claw402Model || !hyperliquidExchange) return
|
||||
setLaunching(true)
|
||||
try {
|
||||
// Re-fetch the live trader list before deciding. The `traders` prop can be
|
||||
// stale (open tab serving an old snapshot), which would make us create a
|
||||
// duplicate "NOFX Autopilot" — paying the ~35s first-create cost again and
|
||||
// orphaning the dashboard onto a deleted id (the "API Not Found" 404). Match
|
||||
// the same way the autopilotTrader memo does, falling back to it on failure.
|
||||
let trader = autopilotTrader
|
||||
try {
|
||||
const fresh = await api.getTraders(true)
|
||||
trader =
|
||||
fresh.find((t) => t.trader_name === 'NOFX Autopilot') ||
|
||||
fresh.find((t) =>
|
||||
(t.strategy_name || '').toLowerCase().includes('claw402')
|
||||
) ||
|
||||
trader
|
||||
} catch {
|
||||
// network hiccup — keep the prop-derived trader rather than risk a dup
|
||||
// Shared launch path (same as Strategy Studio): server preflight with
|
||||
// fresh balances first, then strategy provisioning, then create/start.
|
||||
const outcome = await launchAutopilot({
|
||||
ensureStrategy: ensureClaw402Strategy,
|
||||
scanIntervalMinutes: 5,
|
||||
})
|
||||
|
||||
if (!outcome.ok) {
|
||||
toast.error(outcome.message)
|
||||
if (outcome.kind === 'preflight') {
|
||||
setPreflight(outcome.preflight)
|
||||
}
|
||||
if (outcome.kind !== 'error') {
|
||||
if (outcome.setupTarget === 'claw402') {
|
||||
onOpenClaw402Config?.()
|
||||
} else if (outcome.setupTarget === 'hyperliquid') {
|
||||
onOpenHyperliquidConfig?.()
|
||||
}
|
||||
}
|
||||
await refreshEverything()
|
||||
return
|
||||
}
|
||||
if (!trader) {
|
||||
const strategyId = await ensureClaw402Strategy()
|
||||
trader = await api.createTrader({
|
||||
name: 'NOFX Autopilot',
|
||||
ai_model_id: claw402Model.id,
|
||||
exchange_id: hyperliquidExchange.id,
|
||||
strategy_id: strategyId,
|
||||
scan_interval_minutes: 5,
|
||||
is_cross_margin: true,
|
||||
show_in_competition: true,
|
||||
btc_eth_leverage: 10,
|
||||
altcoin_leverage: 10,
|
||||
})
|
||||
}
|
||||
if (!trader.is_running) {
|
||||
await api.startTrader(trader.trader_id)
|
||||
|
||||
if (outcome.warning) {
|
||||
toast.warning(outcome.warning)
|
||||
}
|
||||
await onRefresh()
|
||||
toast.success('NOFX Autopilot is running')
|
||||
navigate(buildDashboardPath(trader.trader_id))
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Launch failed')
|
||||
navigate(buildDashboardPath(outcome.traderId))
|
||||
} finally {
|
||||
setLaunching(false)
|
||||
}
|
||||
@@ -383,18 +394,20 @@ export function AutopilotLaunchPanel({
|
||||
action?: JSX.Element
|
||||
}> = [
|
||||
{
|
||||
title: 'AI fee wallet',
|
||||
title: 'Step 1 · Fund the AI wallet ($1+)',
|
||||
detail:
|
||||
'Pays Claw402.ai data and model calls with Base USDC. This is separate from trading collateral.',
|
||||
'The AI pays a tiny fee each time it thinks. Send $1 or more of USDC on the Base network to this address — from Binance, OKX, Coinbase or any wallet. Separate from your trading money.',
|
||||
status: feeReady ? 'ready' : 'action',
|
||||
meta: feeWalletAddress
|
||||
? `${shortAddress(feeWalletAddress)} · ${formatUSDC(feeWalletBalance)} USDC`
|
||||
: 'Base USDC wallet required',
|
||||
? `${shortAddress(feeWalletAddress)} · ${formatUSDC(feeWalletBalance)} USDC${
|
||||
feeReady ? '' : ` · needs ≥ ${minAIFeeUSDC} USDC`
|
||||
}`
|
||||
: 'Takes 1 minute — we create the wallet for you',
|
||||
action: feeWalletAddress ? (
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenClaw402Config?.()}
|
||||
onClick={() => navigate(ROUTES.welcome)}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-nofx-accent"
|
||||
>
|
||||
<CircleDollarSign className="h-3.5 w-3.5" />
|
||||
@@ -412,22 +425,22 @@ export function AutopilotLaunchPanel({
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenClaw402Config?.()}
|
||||
onClick={() => navigate(ROUTES.welcome)}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-nofx-accent"
|
||||
>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
Open
|
||||
Create
|
||||
</button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Hyperliquid trading wallet',
|
||||
title: 'Step 2 · Connect Hyperliquid',
|
||||
detail:
|
||||
'Connect an EVM wallet, approve a NOFX Agent, approve the builder fee, then save it to NOFX.',
|
||||
'Approve NOFX once with your crypto wallet (Rabby or MetaMask). This lets the AI place trades for you — it can never withdraw your money.',
|
||||
status: hyperliquidConnected ? 'ready' : 'action',
|
||||
meta: hyperliquidExchange?.hyperliquidWalletAddr
|
||||
? `${shortAddress(hyperliquidExchange.hyperliquidWalletAddr)} · authorized`
|
||||
: 'Agent and trading authorization required',
|
||||
: 'A few clicks + 3 wallet signatures',
|
||||
action: (
|
||||
<button
|
||||
type="button"
|
||||
@@ -440,28 +453,32 @@ export function AutopilotLaunchPanel({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Trading balance',
|
||||
title: 'Step 3 · Add trading money ($12+)',
|
||||
detail:
|
||||
'Deposit USDC to Hyperliquid. NOFX uses it as margin for the Claw402 Autopilot strategy.',
|
||||
'Deposit USDC into your Hyperliquid account (app.hyperliquid.xyz → Deposit, USDC on Arbitrum). This is what the AI trades with — start small, you can add more anytime.',
|
||||
status: tradingBalanceReady
|
||||
? 'ready'
|
||||
: hyperliquidConnected
|
||||
? 'action'
|
||||
: 'blocked',
|
||||
meta: hyperliquidConnected
|
||||
? `${formatUSDC(tradingBalance)} USDC available`
|
||||
: 'Connect Hyperliquid first',
|
||||
? `${formatUSDC(tradingBalance)} USDC available${
|
||||
tradingBalanceReady ? '' : ` · needs ≥ ${minTradingUSDC} USDC`
|
||||
}`
|
||||
: 'Finish step 2 first',
|
||||
},
|
||||
{
|
||||
title: 'NOFX Autopilot',
|
||||
title: 'Step 4 · Press start',
|
||||
detail:
|
||||
'Reads the Claw402 board, fetches Signal Lab and liquidation structure, confirms with candles, then trades full-size 10x only when the setup is strong enough.',
|
||||
'The AI reads the market every few minutes, picks its trades, and manages them on its own. Watch every decision live on the dashboard — stop it with one click anytime.',
|
||||
status: allReady ? 'ready' : 'blocked',
|
||||
meta: autopilotTrader?.is_running
|
||||
? 'Running'
|
||||
? 'Running — open the dashboard to watch'
|
||||
: autopilotTrader
|
||||
? 'Ready to start'
|
||||
: 'Ready to create when setup is complete',
|
||||
: allReady
|
||||
? 'Everything is ready — press the button'
|
||||
: 'Unlocks when steps 1–3 are green',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -470,16 +487,10 @@ export function AutopilotLaunchPanel({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (onOpenClaw402Config) {
|
||||
onOpenClaw402Config()
|
||||
} else {
|
||||
navigate(ROUTES.welcome)
|
||||
}
|
||||
}}
|
||||
onClick={() => navigate(ROUTES.welcome)}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-white hover:bg-nofx-accent"
|
||||
>
|
||||
Open Claw402 wallet
|
||||
Set up the AI wallet
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</button>
|
||||
)
|
||||
@@ -538,7 +549,7 @@ export function AutopilotLaunchPanel({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={launchAutopilot}
|
||||
onClick={() => void handleLaunch()}
|
||||
disabled={launching || !allReady}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-white hover:bg-nofx-accent disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
@@ -553,7 +564,10 @@ export function AutopilotLaunchPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="overflow-hidden rounded-xl border border-nofx-gold/20 bg-nofx-bg-lighter">
|
||||
<section
|
||||
id="autopilot-launch-panel"
|
||||
className="overflow-hidden rounded-xl border border-nofx-gold/20 bg-nofx-bg-lighter"
|
||||
>
|
||||
<div className="grid gap-0 xl:grid-cols-[1.05fr_0.95fr]">
|
||||
<div className="p-5 md:p-6">
|
||||
<div className="mb-5 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
@@ -566,8 +580,8 @@ export function AutopilotLaunchPanel({
|
||||
Start NOFX Autopilot in minutes
|
||||
</h2>
|
||||
<p className="mt-2 max-w-2xl text-sm leading-6 text-nofx-text-muted">
|
||||
One strategy, one launch path. Fund the AI fee wallet, authorize
|
||||
Hyperliquid, deposit USDC, then run the Claw402 Autopilot.
|
||||
Four small steps, about $13 total. No API keys, no config
|
||||
files — the AI trades for you, and you can stop it anytime.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
@@ -36,9 +36,9 @@ function formatDuration(minutes: number): string {
|
||||
}
|
||||
|
||||
// Format date
|
||||
function formatDate(dateStr: string): string {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
function formatDate(value: number | string): string {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (isNaN(date.getTime())) return '-'
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
month: '2-digit',
|
||||
|
||||
@@ -1,371 +1,686 @@
|
||||
import {
|
||||
BookOpen,
|
||||
Settings,
|
||||
GitBranch,
|
||||
Monitor,
|
||||
Shield,
|
||||
TrendingUp,
|
||||
Wrench,
|
||||
Bot,
|
||||
Shield,
|
||||
Monitor,
|
||||
Zap,
|
||||
GitBranch,
|
||||
} from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
|
||||
/**
|
||||
* FAQ content model. Answers are composed from typed blocks so the renderer
|
||||
* stays generic — no per-question JSX special cases. Inline `code` spans are
|
||||
* written with backticks and parsed by the renderer.
|
||||
*/
|
||||
export type FAQBlock =
|
||||
| { type: 'p'; text: string }
|
||||
| { type: 'list'; items: string[] }
|
||||
| { type: 'steps'; items: string[] }
|
||||
| { type: 'note'; text: string }
|
||||
| { type: 'links'; links: { label: string; href: string }[] }
|
||||
|
||||
export interface FAQItem {
|
||||
id: string
|
||||
questionKey: string
|
||||
answerKey: string
|
||||
question: string
|
||||
blocks: FAQBlock[]
|
||||
}
|
||||
|
||||
export interface FAQCategory {
|
||||
id: string
|
||||
titleKey: string
|
||||
title: string
|
||||
icon: LucideIcon
|
||||
items: FAQItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* FAQ Data Configuration
|
||||
* Comprehensive FAQ covering all aspects of NOFX
|
||||
*
|
||||
* Categories:
|
||||
* 1. Getting Started - Basic concepts and overview
|
||||
* 2. Installation - Setup methods and requirements
|
||||
* 3. Configuration - AI models, exchanges, strategies
|
||||
* 4. Trading - How trading works, common issues
|
||||
* 5. Technical Issues - Troubleshooting common problems
|
||||
* 6. Security - API keys, encryption, best practices
|
||||
* 7. Features - Strategy Studio, Backtest
|
||||
* 8. Contributing - How to contribute to the project
|
||||
*/
|
||||
/** Plain text of an item, used by the search filter. */
|
||||
export function faqItemSearchText(item: FAQItem): string {
|
||||
const parts: string[] = [item.question]
|
||||
for (const block of item.blocks) {
|
||||
if (block.type === 'p' || block.type === 'note') parts.push(block.text)
|
||||
else if (block.type === 'list' || block.type === 'steps')
|
||||
parts.push(block.items.join(' '))
|
||||
else if (block.type === 'links')
|
||||
parts.push(block.links.map((l) => l.label).join(' '))
|
||||
}
|
||||
return parts.join(' ').toLowerCase()
|
||||
}
|
||||
|
||||
export const faqCategories: FAQCategory[] = [
|
||||
// ===== 1. GETTING STARTED =====
|
||||
// ───────────────────────── Getting started ─────────────────────────
|
||||
{
|
||||
id: 'getting-started',
|
||||
titleKey: 'faqCategoryGettingStarted',
|
||||
title: 'Getting Started',
|
||||
icon: BookOpen,
|
||||
items: [
|
||||
{
|
||||
id: 'what-is-nofx',
|
||||
questionKey: 'faqWhatIsNOFX',
|
||||
answerKey: 'faqWhatIsNOFXAnswer',
|
||||
question: 'What is NOFX?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'NOFX is an open-source, self-hosted AI trading terminal. Its flagship mode is the NOFX Autopilot: an AI agent that reads the Claw402.ai signal board, verifies candidates with Signal Lab and liquidation structure, confirms timing with raw candles, and executes on Hyperliquid — all on your own machine, with your keys never leaving your server.',
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Beyond the Autopilot you can build custom strategies in Strategy Studio, run multiple AI traders side by side, and compare them on the leaderboard.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'how-does-it-work',
|
||||
questionKey: 'faqHowDoesItWork',
|
||||
answerKey: 'faqHowDoesItWorkAnswer',
|
||||
id: 'what-do-i-need',
|
||||
question: 'What do I need before launching the Autopilot?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Two funded accounts — the guided launch on the Config page walks you through both:',
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'An AI fee wallet: a Base-chain USDC wallet that pays for AI model and market-data calls. Minimum `1 USDC` to launch.',
|
||||
'A Hyperliquid account with trading authorization and at least `12 USDC` available as margin.',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
text: 'The launch button runs a server-side preflight that checks every prerequisite and points you at the exact step that is missing, so you cannot start a half-configured bot.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'which-markets',
|
||||
question: 'Which markets can it trade?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'The Autopilot trades Hyperliquid perpetuals: crypto majors (BTC, ETH, SOL, …) plus the xyz synthetic markets covering US stocks, indices, commodities, and FX — so one account gives the AI a multi-asset universe.',
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Manual traders built in Strategy Studio can also connect Binance, Bybit, OKX, Bitget, KuCoin, Gate, Aster, and Lighter.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ai-models',
|
||||
question: 'Which AI models does it use? Do I need API keys?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'No API keys are required. NOFX routes inference through Claw402 pay-as-you-go infrastructure: your AI fee wallet pays per call with Base USDC, and the terminal accesses supported models (DeepSeek and other frontier models) on demand.',
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Power users can still plug in their own provider keys (OpenAI, Claude, Gemini, DeepSeek, Qwen, Grok, Kimi, or any OpenAI-compatible endpoint) under Config → Models.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'is-it-profitable',
|
||||
questionKey: 'faqIsProfitable',
|
||||
answerKey: 'faqIsProfitableAnswer',
|
||||
},
|
||||
{
|
||||
id: 'supported-exchanges',
|
||||
questionKey: 'faqSupportedExchanges',
|
||||
answerKey: 'faqSupportedExchangesAnswer',
|
||||
},
|
||||
{
|
||||
id: 'supported-ai-models',
|
||||
questionKey: 'faqSupportedAIModels',
|
||||
answerKey: 'faqSupportedAIModelsAnswer',
|
||||
},
|
||||
{
|
||||
id: 'system-requirements',
|
||||
questionKey: 'faqSystemRequirements',
|
||||
answerKey: 'faqSystemRequirementsAnswer',
|
||||
question: 'Will it make money?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'No one can promise that, and you should distrust anyone who does. The AI trades a systematic process, but markets are adversarial and past performance never guarantees future results.',
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
text: 'The dashboard is deliberately honest about performance: it separates realized from unrealized P/L, shows the fee-drag chain (gross − fees = net), profit factor, and max drawdown computed from your real starting balance. Watch those numbers, start small, and only trade money you can afford to lose.',
|
||||
},
|
||||
{
|
||||
type: 'note',
|
||||
text: 'Trading involves substantial risk of loss. NOFX is software, not investment advice.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ===== 2. INSTALLATION =====
|
||||
// ───────────────────────── Launch & wallets ─────────────────────────
|
||||
{
|
||||
id: 'installation',
|
||||
titleKey: 'faqCategoryInstallation',
|
||||
icon: Settings,
|
||||
items: [
|
||||
{
|
||||
id: 'how-to-install',
|
||||
questionKey: 'faqHowToInstall',
|
||||
answerKey: 'faqHowToInstallAnswer',
|
||||
},
|
||||
{
|
||||
id: 'windows-installation',
|
||||
questionKey: 'faqWindowsInstallation',
|
||||
answerKey: 'faqWindowsInstallationAnswer',
|
||||
},
|
||||
{
|
||||
id: 'docker-deployment',
|
||||
questionKey: 'faqDockerDeployment',
|
||||
answerKey: 'faqDockerDeploymentAnswer',
|
||||
},
|
||||
{
|
||||
id: 'manual-installation',
|
||||
questionKey: 'faqManualInstallation',
|
||||
answerKey: 'faqManualInstallationAnswer',
|
||||
},
|
||||
{
|
||||
id: 'server-deployment',
|
||||
questionKey: 'faqServerDeployment',
|
||||
answerKey: 'faqServerDeploymentAnswer',
|
||||
},
|
||||
{
|
||||
id: 'update-nofx',
|
||||
questionKey: 'faqUpdateNOFX',
|
||||
answerKey: 'faqUpdateNOFXAnswer',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ===== 3. CONFIGURATION =====
|
||||
{
|
||||
id: 'configuration',
|
||||
titleKey: 'faqCategoryConfiguration',
|
||||
id: 'launch-wallets',
|
||||
title: 'Launch & Wallets',
|
||||
icon: Zap,
|
||||
items: [
|
||||
{
|
||||
id: 'configure-ai-models',
|
||||
questionKey: 'faqConfigureAIModels',
|
||||
answerKey: 'faqConfigureAIModelsAnswer',
|
||||
id: 'ai-fee-wallet',
|
||||
question: 'What is the AI fee wallet?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'A dedicated EVM wallet on Base that pays for AI model calls and paid market data (x402 micropayments). It is completely separate from your trading collateral — it never touches Hyperliquid.',
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'The guided setup creates it for you (or reuses an existing one).',
|
||||
'Deposit only USDC on the Base network to its address.',
|
||||
'Launch requires at least `1 USDC`; the balance display refreshes automatically after a deposit.',
|
||||
'A typical cycle costs a fraction of a cent to a few cents depending on the model.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'configure-exchanges',
|
||||
questionKey: 'faqConfigureExchanges',
|
||||
answerKey: 'faqConfigureExchangesAnswer',
|
||||
id: 'fee-wallet-private-key',
|
||||
question: 'Where is the AI fee wallet private key kept?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'The key is generated locally on your server, stored encrypted (AES-256) in your own database, and shown to you once in the onboarding screen. Back it up — it cannot be recovered if you lose your database.',
|
||||
},
|
||||
{
|
||||
type: 'note',
|
||||
text: 'Keep only fee money in this wallet. It exists to pay for AI calls, not to hold savings.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'binance-api-setup',
|
||||
questionKey: 'faqBinanceAPISetup',
|
||||
answerKey: 'faqBinanceAPISetupAnswer',
|
||||
id: 'hyperliquid-authorization',
|
||||
question: 'How does the Hyperliquid authorization work? Is it safe?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'NOFX uses Hyperliquid agent wallets, so your main wallet key is never shared. The connect flow has four signed steps:',
|
||||
},
|
||||
{
|
||||
type: 'steps',
|
||||
items: [
|
||||
'Connect your EVM wallet (Rabby, MetaMask, OKX, Coinbase Wallet).',
|
||||
'Approve a freshly generated NOFX agent wallet — valid for 180 days, trading only.',
|
||||
'Approve the builder fee (a small per-order fee that funds the platform).',
|
||||
'Save the agent key to your NOFX server (stored encrypted).',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
text: 'The agent wallet can place and close orders, nothing else. It cannot withdraw funds, and your collateral stays inside your own Hyperliquid account at all times.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'hyperliquid-setup',
|
||||
questionKey: 'faqHyperliquidSetup',
|
||||
answerKey: 'faqHyperliquidSetupAnswer',
|
||||
id: 'launch-preflight',
|
||||
question: 'What does the launch preflight check?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Before anything is created or changed, the server verifies the full chain with live data:',
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'AI model is enabled and has a credential.',
|
||||
'AI fee wallet key is valid and the Base USDC balance is at least `1 USDC` (queried on-chain).',
|
||||
'Hyperliquid account is authorized (agent + builder fee) and reachable.',
|
||||
'Trading funds: at least `12 USDC` counting equity in open positions.',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Each failing check names the exact fix and deep-links into the guided setup. The same checks are enforced server-side on every start, so the UI cannot be bypassed accidentally.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'create-strategy',
|
||||
questionKey: 'faqCreateStrategy',
|
||||
answerKey: 'faqCreateStrategyAnswer',
|
||||
id: 'relaunch-behavior',
|
||||
question: 'What happens if I press Launch again?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Launching is idempotent. If a NOFX Autopilot already exists, the launcher updates it with the current strategy config and restarts it — it never creates a duplicate. A restart can take up to a minute if the bot is mid-cycle; the UI waits for it.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'create-trader',
|
||||
questionKey: 'faqCreateTrader',
|
||||
answerKey: 'faqCreateTraderAnswer',
|
||||
id: 'deposit-not-showing',
|
||||
question: 'I deposited USDC but the balance still shows zero.',
|
||||
blocks: [
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'AI fee wallet: make sure you sent USDC on the Base network to the exact address shown. Balances are cached for ~30 seconds, and the setup panel re-checks automatically every few seconds.',
|
||||
'Hyperliquid: deposits land in your own Hyperliquid account; the balance step polls the live account state. Use Refresh in the guided panel if in doubt.',
|
||||
'If the on-chain RPC is temporarily unreachable, the panel marks the balance as unknown instead of zero — retry in a minute.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ===== 4. TRADING =====
|
||||
// ───────────────────────── Trading & execution ─────────────────────────
|
||||
{
|
||||
id: 'trading',
|
||||
titleKey: 'faqCategoryTrading',
|
||||
title: 'Trading & Execution',
|
||||
icon: TrendingUp,
|
||||
items: [
|
||||
{
|
||||
id: 'how-ai-decides',
|
||||
questionKey: 'faqHowAIDecides',
|
||||
answerKey: 'faqHowAIDecidesAnswer',
|
||||
id: 'autopilot-pipeline',
|
||||
question: 'How does the Autopilot strategy work, step by step?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Every cycle runs the same four-stage funnel — each stage can reject a candidate, and only setups that survive all four get traded:',
|
||||
},
|
||||
{
|
||||
type: 'steps',
|
||||
items: [
|
||||
'Build the universe — pull the live Claw402.ai ranking and take the top candidates (default 10) across crypto majors and the xyz synthetic markets (US stocks, indices, commodities, FX), each with a direction bias and signal z-score.',
|
||||
'Verify each candidate — fetch its Signal Lab deep signal plus the cost/liquidation structure around price: liquidation clusters and cost bases show whether a move has fuel ahead of it or walls against it.',
|
||||
'Confirm timing — read the raw 15-minute OHLCV candles (30 bars) to check the entry is riding structure, not chasing an extended move.',
|
||||
'Decide and size — only setups clearing the confidence threshold (default `78/100`) with roughly `3:1` risk/reward get positions at 10x; closes always execute before opens, and both long and short books are considered every cycle.',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
text: 'A fifth layer sits outside the AI entirely: hard risk controls (position cap, leverage caps, margin ceiling, trade throttle) reject any decision that violates them, no matter how confident the model is.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'decision-frequency',
|
||||
questionKey: 'faqDecisionFrequency',
|
||||
answerKey: 'faqDecisionFrequencyAnswer',
|
||||
id: 'data-sources',
|
||||
question: 'What data does it use, and which parts are paid?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'Claw402.ai signal data — the ranking board, per-symbol Signal Lab deep signals, and market net-flow. These are paid endpoints, billed per call in USDC from your AI fee wallet (x402 micropayments).',
|
||||
'Cost/liquidation heatmaps — aggregated position-cost and liquidation-cluster structure for each market.',
|
||||
'Hyperliquid market data — raw OHLCV candles and the live L2 order book (free public data).',
|
||||
'Your account via the agent wallet — equity, available margin, open positions with PnL.',
|
||||
'Its own history — closed trades feed win rate, profit factor and drawdown back into the next prompt, so the AI knows its recent form.',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'note',
|
||||
text: 'The dashboard polls the paid Claw402 endpoints slowly (every few minutes) to conserve your fee wallet — market-data panels stay live from the free feeds.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'no-trades-executing',
|
||||
questionKey: 'faqNoTradesExecuting',
|
||||
answerKey: 'faqNoTradesExecutingAnswer',
|
||||
id: 'decision-cycle',
|
||||
question: 'How often does the AI make decisions?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'The Autopilot runs a scan cycle every 5–15 minutes depending on how you launched it (configurable per trader, minimum 3 minutes). The first cycle starts right after launch; a single cycle usually takes 30–60 seconds because the AI reads the full market context before deciding.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'only-short-positions',
|
||||
questionKey: 'faqOnlyShortPositions',
|
||||
answerKey: 'faqOnlyShortPositionsAnswer',
|
||||
id: 'what-ai-sees',
|
||||
question: 'What information does the AI see each cycle?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'Your account: equity, available margin, open positions with PnL.',
|
||||
'The Claw402 ranking board: candidate universe with direction bias.',
|
||||
'Signal Lab deep signals and cost/liquidation structure per candidate.',
|
||||
'Raw OHLCV candles for timing confirmation.',
|
||||
'Its own track record: win rate, profit factor, drawdown, recent trades.',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Every cycle is stored as a decision record — the Execution Log on the dashboard shows the reasoning chain, actions, and any blocked orders.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'leverage-settings',
|
||||
questionKey: 'faqLeverageSettings',
|
||||
answerKey: 'faqLeverageSettingsAnswer',
|
||||
id: 'leverage-and-risk',
|
||||
question: 'What leverage and risk controls does it use?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'The Autopilot defaults to 10x cross margin. Hard risk controls run outside the AI and cannot be overridden by it:',
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'Position-count cap from the strategy config — new opens are rejected at the cap.',
|
||||
'Leverage limits per asset class (BTC/ETH vs altcoins).',
|
||||
'A trade throttle blocks churn, e.g. closing a barely-moved position minutes after opening it.',
|
||||
'Safe mode (below) protects the book when the AI itself is failing.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'stop-loss-take-profit',
|
||||
questionKey: 'faqStopLossTakeProfit',
|
||||
answerKey: 'faqStopLossTakeProfitAnswer',
|
||||
id: 'safe-mode',
|
||||
question: 'What is safe mode?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'If the AI fails 3 cycles in a row (provider outage, empty fee wallet, bad responses), the trader enters safe mode: no new positions are opened, existing positions keep their protection, and the loop keeps retrying. It exits safe mode automatically on the next successful AI call.',
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Safe mode is shown as a banner on the dashboard together with the reason, so it never happens silently.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'multiple-traders',
|
||||
questionKey: 'faqMultipleTraders',
|
||||
answerKey: 'faqMultipleTradersAnswer',
|
||||
id: 'fee-wallet-empty-mid-run',
|
||||
question: 'What happens if the AI fee wallet runs out mid-run?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'AI calls start failing with a clear "out of funds" status. The dashboard shows a persistent red banner with the wallet balance, and after three failed cycles the bot enters safe mode. Top up Base USDC to the fee wallet and the trader recovers on its own — no restart needed.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ai-costs',
|
||||
questionKey: 'faqAICosts',
|
||||
answerKey: 'faqAICostsAnswer',
|
||||
id: 'trading-fees',
|
||||
question: 'What fees am I paying?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'Hyperliquid trading fees on every order, plus the approved builder fee.',
|
||||
'AI/data costs paid per call from the fee wallet (cents per cycle).',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Fees are the silent killer of high-frequency strategies. The dashboard stats strip shows the full chain — gross realized P/L, minus fees, equals net — so you can see immediately whether fees are eating the edge.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'stop-and-manual',
|
||||
question: 'How do I stop the bot or close positions manually?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'Stop: use the Stop button on the Config page trader list. Stopping halts the decision loop; open positions remain open and are yours to manage.',
|
||||
'Manual close: close any position from the dashboard positions panel — manual closes sync back into the position history.',
|
||||
'Emergencies: you can always manage positions directly on Hyperliquid; NOFX never locks you out of your own account.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ===== 5. TECHNICAL ISSUES =====
|
||||
// ───────────────────────── Dashboard & metrics ─────────────────────────
|
||||
{
|
||||
id: 'technical-issues',
|
||||
titleKey: 'faqCategoryTechnicalIssues',
|
||||
icon: Wrench,
|
||||
items: [
|
||||
{
|
||||
id: 'port-in-use',
|
||||
questionKey: 'faqPortInUse',
|
||||
answerKey: 'faqPortInUseAnswer',
|
||||
},
|
||||
{
|
||||
id: 'frontend-not-loading',
|
||||
questionKey: 'faqFrontendNotLoading',
|
||||
answerKey: 'faqFrontendNotLoadingAnswer',
|
||||
},
|
||||
{
|
||||
id: 'database-locked',
|
||||
questionKey: 'faqDatabaseLocked',
|
||||
answerKey: 'faqDatabaseLockedAnswer',
|
||||
},
|
||||
{
|
||||
id: 'talib-not-found',
|
||||
questionKey: 'faqTALibNotFound',
|
||||
answerKey: 'faqTALibNotFoundAnswer',
|
||||
},
|
||||
{
|
||||
id: 'ai-api-timeout',
|
||||
questionKey: 'faqAIAPITimeout',
|
||||
answerKey: 'faqAIAPITimeoutAnswer',
|
||||
},
|
||||
{
|
||||
id: 'binance-position-mode',
|
||||
questionKey: 'faqBinancePositionMode',
|
||||
answerKey: 'faqBinancePositionModeAnswer',
|
||||
},
|
||||
{
|
||||
id: 'balance-shows-zero',
|
||||
questionKey: 'faqBalanceShowsZero',
|
||||
answerKey: 'faqBalanceShowsZeroAnswer',
|
||||
},
|
||||
{
|
||||
id: 'docker-pull-failed',
|
||||
questionKey: 'faqDockerPullFailed',
|
||||
answerKey: 'faqDockerPullFailedAnswer',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ===== 6. SECURITY =====
|
||||
{
|
||||
id: 'security',
|
||||
titleKey: 'faqCategorySecurity',
|
||||
icon: Shield,
|
||||
items: [
|
||||
{
|
||||
id: 'api-key-storage',
|
||||
questionKey: 'faqAPIKeyStorage',
|
||||
answerKey: 'faqAPIKeyStorageAnswer',
|
||||
},
|
||||
{
|
||||
id: 'encryption-details',
|
||||
questionKey: 'faqEncryptionDetails',
|
||||
answerKey: 'faqEncryptionDetailsAnswer',
|
||||
},
|
||||
{
|
||||
id: 'security-best-practices',
|
||||
questionKey: 'faqSecurityBestPractices',
|
||||
answerKey: 'faqSecurityBestPracticesAnswer',
|
||||
},
|
||||
{
|
||||
id: 'can-nofx-steal-funds',
|
||||
questionKey: 'faqCanNOFXStealFunds',
|
||||
answerKey: 'faqCanNOFXStealFundsAnswer',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ===== 7. FEATURES =====
|
||||
{
|
||||
id: 'features',
|
||||
titleKey: 'faqCategoryFeatures',
|
||||
id: 'dashboard',
|
||||
title: 'Dashboard & Metrics',
|
||||
icon: Monitor,
|
||||
items: [
|
||||
{
|
||||
id: 'strategy-studio',
|
||||
questionKey: 'faqStrategyStudio',
|
||||
answerKey: 'faqStrategyStudioAnswer',
|
||||
id: 'metrics-meaning',
|
||||
question: 'What do the header metrics mean exactly?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'Equity — live account value including unrealized PnL.',
|
||||
'Total P/L (incl. unrealized) — equity versus your starting balance; moves with open positions.',
|
||||
'Realized P/L (closed trades) — net result of finished trades only; this is what win rate, profit factor and sharpe are computed from.',
|
||||
'Profit factor — gross wins ÷ gross losses on closed trades; above 1.0 means the closed book is net positive.',
|
||||
'Max drawdown — worst peak-to-trough dip of the realized equity curve, measured against your real starting balance.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'backtest-lab',
|
||||
questionKey: 'faqBacktestLab',
|
||||
answerKey: 'faqBacktestLabAnswer',
|
||||
id: 'pl-contradiction',
|
||||
question: 'Why is Total P/L positive while Realized P/L is negative?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'They measure different things. Realized P/L only counts closed trades; Total P/L also includes the unrealized gains of positions still open. A bot can be down on its closed trades while its open book carries enough unrealized profit to put total P/L in the green — and vice versa. Check the gross/fees/net strip to see how much of the realized result is fee drag.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'competition-mode',
|
||||
questionKey: 'faqCompetitionMode',
|
||||
answerKey: 'faqCompetitionModeAnswer',
|
||||
id: 'execution-log',
|
||||
question: 'Where can I see why the AI did (or refused) something?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'The Execution Log panel lists every cycle with the actions taken, the AI call duration, and any blocked orders with the exact guard that fired (throttle, position cap, risk control). Full reasoning chains are stored with each decision record.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'chain-of-thought',
|
||||
questionKey: 'faqChainOfThought',
|
||||
answerKey: 'faqChainOfThoughtAnswer',
|
||||
id: 'competition',
|
||||
question: 'What is the leaderboard / competition?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Traders with "show in competition" enabled appear on the public leaderboard, ranked by live performance. It is opt-in per trader and can be toggled from the trader list at any time.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ===== 8. AI MODELS =====
|
||||
// ───────────────────────── Security ─────────────────────────
|
||||
{
|
||||
id: 'ai-models',
|
||||
titleKey: 'faqCategoryAIModels',
|
||||
icon: Bot,
|
||||
id: 'security',
|
||||
title: 'Security',
|
||||
icon: Shield,
|
||||
items: [
|
||||
{
|
||||
id: 'which-ai-model-best',
|
||||
questionKey: 'faqWhichAIModelBest',
|
||||
answerKey: 'faqWhichAIModelBestAnswer',
|
||||
id: 'key-storage',
|
||||
question: 'How are my keys stored?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'All secrets (agent keys, fee wallet key, exchange API keys) are AES-256 encrypted at rest in your own database.',
|
||||
'Optional RSA transport encryption protects secrets in flight between browser and server.',
|
||||
'NOFX is self-hosted: nothing is sent to any third-party server. The code is open source and auditable.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'custom-ai-api',
|
||||
questionKey: 'faqCustomAIAPI',
|
||||
answerKey: 'faqCustomAIAPIAnswer',
|
||||
id: 'can-nofx-steal-funds',
|
||||
question: 'Can NOFX withdraw or steal my funds?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'No. On Hyperliquid, NOFX only ever holds an agent wallet, which by protocol design can trade but cannot withdraw. Your collateral stays in your own account under your main wallet’s control.',
|
||||
},
|
||||
{
|
||||
type: 'note',
|
||||
text: 'If you connect a CEX instead, create its API key with trading permission only — disable withdrawals and set an IP whitelist.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ai-hallucinations',
|
||||
questionKey: 'faqAIHallucinations',
|
||||
answerKey: 'faqAIHallucinationsAnswer',
|
||||
},
|
||||
{
|
||||
id: 'compare-ai-models',
|
||||
questionKey: 'faqCompareAIModels',
|
||||
answerKey: 'faqCompareAIModelsAnswer',
|
||||
id: 'registration-model',
|
||||
question: 'Why can’t anyone else register on my instance?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'By design, an instance is single-operator: the first account registered becomes the operator and registration closes ("System already initialized"). This prevents strangers from creating accounts on an exposed deployment. Run one instance per operator.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ===== 9. CONTRIBUTING =====
|
||||
// ───────────────────────── Self-hosting ─────────────────────────
|
||||
{
|
||||
id: 'self-hosting',
|
||||
title: 'Self-Hosting & Troubleshooting',
|
||||
icon: Wrench,
|
||||
items: [
|
||||
{
|
||||
id: 'how-to-install',
|
||||
question: 'How do I install NOFX?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'One line on Linux/macOS (installs and starts everything via Docker):',
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'Script: `curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash`',
|
||||
'Docker: download `docker-compose.prod.yml` and run `docker compose -f docker-compose.prod.yml up -d`',
|
||||
'Windows: install Docker Desktop, then use the Docker route above.',
|
||||
'From source: Go 1.21+, Node 18+, TA-Lib (`brew install ta-lib` / `apt-get install libta-lib0-dev`), then `go run .` and `npm --prefix web run dev`.',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Then open `http://127.0.0.1:3000` — the web UI is on port 3000, the API on 8080.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'how-to-update',
|
||||
question: 'How do I update?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Re-run the install script, or with Docker: `docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d`. Your database and keys live in the mounted `data/` directory and survive updates. Running traders are restarted automatically after the backend comes back.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'launch-blocked',
|
||||
question: 'Launch is blocked by a failing check — now what?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Read the message: every preflight failure names its fix and routes you to the right setup step — fund the AI wallet, finish the Hyperliquid authorization, or deposit trading USDC. Balances are re-checked live, so once you fix the item the launch goes through.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'exchange-unreachable',
|
||||
question: 'The exchange account shows "invalid credentials" or "unavailable".',
|
||||
blocks: [
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'Invalid credentials: the agent authorization expired (180 days) or the saved key is stale — reconnect the Hyperliquid wallet; the flow offers a one-click renewal.',
|
||||
'Unavailable: the exchange API did not respond; the account state is cached for 30 seconds, so wait and refresh.',
|
||||
'CEX keys: verify trading permission, IP whitelist, and that futures/perp access is enabled.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'where-are-logs',
|
||||
question: 'Where are the logs?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'list',
|
||||
items: [
|
||||
'Backend: `docker logs nofx-trading` (or the terminal running `go run .`).',
|
||||
'Per-cycle AI reasoning and errors: the dashboard Execution Log.',
|
||||
'Frontend build/runtime issues: browser devtools console.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'port-conflicts',
|
||||
question: 'Port 3000 or 8080 is already in use.',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Stop the conflicting service or remap the published ports in your compose file (e.g. `"3100:80"` for the frontend, `"8180:8080"` for the API), then restart the containers.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ───────────────────────── Contributing ─────────────────────────
|
||||
{
|
||||
id: 'contributing',
|
||||
titleKey: 'faqCategoryContributing',
|
||||
title: 'Contributing',
|
||||
icon: GitBranch,
|
||||
items: [
|
||||
{
|
||||
id: 'how-to-contribute',
|
||||
questionKey: 'faqHowToContribute',
|
||||
answerKey: 'faqHowToContributeAnswer',
|
||||
},
|
||||
{
|
||||
id: 'pr-guidelines',
|
||||
questionKey: 'faqPRGuidelines',
|
||||
answerKey: 'faqPRGuidelinesAnswer',
|
||||
question: 'How do I contribute code?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'links',
|
||||
links: [
|
||||
{
|
||||
label: 'Roadmap',
|
||||
href: 'https://github.com/orgs/NoFxAiOS/projects/3',
|
||||
},
|
||||
{
|
||||
label: 'Task Dashboard',
|
||||
href: 'https://github.com/orgs/NoFxAiOS/projects/5',
|
||||
},
|
||||
{
|
||||
label: 'CONTRIBUTING.md',
|
||||
href: 'https://github.com/NoFxAiOS/nofx/blob/dev/CONTRIBUTING.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'steps',
|
||||
items: [
|
||||
'Pick a task from the boards above (filter by good first issue / help wanted) and comment "assign me".',
|
||||
'Fork the repo and branch from `dev`: `git checkout -b feat/your-topic`.',
|
||||
'Follow Conventional Commits; run `npm --prefix web run lint && npm --prefix web run build` before pushing.',
|
||||
'Open a PR against `NoFxAiOS/nofx:dev`, reference the issue (`Closes #123`), and attach screenshots for UI changes.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'bounty-program',
|
||||
questionKey: 'faqBountyProgram',
|
||||
answerKey: 'faqBountyProgramAnswer',
|
||||
question: 'Is there a bounty program?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Yes — selected issues carry cash bounties, plus badges, priority review, and beta access for regular contributors.',
|
||||
},
|
||||
{
|
||||
type: 'links',
|
||||
links: [
|
||||
{
|
||||
label: 'Issues with bounty label',
|
||||
href: 'https://github.com/NoFxAiOS/nofx/labels/bounty',
|
||||
},
|
||||
{
|
||||
label: 'Bounty claim template',
|
||||
href: 'https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'report-bugs',
|
||||
questionKey: 'faqReportBugs',
|
||||
answerKey: 'faqReportBugsAnswer',
|
||||
question: 'How do I report a bug?',
|
||||
blocks: [
|
||||
{
|
||||
type: 'p',
|
||||
text: 'Open a GitHub issue with the template: what you did, what happened, backend logs (`docker logs nofx-trading`), and screenshots. For suspected security issues, please follow the responsible-disclosure notes in SECURITY.md instead of a public issue.',
|
||||
},
|
||||
{
|
||||
type: 'links',
|
||||
links: [
|
||||
{
|
||||
label: 'New issue',
|
||||
href: 'https://github.com/NoFxAiOS/nofx/issues/new/choose',
|
||||
},
|
||||
{
|
||||
label: 'SECURITY.md',
|
||||
href: 'https://github.com/NoFxAiOS/nofx/blob/dev/SECURITY.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -671,226 +671,66 @@ export const translations = {
|
||||
configureSignalSourceNow: 'Configure Signal Source Now',
|
||||
|
||||
// FAQ Page
|
||||
faqTitle: 'Frequently Asked Questions',
|
||||
faqSubtitle: 'Find answers to common questions about NOFX',
|
||||
faqStillHaveQuestions: 'Still Have Questions?',
|
||||
faqContactUs: 'Join our community or check our GitHub for more help',
|
||||
|
||||
// FAQ Categories
|
||||
faqCategoryGettingStarted: 'Getting Started',
|
||||
faqCategoryInstallation: 'Installation',
|
||||
faqCategoryConfiguration: 'Configuration',
|
||||
faqCategoryTrading: 'Trading',
|
||||
faqCategoryTechnicalIssues: 'Technical Issues',
|
||||
faqCategorySecurity: 'Security',
|
||||
faqCategoryFeatures: 'Features',
|
||||
faqCategoryAIModels: 'AI Models',
|
||||
faqCategoryContributing: 'Contributing',
|
||||
|
||||
// ===== GETTING STARTED =====
|
||||
faqWhatIsNOFX: 'What is NOFX?',
|
||||
faqWhatIsNOFXAnswer:
|
||||
'NOFX is an open-source AI-powered trading operating system for cryptocurrency and US stock markets. It uses large language models (LLMs) like DeepSeek, GPT, Claude, Gemini to analyze market data and make autonomous trading decisions. Key features include: multi-AI model support, multi-exchange trading, and visual strategy builder.',
|
||||
|
||||
faqHowDoesItWork: 'How does NOFX work?',
|
||||
faqHowDoesItWorkAnswer:
|
||||
'NOFX works in 5 steps: 1) Configure AI models and exchange API credentials; 2) Create a trading strategy (coin selection, indicators, risk controls); 3) Create a "Trader" combining AI model + Exchange + Strategy; 4) Start the trader - it will analyze market data at regular intervals and make buy/sell/hold decisions; 5) Monitor performance on the dashboard. The AI uses Chain of Thought reasoning to explain each decision.',
|
||||
|
||||
faqIsProfitable: 'Is NOFX profitable?',
|
||||
faqIsProfitableAnswer:
|
||||
'AI trading is experimental and NOT guaranteed to be profitable. Cryptocurrency futures are highly volatile and risky. NOFX is designed for educational and research purposes. We strongly recommend: starting with small amounts (10-50 USDT), never investing more than you can afford to lose, thoroughly testing before live trading, and understanding that past performance does not guarantee future results.',
|
||||
|
||||
faqSupportedExchanges: 'Which exchanges are supported?',
|
||||
faqSupportedExchangesAnswer:
|
||||
'CEX (Centralized): Binance Futures, Bybit, OKX, Bitget. DEX (Decentralized): Hyperliquid, Aster DEX, Lighter. Each exchange has different features - Binance has the most liquidity, Hyperliquid is fully on-chain with no KYC required. Check the documentation for setup guides for each exchange.',
|
||||
|
||||
faqSupportedAIModels: 'Which AI models are supported?',
|
||||
faqSupportedAIModelsAnswer:
|
||||
'NOFX supports 7+ AI models: DeepSeek (recommended for cost/performance), Alibaba Qwen, OpenAI (GPT-5.2), Anthropic Claude, Google Gemini, xAI Grok, and Kimi (Moonshot). You can also use any OpenAI-compatible API endpoint. Each model has different strengths - DeepSeek is cost-effective, OpenAI models are powerful but expensive, Claude excels at reasoning.',
|
||||
|
||||
faqSystemRequirements: 'What are the system requirements?',
|
||||
faqSystemRequirementsAnswer:
|
||||
'Minimum: 2 CPU cores, 2GB RAM, 1GB disk space, stable internet. Recommended: 4GB RAM for running multiple traders. Supported OS: Linux, macOS, or Windows (via Docker or WSL2). Docker is the easiest installation method. For manual installation, you need Go 1.21+, Node.js 18+, and TA-Lib library.',
|
||||
|
||||
// ===== INSTALLATION =====
|
||||
faqHowToInstall: 'How do I install NOFX?',
|
||||
faqHowToInstallAnswer:
|
||||
'Easiest method (Linux/macOS): Run "curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash" - this installs Docker containers automatically. Then open http://127.0.0.1:3000 in your browser. For manual installation or development, clone the repository and follow the README instructions.',
|
||||
|
||||
faqWindowsInstallation: 'How do I install on Windows?',
|
||||
faqWindowsInstallationAnswer:
|
||||
'Three options: 1) Docker Desktop (Recommended) - Install Docker Desktop, then run "docker compose -f docker-compose.prod.yml up -d" in PowerShell; 2) WSL2 - Install Windows Subsystem for Linux, then follow Linux installation; 3) Docker in WSL2 - Best of both worlds, run the install script in WSL2 terminal. Access via http://127.0.0.1:3000',
|
||||
|
||||
faqDockerDeployment: 'Docker deployment keeps failing',
|
||||
faqDockerDeploymentAnswer:
|
||||
'Common solutions: 1) Check Docker is running: "docker info"; 2) Ensure sufficient memory (2GB minimum); 3) If stuck on "go build", try: "docker compose down && docker compose build --no-cache && docker compose up -d"; 4) Check logs: "docker compose logs -f"; 5) For slow pulls, configure a Docker mirror in daemon.json.',
|
||||
|
||||
faqManualInstallation: 'How do I install manually for development?',
|
||||
faqManualInstallationAnswer:
|
||||
'Prerequisites: Go 1.21+, Node.js 18+, TA-Lib. Steps: 1) Clone repo: "git clone https://github.com/NoFxAiOS/nofx.git"; 2) Install backend deps: "go mod download"; 3) Install frontend deps: "cd web && npm install"; 4) Build backend: "go build -o nofx"; 5) Run backend: "./nofx"; 6) Run frontend (new terminal): "cd web && npm run dev". Access at http://127.0.0.1:3000',
|
||||
|
||||
faqServerDeployment: 'How do I deploy to a remote server?',
|
||||
faqServerDeploymentAnswer:
|
||||
'Run the install script on your server - it auto-detects the server IP. Access via http://YOUR_SERVER_IP:3000. For HTTPS: 1) Use Cloudflare (free) - add domain, create A record pointing to server IP, set SSL to "Flexible"; 2) Enable TRANSPORT_ENCRYPTION=true in .env for browser-side encryption; 3) Access via https://your-domain.com',
|
||||
|
||||
faqUpdateNOFX: 'How do I update NOFX?',
|
||||
faqUpdateNOFXAnswer:
|
||||
'For Docker: Run "docker compose pull && docker compose up -d" to pull latest images and restart. For manual installation: "git pull && go build -o nofx" for backend, "cd web && npm install && npm run build" for frontend. Your configurations in data.db are preserved during updates.',
|
||||
|
||||
// ===== CONFIGURATION =====
|
||||
faqConfigureAIModels: 'How do I configure AI models?',
|
||||
faqConfigureAIModelsAnswer:
|
||||
'Go to Config page → AI Models section. For each model: 1) Get API key from the provider (links provided in UI); 2) Enter API key; 3) Optionally customize base URL and model name; 4) Save. API keys are encrypted before storage. Test the connection after saving to verify it works.',
|
||||
|
||||
faqConfigureExchanges: 'How do I configure exchange connections?',
|
||||
faqConfigureExchangesAnswer:
|
||||
'Go to Config page → Exchanges section. Click "Add Exchange", select exchange type, and enter credentials. For CEX (Binance/Bybit/OKX): Need API Key + Secret Key (+ Passphrase for OKX). For DEX (Hyperliquid/Aster/Lighter): Need wallet address and private key. Always enable only necessary permissions (Futures Trading) and consider IP whitelisting.',
|
||||
|
||||
faqBinanceAPISetup: 'How do I set up Binance API correctly?',
|
||||
faqBinanceAPISetupAnswer:
|
||||
'Important steps: 1) Create API key in Binance → API Management; 2) Enable ONLY "Enable Futures" permission; 3) Consider adding IP whitelist for security; 4) CRITICAL: Switch to Hedge Mode (双向持仓) in Futures settings → Preferences → Position Mode; 5) Ensure funds are in Futures wallet (not Spot). Common error -4061 means you need Hedge Mode.',
|
||||
|
||||
faqHyperliquidSetup: 'How do I set up Hyperliquid?',
|
||||
faqHyperliquidSetupAnswer:
|
||||
'Hyperliquid is a decentralized exchange requiring wallet authentication. Steps: 1) Go to app.hyperliquid.xyz; 2) Connect your wallet; 3) Generate an API wallet (recommended) or use your main wallet; 4) Copy the wallet address and private key; 5) In NOFX, add Hyperliquid exchange with these credentials. No KYC required, fully on-chain.',
|
||||
|
||||
faqCreateStrategy: 'How do I create a trading strategy?',
|
||||
faqCreateStrategyAnswer:
|
||||
'Go to Strategy Studio: 1) Coin Source - select which coins to trade (static list, AI500 pool, or OI Top ranking); 2) Indicators - enable technical indicators (EMA, MACD, RSI, ATR, Volume, OI, Funding Rate); 3) Risk Controls - set leverage limits, max positions, margin usage cap, position size limits; 4) Custom Prompt (optional) - add specific instructions for the AI. Save and assign to a trader.',
|
||||
|
||||
faqCreateTrader: 'How do I create and start a trader?',
|
||||
faqCreateTraderAnswer:
|
||||
'Go to Traders page: 1) Click "Create Trader"; 2) Select AI Model (must be configured first); 3) Select Exchange (must be configured first); 4) Select Strategy (or use default); 5) Set decision interval (e.g., 15 minutes); 6) Save, then click "Start" to begin trading. Monitor performance on Dashboard page.',
|
||||
|
||||
// ===== TRADING =====
|
||||
faqHowAIDecides: 'How does the AI make trading decisions?',
|
||||
faqHowAIDecidesAnswer:
|
||||
'The AI uses Chain of Thought (CoT) reasoning in 4 steps: 1) Position Analysis - reviews current holdings and P/L; 2) Risk Assessment - checks account margin, available balance; 3) Opportunity Evaluation - analyzes market data, indicators, candidate coins; 4) Final Decision - outputs specific action (buy/sell/hold) with reasoning. You can view the full reasoning in decision logs.',
|
||||
|
||||
faqDecisionFrequency: 'How often does the AI make decisions?',
|
||||
faqDecisionFrequencyAnswer:
|
||||
'Configurable per trader, default is 15 minutes. Too frequent polling can create churn and fee drag; 15-30 minutes gives signals time to develop. The execution layer also blocks early flat closes and rapid re-entry.',
|
||||
|
||||
faqNoTradesExecuting: "Why isn't my trader executing any trades?",
|
||||
faqNoTradesExecutingAnswer:
|
||||
'Common causes: 1) AI decided to wait (check decision logs for reasoning); 2) Insufficient balance in futures account; 3) Max positions limit reached (default: 3); 4) Exchange API issues (check error messages); 5) Strategy constraints too restrictive. Check Dashboard → Decision Logs for detailed AI reasoning each cycle.',
|
||||
|
||||
faqOnlyShortPositions: 'Why is the AI only opening short positions?',
|
||||
faqOnlyShortPositionsAnswer:
|
||||
'This is usually due to Binance Position Mode. Solution: Switch to Hedge Mode (双向持仓) in Binance Futures → Preferences → Position Mode. You must close all positions first. After switching, the AI can open both long and short positions independently.',
|
||||
|
||||
faqLeverageSettings: 'How do leverage settings work?',
|
||||
faqLeverageSettingsAnswer:
|
||||
'Leverage is set in Strategy → Risk Controls: BTC/ETH leverage (typically 5-20x) and Altcoin leverage (typically 3-10x). Higher leverage = higher risk and potential returns. Subaccounts may have restrictions (e.g., Binance subaccounts limited to 5x). The AI respects these limits when placing orders.',
|
||||
|
||||
faqStopLossTakeProfit: 'Does NOFX support stop-loss and take-profit?',
|
||||
faqStopLossTakeProfitAnswer:
|
||||
'The AI can suggest stop-loss/take-profit levels in its decisions, but these are guidance-based rather than hard-coded exchange orders. The AI monitors positions each cycle and may decide to close based on P/L. For guaranteed stop-loss, you can set exchange-level orders manually or adjust the strategy prompt to be more conservative.',
|
||||
|
||||
faqMultipleTraders: 'Can I run multiple traders?',
|
||||
faqMultipleTradersAnswer:
|
||||
'Yes! NOFX supports running 20+ concurrent traders. Each trader can have different: AI model, exchange account, strategy, decision interval. Use this to A/B test strategies, compare AI models, or diversify across exchanges. Monitor all traders on the Competition page.',
|
||||
|
||||
faqAICosts: 'How much do AI API calls cost?',
|
||||
faqAICostsAnswer:
|
||||
'Approximate daily costs per trader (15-min intervals): DeepSeek: $0.03-0.20; Qwen: $0.07-0.30; OpenAI: $0.70-2; Claude: $0.30-1. Costs depend on prompt length and response tokens. DeepSeek offers the best cost/performance ratio. Longer decision intervals reduce costs.',
|
||||
|
||||
// ===== TECHNICAL ISSUES =====
|
||||
faqPortInUse: 'Port 8080 or 3000 already in use',
|
||||
faqPortInUseAnswer:
|
||||
'Check what\'s using the port: "lsof -i :8080" (macOS/Linux) or "netstat -ano | findstr 8080" (Windows). Kill the process or change ports in .env: NOFX_BACKEND_PORT=8081, NOFX_FRONTEND_PORT=3001. Restart with "docker compose down && docker compose up -d".',
|
||||
|
||||
faqFrontendNotLoading: 'Frontend shows "Loading..." forever',
|
||||
faqFrontendNotLoadingAnswer:
|
||||
'Backend may not be running or reachable. Check: 1) "curl http://127.0.0.1:8080/api/health" should return {"status":"ok"}; 2) "docker compose ps" to verify containers are running; 3) Check backend logs: "docker compose logs nofx-backend"; 4) Ensure firewall allows port 8080.',
|
||||
|
||||
faqDatabaseLocked: 'Database locked error',
|
||||
faqDatabaseLockedAnswer:
|
||||
'Multiple processes accessing SQLite simultaneously. Solution: 1) Stop all processes: "docker compose down" or "pkill nofx"; 2) Remove lock files if present: "rm -f data/data.db-wal data/data.db-shm"; 3) Restart: "docker compose up -d". Only one backend instance should access the database.',
|
||||
|
||||
faqTALibNotFound: 'TA-Lib not found during build',
|
||||
faqTALibNotFoundAnswer:
|
||||
'TA-Lib is required for technical indicators. Install: macOS: "brew install ta-lib"; Ubuntu/Debian: "sudo apt-get install libta-lib0-dev"; CentOS: "yum install ta-lib-devel". After installing, rebuild: "go build -o nofx". Docker images include TA-Lib pre-installed.',
|
||||
|
||||
faqAIAPITimeout: 'AI API timeout or connection refused',
|
||||
faqAIAPITimeoutAnswer:
|
||||
'Check: 1) API key is valid (test with curl); 2) Network can reach API endpoint (ping/curl); 3) API provider is not down (check status page); 4) VPN/firewall not blocking; 5) Rate limits not exceeded. Default timeout is 120 seconds.',
|
||||
|
||||
faqBinancePositionMode: 'Binance error code -4061 (Position Mode)',
|
||||
faqBinancePositionModeAnswer:
|
||||
'Error: "Order\'s position side does not match user\'s setting". You\'re in One-way Mode but NOFX requires Hedge Mode. Fix: 1) Close ALL positions first; 2) Binance Futures → Settings (gear icon) → Preferences → Position Mode → Switch to "Hedge Mode" (双向持仓); 3) Restart your trader.',
|
||||
|
||||
faqBalanceShowsZero: 'Account balance shows 0',
|
||||
faqBalanceShowsZeroAnswer:
|
||||
'Funds are likely in Spot wallet, not Futures wallet. Solution: 1) In Binance, go to Wallet → Futures → Transfer; 2) Transfer USDT from Spot to Futures; 3) Refresh NOFX dashboard. Also check: funds not locked in savings/staking products.',
|
||||
|
||||
faqDockerPullFailed: 'Docker image pull failed or slow',
|
||||
faqDockerPullFailedAnswer:
|
||||
'Docker Hub can be slow in some regions. Solutions: 1) Configure a Docker mirror in /etc/docker/daemon.json: {"registry-mirrors": ["https://mirror.gcr.io"]}; 2) Restart Docker; 3) Retry pull. Alternatively, use GitHub Container Registry (ghcr.io) which may have better connectivity in your region.',
|
||||
|
||||
// ===== SECURITY =====
|
||||
faqAPIKeyStorage: 'How are API keys stored?',
|
||||
faqAPIKeyStorageAnswer:
|
||||
'API keys are encrypted using AES-256-GCM before storage in the local SQLite database. The encryption key (DATA_ENCRYPTION_KEY) is stored in your .env file. Keys are decrypted only in memory when needed for API calls. Never share your data.db or .env files.',
|
||||
|
||||
faqEncryptionDetails: 'What encryption does NOFX use?',
|
||||
faqEncryptionDetailsAnswer:
|
||||
'NOFX uses multiple encryption layers: 1) AES-256-GCM for database storage (API keys, secrets); 2) RSA-2048 for optional transport encryption (browser to server); 3) JWT for authentication tokens. Keys are generated during installation. Enable TRANSPORT_ENCRYPTION=true for HTTPS environments.',
|
||||
|
||||
faqSecurityBestPractices: 'What are security best practices?',
|
||||
faqSecurityBestPracticesAnswer:
|
||||
'Recommended: 1) Use exchange API keys with IP whitelist and minimal permissions (Futures Trading only); 2) Use dedicated subaccount for NOFX; 3) Enable TRANSPORT_ENCRYPTION for remote deployments; 4) Never share .env or data.db files; 5) Use HTTPS with valid certificates; 6) Regularly rotate API keys; 7) Monitor account activity.',
|
||||
|
||||
faqCanNOFXStealFunds: 'Can NOFX steal my funds?',
|
||||
faqCanNOFXStealFundsAnswer:
|
||||
'NOFX is open-source (AGPL-3.0 license) - you can audit all code on GitHub. API keys are stored locally on YOUR machine, never sent to external servers. NOFX only has the permissions you grant via API keys. For maximum safety: use API keys with trading-only permissions (no withdrawal), enable IP whitelist, use a dedicated subaccount.',
|
||||
|
||||
// ===== FEATURES =====
|
||||
faqStrategyStudio: 'What is Strategy Studio?',
|
||||
faqStrategyStudioAnswer:
|
||||
'Strategy Studio is a visual strategy builder where you configure: 1) Coin Sources - which cryptocurrencies to trade (static list, AI500 top coins, OI ranking); 2) Technical Indicators - EMA, MACD, RSI, ATR, Volume, Open Interest, Funding Rate; 3) Risk Controls - leverage limits, position sizing, margin caps; 4) Custom Prompts - specific instructions for AI. No coding required.',
|
||||
|
||||
faqCompetitionMode: 'What is Competition Mode?',
|
||||
faqCompetitionModeAnswer:
|
||||
'Competition page shows a real-time leaderboard of all your traders. Compare: ROI, P&L, Sharpe ratio, win rate, number of trades. Use this to A/B test different AI models, strategies, or configurations. Traders can be marked as "Show in Competition" to appear on the leaderboard.',
|
||||
|
||||
faqChainOfThought: 'What is Chain of Thought (CoT)?',
|
||||
faqChainOfThoughtAnswer:
|
||||
"Chain of Thought is the AI's reasoning process, visible in decision logs. The AI explains its thinking in 4 steps: 1) Current position analysis; 2) Account risk assessment; 3) Market opportunity evaluation; 4) Final decision rationale. This transparency helps you understand WHY the AI made each decision, useful for improving strategies.",
|
||||
|
||||
// ===== AI MODELS =====
|
||||
faqWhichAIModelBest: 'Which AI model should I use?',
|
||||
faqWhichAIModelBestAnswer:
|
||||
'Recommended: DeepSeek for best cost/performance ratio ($0.03-0.20/day at the default interval). Alternatives: OpenAI for best reasoning but expensive; Claude for nuanced analysis; Qwen for competitive pricing. You can run multiple traders with different models to compare. Check the Competition page to see which performs best for your strategy.',
|
||||
|
||||
faqCustomAIAPI: 'Can I use a custom AI API?',
|
||||
faqCustomAIAPIAnswer:
|
||||
'Yes! NOFX supports any OpenAI-compatible API. In Config → AI Models → Custom API: 1) Enter your API endpoint URL (e.g., https://your-api.com/v1); 2) Enter API key; 3) Specify model name. This works with self-hosted models, alternative providers, or Claude via third-party proxies.',
|
||||
|
||||
faqAIHallucinations: 'What about AI hallucinations?',
|
||||
faqAIHallucinationsAnswer:
|
||||
'AI models can sometimes produce incorrect or fabricated information ("hallucinations"). NOFX mitigates this by: 1) Providing structured prompts with real market data; 2) Enforcing JSON output format for decisions; 3) Validating orders before execution. However, AI trading is experimental - always monitor decisions and don\'t rely solely on AI judgment.',
|
||||
|
||||
faqCompareAIModels: 'How do I compare different AI models?',
|
||||
faqCompareAIModelsAnswer:
|
||||
'Create multiple traders with different AI models but same strategy/exchange. Run them simultaneously and compare on Competition page. Metrics to watch: ROI, win rate, Sharpe ratio, max drawdown.',
|
||||
|
||||
// ===== CONTRIBUTING =====
|
||||
faqHowToContribute: 'How can I contribute to NOFX?',
|
||||
faqHowToContributeAnswer:
|
||||
'NOFX is open-source and welcomes contributions! Ways to contribute: 1) Code - fix bugs, add features (check GitHub Issues); 2) Documentation - improve guides, translate; 3) Bug Reports - report issues with details; 4) Feature Ideas - suggest improvements. Start with issues labeled "good first issue". All contributors may receive airdrop rewards.',
|
||||
|
||||
faqPRGuidelines: 'What are the PR guidelines?',
|
||||
faqPRGuidelinesAnswer:
|
||||
'PR Process: 1) Fork repo to your account; 2) Create feature branch from dev: "git checkout -b feat/your-feature"; 3) Make changes, run lint: "npm --prefix web run lint"; 4) Commit with Conventional Commits format; 5) Push and create PR to NoFxAiOS/nofx:dev; 6) Reference related issue (Closes #123); 7) Wait for review. Keep PRs small and focused.',
|
||||
|
||||
faqBountyProgram: 'Is there a bounty program?',
|
||||
faqBountyProgramAnswer:
|
||||
'Yes! Contributors receive airdrop rewards based on contributions: Code commits (highest weight), bug fixes, feature suggestions, documentation. Issues with "bounty" label have cash rewards. After completing work, submit a Bounty Claim. Check CONTRIBUTING.md for details on the reward structure.',
|
||||
|
||||
faqReportBugs: 'How do I report bugs?',
|
||||
faqReportBugsAnswer:
|
||||
'For bugs: Open a GitHub Issue with: 1) Clear description of the problem; 2) Steps to reproduce; 3) Expected vs actual behavior; 4) System info (OS, Docker version, browser); 5) Relevant logs. For SECURITY vulnerabilities: Do NOT open public issues - DM @Web3Tinkle on Twitter instead.',
|
||||
|
||||
// Web Crypto Environment Check
|
||||
environmentCheck: {
|
||||
@@ -2009,226 +1849,66 @@ export const translations = {
|
||||
configureSignalSourceNow: '立即配置信号源',
|
||||
|
||||
// FAQ Page
|
||||
faqTitle: '常见问题',
|
||||
faqSubtitle: '查找关于 NOFX 的常见问题解答',
|
||||
faqStillHaveQuestions: '还有其他问题?',
|
||||
faqContactUs: '加入我们的社区或查看 GitHub 获取更多帮助',
|
||||
|
||||
// FAQ Categories
|
||||
faqCategoryGettingStarted: '入门指南',
|
||||
faqCategoryInstallation: '安装部署',
|
||||
faqCategoryConfiguration: '配置设置',
|
||||
faqCategoryTrading: '交易相关',
|
||||
faqCategoryTechnicalIssues: '技术问题',
|
||||
faqCategorySecurity: '安全相关',
|
||||
faqCategoryFeatures: '功能介绍',
|
||||
faqCategoryAIModels: 'AI 模型',
|
||||
faqCategoryContributing: '参与贡献',
|
||||
|
||||
// ===== 入门指南 =====
|
||||
faqWhatIsNOFX: 'NOFX 是什么?',
|
||||
faqWhatIsNOFXAnswer:
|
||||
'NOFX 是一个开源的 AI 驱动交易操作系统,支持加密货币和美股市场。它使用大语言模型(LLM)如 DeepSeek、GPT、Claude、Gemini 来分析市场数据,进行自主交易决策。核心功能包括:多 AI 模型支持、多交易所交易、可视化策略构建器、回测系统。',
|
||||
|
||||
faqHowDoesItWork: 'NOFX 是如何工作的?',
|
||||
faqHowDoesItWorkAnswer:
|
||||
'NOFX 分 5 步工作:1)配置 AI 模型和交易所 API 凭证;2)创建交易策略(币种选择、指标、风控);3)创建"交易员",组合 AI 模型 + 交易所 + 策略;4)启动交易员 - 它会定期分析市场数据并做出买入/卖出/持有决策;5)在仪表板上监控表现。AI 使用思维链(Chain of Thought)推理来解释每个决策。',
|
||||
|
||||
faqIsProfitable: 'NOFX 能盈利吗?',
|
||||
faqIsProfitableAnswer:
|
||||
'AI 交易是实验性的,不保证盈利。加密货币期货波动性大、风险高。NOFX 仅用于教育和研究目的。我们强烈建议:从小额开始(10-50 USDT),不要投入超过承受能力的资金,在实盘交易前充分回测,并理解过去的表现不代表未来的结果。',
|
||||
|
||||
faqSupportedExchanges: '支持哪些交易所?',
|
||||
faqSupportedExchangesAnswer:
|
||||
'CEX(中心化):币安合约、Bybit、OKX、Bitget。DEX(去中心化):Hyperliquid、Aster DEX、Lighter。每个交易所有不同特点 - 币安流动性最好,Hyperliquid 完全链上无需 KYC。查看文档获取各交易所的设置指南。',
|
||||
|
||||
faqSupportedAIModels: '支持哪些 AI 模型?',
|
||||
faqSupportedAIModelsAnswer:
|
||||
'NOFX 支持 7+ 种 AI 模型:DeepSeek(推荐性价比)、阿里云通义千问、OpenAI(GPT-5.2)、Anthropic Claude、Google Gemini、xAI Grok、Kimi(月之暗面)。您也可以使用任何 OpenAI 兼容的 API 端点。每个模型各有优势 - DeepSeek 性价比高,OpenAI 能力强但贵,Claude 擅长推理。',
|
||||
|
||||
faqSystemRequirements: '系统要求是什么?',
|
||||
faqSystemRequirementsAnswer:
|
||||
'最低配置:2 核 CPU,2GB 内存,1GB 硬盘,稳定网络。推荐:4GB 内存用于运行多个交易员。支持系统:Linux、macOS 或 Windows(通过 Docker 或 WSL2)。Docker 是最简单的安装方式。手动安装需要 Go 1.21+、Node.js 18+ 和 TA-Lib 库。',
|
||||
|
||||
// ===== 安装部署 =====
|
||||
faqHowToInstall: '如何安装 NOFX?',
|
||||
faqHowToInstallAnswer:
|
||||
'最简单的方法(Linux/macOS):运行 "curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash" - 这会自动安装 Docker 容器。然后在浏览器中打开 http://127.0.0.1:3000。手动安装或开发请克隆仓库并按照 README 说明操作。',
|
||||
|
||||
faqWindowsInstallation: 'Windows 如何安装?',
|
||||
faqWindowsInstallationAnswer:
|
||||
'三种方式:1)Docker Desktop(推荐)- 安装 Docker Desktop,然后在 PowerShell 中运行 "docker compose -f docker-compose.prod.yml up -d";2)WSL2 - 安装 Windows 子系统 Linux,然后按 Linux 方式安装;3)WSL2 + Docker - 两全其美,在 WSL2 终端运行安装脚本。通过 http://127.0.0.1:3000 访问。',
|
||||
|
||||
faqDockerDeployment: 'Docker 部署一直失败',
|
||||
faqDockerDeploymentAnswer:
|
||||
'常见解决方案:1)检查 Docker 是否运行:"docker info";2)确保足够内存(最少 2GB);3)如果卡在 "go build",尝试:"docker compose down && docker compose build --no-cache && docker compose up -d";4)查看日志:"docker compose logs -f";5)拉取较慢时,在 daemon.json 配置 Docker 镜像。',
|
||||
|
||||
faqManualInstallation: '如何手动安装用于开发?',
|
||||
faqManualInstallationAnswer:
|
||||
'前置条件:Go 1.21+、Node.js 18+、TA-Lib。步骤:1)克隆仓库:"git clone https://github.com/NoFxAiOS/nofx.git";2)安装后端依赖:"go mod download";3)安装前端依赖:"cd web && npm install";4)构建后端:"go build -o nofx";5)运行后端:"./nofx";6)运行前端(新终端):"cd web && npm run dev"。访问 http://127.0.0.1:3000',
|
||||
|
||||
faqServerDeployment: '如何部署到远程服务器?',
|
||||
faqServerDeploymentAnswer:
|
||||
'在服务器上运行安装脚本 - 它会自动检测服务器 IP。通过 http://服务器IP:3000 访问。配置 HTTPS:1)使用 Cloudflare(免费)- 添加域名,创建 A 记录指向服务器 IP,SSL 设为"灵活";2)在 .env 中启用 TRANSPORT_ENCRYPTION=true 进行浏览器端加密;3)通过 https://你的域名.com 访问。',
|
||||
|
||||
faqUpdateNOFX: '如何更新 NOFX?',
|
||||
faqUpdateNOFXAnswer:
|
||||
'Docker 方式:运行 "docker compose pull && docker compose up -d" 拉取最新镜像并重启。手动安装:后端 "git pull && go build -o nofx",前端 "cd web && npm install && npm run build"。data.db 中的配置在更新时会保留。',
|
||||
|
||||
// ===== 配置设置 =====
|
||||
faqConfigureAIModels: '如何配置 AI 模型?',
|
||||
faqConfigureAIModelsAnswer:
|
||||
'进入配置页面 → AI 模型部分。对于每个模型:1)从提供商获取 API 密钥(界面提供链接);2)输入 API 密钥;3)可选自定义基础 URL 和模型名称;4)保存。API 密钥在存储前会加密。保存后测试连接以验证。',
|
||||
|
||||
faqConfigureExchanges: '如何配置交易所连接?',
|
||||
faqConfigureExchangesAnswer:
|
||||
'进入配置页面 → 交易所部分。点击"添加交易所",选择类型并输入凭证。CEX(币安/Bybit/OKX):需要 API Key + Secret Key(OKX 还需要 Passphrase)。DEX(Hyperliquid/Aster/Lighter):需要钱包地址和私钥。务必只启用必要权限(合约交易)并考虑 IP 白名单。',
|
||||
|
||||
faqBinanceAPISetup: '如何正确设置币安 API?',
|
||||
faqBinanceAPISetupAnswer:
|
||||
'重要步骤:1)在币安 → API 管理中创建 API 密钥;2)仅启用"启用合约"权限;3)考虑添加 IP 白名单增强安全;4)关键:在合约设置 → 偏好设置 → 持仓模式中切换为双向持仓模式;5)确保资金在合约钱包(不是现货)。-4061 错误表示需要双向持仓模式。',
|
||||
|
||||
faqHyperliquidSetup: '如何设置 Hyperliquid?',
|
||||
faqHyperliquidSetupAnswer:
|
||||
'Hyperliquid 是去中心化交易所,需要钱包认证。步骤:1)访问 app.hyperliquid.xyz;2)连接钱包;3)生成 API 钱包(推荐)或使用主钱包;4)复制钱包地址和私钥;5)在 NOFX 中添加 Hyperliquid 交易所并填入凭证。无需 KYC,完全链上。',
|
||||
|
||||
faqCreateStrategy: '如何创建交易策略?',
|
||||
faqCreateStrategyAnswer:
|
||||
'进入策略工作室:1)币种来源 - 选择交易哪些币(静态列表、AI500 池或 OI 排行);2)指标 - 启用技术指标(EMA、MACD、RSI、ATR、成交量、OI、资金费率);3)风控 - 设置杠杆限制、最大持仓数、保证金使用上限、仓位大小限制;4)自定义提示词(可选)- 为 AI 添加特定指令。保存后分配给交易员。',
|
||||
|
||||
faqCreateTrader: '如何创建并启动交易员?',
|
||||
faqCreateTraderAnswer:
|
||||
'进入交易员页面:1)点击"创建交易员";2)选择 AI 模型(需先配置);3)选择交易所(需先配置);4)选择策略(或使用默认);5)设置决策间隔(如 15 分钟);6)保存,然后点击"启动"开始交易。在仪表板页面监控表现。',
|
||||
|
||||
// ===== 交易相关 =====
|
||||
faqHowAIDecides: 'AI 如何做出交易决策?',
|
||||
faqHowAIDecidesAnswer:
|
||||
'AI 使用思维链(CoT)推理分 4 步:1)持仓分析 - 审查当前持仓和盈亏;2)风险评估 - 检查账户保证金、可用余额;3)机会评估 - 分析市场数据、指标、候选币种;4)最终决策 - 输出具体操作(买入/卖出/持有)及理由。您可以在决策日志中查看完整推理过程。',
|
||||
|
||||
faqDecisionFrequency: 'AI 多久做一次决策?',
|
||||
faqDecisionFrequencyAnswer:
|
||||
'每个交易员可单独配置,默认 15 分钟。过于频繁会造成来回开平和手续费损耗;15-30 分钟能给信号足够时间兑现。执行层也会阻止过早的小亏小赚平仓和快速重新进场。',
|
||||
|
||||
faqNoTradesExecuting: '为什么交易员不执行任何交易?',
|
||||
faqNoTradesExecutingAnswer:
|
||||
'常见原因:1)AI 决定等待(查看决策日志了解原因);2)合约账户余额不足;3)达到最大持仓数限制(默认:3);4)交易所 API 问题(检查错误信息);5)策略约束太严格。查看仪表板 → 决策日志了解每个周期的 AI 推理详情。',
|
||||
|
||||
faqOnlyShortPositions: '为什么 AI 只开空单?',
|
||||
faqOnlyShortPositionsAnswer:
|
||||
'通常是因为币安持仓模式问题。解决方案:在币安合约 → 偏好设置 → 持仓模式中切换为双向持仓。必须先平掉所有持仓。切换后,AI 可以独立开多单和空单。',
|
||||
|
||||
faqLeverageSettings: '杠杆设置如何工作?',
|
||||
faqLeverageSettingsAnswer:
|
||||
'杠杆在策略 → 风控中设置:BTC/ETH 杠杆(通常 5-20 倍)和山寨币杠杆(通常 3-10 倍)。更高杠杆 = 更高风险和潜在收益。子账户可能有限制(如币安子账户限制 5 倍)。AI 下单时会遵守这些限制。',
|
||||
|
||||
faqStopLossTakeProfit: 'NOFX 支持止损止盈吗?',
|
||||
faqStopLossTakeProfitAnswer:
|
||||
'AI 可以在决策中建议止损/止盈价位,但这是基于指导而非交易所硬编码订单。AI 每个周期监控持仓,可能根据盈亏决定平仓。如需保证止损,可以手动在交易所设置订单,或调整策略提示词使其更保守。',
|
||||
|
||||
faqMultipleTraders: '可以运行多个交易员吗?',
|
||||
faqMultipleTradersAnswer:
|
||||
'可以!NOFX 支持运行 20+ 个并发交易员。每个交易员可以有不同的:AI 模型、交易所账户、策略、决策间隔。用于 A/B 测试策略、比较 AI 模型或跨交易所分散风险。在竞赛页面监控所有交易员。',
|
||||
|
||||
faqAICosts: 'AI API 调用费用是多少?',
|
||||
faqAICostsAnswer:
|
||||
'每个交易员每天大约费用(15 分钟间隔):DeepSeek:$0.03-0.20;Qwen:$0.07-0.30;OpenAI:$0.70-2;Claude:$0.30-1。费用取决于提示词长度和响应 token 数。DeepSeek 性价比最高。更长的决策间隔可降低费用。',
|
||||
|
||||
// ===== 技术问题 =====
|
||||
faqPortInUse: '端口 8080 或 3000 被占用',
|
||||
faqPortInUseAnswer:
|
||||
'查看占用端口的进程:macOS/Linux 用 "lsof -i :8080",Windows 用 "netstat -ano | findstr 8080"。终止进程或在 .env 中修改端口:NOFX_BACKEND_PORT=8081、NOFX_FRONTEND_PORT=3001。然后 "docker compose down && docker compose up -d" 重启。',
|
||||
|
||||
faqFrontendNotLoading: '前端一直显示"加载中..."',
|
||||
faqFrontendNotLoadingAnswer:
|
||||
'后端可能未运行或无法访问。检查:1)"curl http://127.0.0.1:8080/api/health" 应返回 {"status":"ok"};2)"docker compose ps" 验证容器运行中;3)查看后端日志:"docker compose logs nofx-backend";4)确保防火墙允许 8080 端口。',
|
||||
|
||||
faqDatabaseLocked: '数据库锁定错误',
|
||||
faqDatabaseLockedAnswer:
|
||||
'多个进程同时访问 SQLite 导致。解决方案:1)停止所有进程:"docker compose down" 或 "pkill nofx";2)如有锁文件删除:"rm -f data/data.db-wal data/data.db-shm";3)重启:"docker compose up -d"。只能有一个后端实例访问数据库。',
|
||||
|
||||
faqTALibNotFound: '构建时找不到 TA-Lib',
|
||||
faqTALibNotFoundAnswer:
|
||||
'TA-Lib 是技术指标所需。安装:macOS:"brew install ta-lib";Ubuntu/Debian:"sudo apt-get install libta-lib0-dev";CentOS:"yum install ta-lib-devel"。安装后重新构建:"go build -o nofx"。Docker 镜像已预装 TA-Lib。',
|
||||
|
||||
faqAIAPITimeout: 'AI API 超时或连接被拒绝',
|
||||
faqAIAPITimeoutAnswer:
|
||||
'检查:1)API 密钥有效(用 curl 测试);2)网络能访问 API 端点(ping/curl);3)API 提供商未宕机(查看状态页);4)VPN/防火墙未阻止;5)未超过速率限制。默认超时 120 秒。',
|
||||
|
||||
faqBinancePositionMode: '币安错误代码 -4061(持仓模式)',
|
||||
faqBinancePositionModeAnswer:
|
||||
'错误:"Order\'s position side does not match user\'s setting"。您处于单向持仓模式,但 NOFX 需要双向持仓模式。修复:1)先平掉所有持仓;2)币安合约 → 设置(齿轮图标)→ 偏好设置 → 持仓模式 → 切换为"双向持仓";3)重启交易员。',
|
||||
|
||||
faqBalanceShowsZero: '账户余额显示 0',
|
||||
faqBalanceShowsZeroAnswer:
|
||||
'资金可能在现货钱包而非合约钱包。解决方案:1)在币安进入钱包 → 合约 → 划转;2)将 USDT 从现货划转到合约;3)刷新 NOFX 仪表板。也检查:资金未被理财/质押产品锁定。',
|
||||
|
||||
faqDockerPullFailed: 'Docker 镜像拉取失败或缓慢',
|
||||
faqDockerPullFailedAnswer:
|
||||
'Docker Hub 在某些地区可能较慢。解决方案:1)在 /etc/docker/daemon.json 配置 Docker 镜像:{"registry-mirrors": ["https://mirror.gcr.io"]};2)重启 Docker;3)重试拉取。或使用 GitHub Container Registry(ghcr.io)在您的地区可能连接更好。',
|
||||
|
||||
// ===== 安全相关 =====
|
||||
faqAPIKeyStorage: 'API 密钥如何存储?',
|
||||
faqAPIKeyStorageAnswer:
|
||||
'API 密钥使用 AES-256-GCM 加密后存储在本地 SQLite 数据库中。加密密钥(DATA_ENCRYPTION_KEY)存储在您的 .env 文件中。密钥仅在 API 调用需要时在内存中解密。切勿分享您的 data.db 或 .env 文件。',
|
||||
|
||||
faqEncryptionDetails: 'NOFX 使用什么加密?',
|
||||
faqEncryptionDetailsAnswer:
|
||||
'NOFX 使用多层加密:1)AES-256-GCM 用于数据库存储(API 密钥、密钥);2)RSA-2048 用于可选的传输加密(浏览器到服务器);3)JWT 用于认证令牌。密钥在安装时生成。HTTPS 环境启用 TRANSPORT_ENCRYPTION=true。',
|
||||
|
||||
faqSecurityBestPractices: '安全最佳实践是什么?',
|
||||
faqSecurityBestPracticesAnswer:
|
||||
'建议:1)使用带 IP 白名单和最小权限(仅合约交易)的交易所 API 密钥;2)为 NOFX 使用专用子账户;3)远程部署启用 TRANSPORT_ENCRYPTION;4)切勿分享 .env 或 data.db 文件;5)使用有效证书的 HTTPS;6)定期轮换 API 密钥;7)监控账户活动。',
|
||||
|
||||
faqCanNOFXStealFunds: 'NOFX 会盗取我的资金吗?',
|
||||
faqCanNOFXStealFundsAnswer:
|
||||
'NOFX 是开源的(AGPL-3.0 许可)- 您可以在 GitHub 审计所有代码。API 密钥存储在您的机器本地,从不发送到外部服务器。NOFX 只有您通过 API 密钥授予的权限。为最大安全:使用仅交易权限(无提现)的 API 密钥,启用 IP 白名单,使用专用子账户。',
|
||||
|
||||
// ===== 功能介绍 =====
|
||||
faqStrategyStudio: '什么是策略工作室?',
|
||||
faqStrategyStudioAnswer:
|
||||
'策略工作室是可视化策略构建器,您可以配置:1)币种来源 - 交易哪些加密货币(静态列表、AI500 热门币、OI 排行);2)技术指标 - EMA、MACD、RSI、ATR、成交量、持仓量、资金费率;3)风控 - 杠杆限制、仓位大小、保证金上限;4)自定义提示词 - AI 的特定指令。无需编程。',
|
||||
|
||||
faqCompetitionMode: '什么是竞赛模式?',
|
||||
faqCompetitionModeAnswer:
|
||||
'竞赛页面显示所有交易员的实时排行榜。比较:ROI、盈亏、夏普比率、胜率、交易次数。用于 A/B 测试不同 AI 模型、策略或配置。交易员可标记为"在竞赛中显示"以出现在排行榜上。',
|
||||
|
||||
faqChainOfThought: '什么是思维链(CoT)?',
|
||||
faqChainOfThoughtAnswer:
|
||||
'思维链是 AI 的推理过程,可在决策日志中查看。AI 分 4 步解释思考:1)当前持仓分析;2)账户风险评估;3)市场机会评估;4)最终决策理由。这种透明度帮助您理解 AI 为什么做出每个决策,有助于改进策略。',
|
||||
|
||||
// ===== AI 模型 =====
|
||||
faqWhichAIModelBest: '应该使用哪个 AI 模型?',
|
||||
faqWhichAIModelBestAnswer:
|
||||
'推荐:DeepSeek 性价比最高(每天 $0.10-0.50)。备选:OpenAI 推理能力最强但贵(每天 $2-5);Claude 适合细致分析;Qwen 价格有竞争力。您可以运行多个交易员使用不同模型进行比较。查看竞赛页面看哪个对您的策略表现最好。',
|
||||
|
||||
faqCustomAIAPI: '可以使用自定义 AI API 吗?',
|
||||
faqCustomAIAPIAnswer:
|
||||
'可以!NOFX 支持任何 OpenAI 兼容的 API。在配置 → AI 模型 → 自定义 API 中:1)输入 API 端点 URL(如 https://your-api.com/v1);2)输入 API 密钥;3)指定模型名称。适用于自托管模型、替代提供商或通过第三方代理的 Claude。',
|
||||
|
||||
faqAIHallucinations: 'AI 幻觉问题怎么办?',
|
||||
faqAIHallucinationsAnswer:
|
||||
'AI 模型有时会产生不正确或虚构的信息("幻觉")。NOFX 通过以下方式缓解:1)提供带真实市场数据的结构化提示词;2)强制 JSON 输出格式;3)执行前验证订单。但 AI 交易是实验性的 - 始终监控决策,不要完全依赖 AI 判断。',
|
||||
|
||||
faqCompareAIModels: '如何比较不同 AI 模型?',
|
||||
faqCompareAIModelsAnswer:
|
||||
'创建多个交易员,使用不同 AI 模型但相同策略/交易所。同时运行并在竞赛页面比较。关注指标:ROI、胜率、夏普比率、最大回撤。',
|
||||
|
||||
// ===== 参与贡献 =====
|
||||
faqHowToContribute: '如何为 NOFX 做贡献?',
|
||||
faqHowToContributeAnswer:
|
||||
'NOFX 是开源项目,欢迎贡献!贡献方式:1)代码 - 修复 bug、添加功能(查看 GitHub Issues);2)文档 - 改进指南、翻译;3)Bug 报告 - 详细报告问题;4)功能建议 - 提出改进意见。从标记为"good first issue"的问题开始。所有贡献者可能获得空投奖励。',
|
||||
|
||||
faqPRGuidelines: 'PR 指南是什么?',
|
||||
faqPRGuidelinesAnswer:
|
||||
'PR 流程:1)Fork 仓库到您的账户;2)从 dev 创建功能分支:"git checkout -b feat/your-feature";3)修改代码,运行 lint:"npm --prefix web run lint";4)使用 Conventional Commits 格式提交;5)推送并创建 PR 到 NoFxAiOS/nofx:dev;6)关联相关 issue(Closes #123);7)等待审核。保持 PR 小而聚焦。',
|
||||
|
||||
faqBountyProgram: '有赏金计划吗?',
|
||||
faqBountyProgramAnswer:
|
||||
'有!贡献者根据贡献获得空投奖励:代码提交(权重最高)、bug 修复、功能建议、文档。带"bounty"标签的 issue 有现金奖励。完成工作后提交 Bounty Claim。查看 CONTRIBUTING.md 了解奖励结构详情。',
|
||||
|
||||
faqReportBugs: '如何报告 bug?',
|
||||
faqReportBugsAnswer:
|
||||
'Bug 报告:在 GitHub 开 Issue,包含:1)问题清晰描述;2)复现步骤;3)预期 vs 实际行为;4)系统信息(OS、Docker 版本、浏览器);5)相关日志。安全漏洞:不要开公开 issue - 请在 Twitter 私信 @Web3Tinkle。',
|
||||
|
||||
// Web Crypto Environment Check
|
||||
environmentCheck: {
|
||||
@@ -3345,168 +3025,6 @@ export const translations = {
|
||||
configureSignalSourceNow: 'Konfigurasi Sumber Sinyal Sekarang',
|
||||
|
||||
// FAQ Page
|
||||
faqTitle: 'Pertanyaan yang Sering Diajukan',
|
||||
faqSubtitle: 'Temukan jawaban untuk pertanyaan umum tentang NOFX',
|
||||
faqStillHaveQuestions: 'Masih Punya Pertanyaan?',
|
||||
faqContactUs:
|
||||
'Bergabunglah dengan komunitas kami atau kunjungi GitHub untuk bantuan lebih lanjut',
|
||||
faqCategoryGettingStarted: 'Memulai',
|
||||
faqCategoryInstallation: 'Instalasi',
|
||||
faqCategoryConfiguration: 'Konfigurasi',
|
||||
faqCategoryTrading: 'Trading',
|
||||
faqCategoryTechnicalIssues: 'Masalah Teknis',
|
||||
faqCategorySecurity: 'Keamanan',
|
||||
faqCategoryFeatures: 'Fitur',
|
||||
faqCategoryAIModels: 'Model AI',
|
||||
faqCategoryContributing: 'Kontribusi',
|
||||
faqWhatIsNOFX: 'Apa itu NOFX?',
|
||||
faqWhatIsNOFXAnswer:
|
||||
'NOFX adalah sistem operasi trading bertenaga AI open-source untuk pasar kripto dan saham AS. Ia menggunakan model bahasa besar (LLM) seperti DeepSeek, GPT, Claude, Gemini untuk menganalisis data pasar dan membuat keputusan trading secara otonom. Fitur utama: dukungan multi-model AI, trading multi-bursa, dan pembangun strategi visual.',
|
||||
faqHowDoesItWork: 'Bagaimana cara kerja NOFX?',
|
||||
faqHowDoesItWorkAnswer:
|
||||
'NOFX bekerja dalam 5 langkah: 1) Konfigurasi model AI dan kredensial API bursa; 2) Buat strategi trading (pemilihan koin, indikator, kontrol risiko); 3) Buat "Trader" menggabungkan Model AI + Bursa + Strategi; 4) Mulai trader - dia akan menganalisis data pasar secara berkala dan membuat keputusan beli/jual/tahan; 5) Pantau performa di dasbor.',
|
||||
faqIsProfitable: 'Apakah NOFX menguntungkan?',
|
||||
faqIsProfitableAnswer:
|
||||
'Trading AI bersifat eksperimental dan TIDAK dijamin menguntungkan. Futures kripto sangat volatil dan berisiko. NOFX dirancang untuk tujuan edukasi dan riset. Kami sangat menyarankan: mulai dengan jumlah kecil (10-50 USDT), jangan investasi melebihi yang sanggup Anda rugi, uji sebelum trading nyata.',
|
||||
faqSupportedExchanges: 'Bursa mana yang didukung?',
|
||||
faqSupportedExchangesAnswer:
|
||||
'CEX (Tersentralisasi): Binance Futures, Bybit, OKX, Bitget. DEX (Terdesentralisasi): Hyperliquid, Aster DEX, Lighter. Setiap bursa memiliki fitur berbeda - Binance memiliki likuiditas terbesar, Hyperliquid sepenuhnya on-chain tanpa KYC.',
|
||||
faqSupportedAIModels: 'Model AI mana yang didukung?',
|
||||
faqSupportedAIModelsAnswer:
|
||||
'NOFX mendukung 7+ model AI: DeepSeek (direkomendasikan untuk biaya/performa), Qwen, OpenAI (GPT), Claude, Gemini, Grok, dan Kimi. Anda juga dapat menggunakan endpoint API yang kompatibel dengan OpenAI.',
|
||||
faqSystemRequirements: 'Apa persyaratan sistem?',
|
||||
faqSystemRequirementsAnswer:
|
||||
'Minimum: 2 core CPU, 2GB RAM, 1GB disk, internet stabil. Direkomendasikan: 4GB RAM untuk menjalankan beberapa trader. OS yang didukung: Linux, macOS, atau Windows (via Docker atau WSL2).',
|
||||
faqHowToInstall: 'Bagaimana cara menginstal NOFX?',
|
||||
faqHowToInstallAnswer:
|
||||
'Metode termudah (Linux/macOS): Jalankan "curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash" - ini menginstal kontainer Docker secara otomatis. Lalu buka http://127.0.0.1:3000 di browser Anda.',
|
||||
faqWindowsInstallation: 'Bagaimana cara menginstal di Windows?',
|
||||
faqWindowsInstallationAnswer:
|
||||
'Tiga opsi: 1) Docker Desktop (Direkomendasikan); 2) WSL2 - Instal Windows Subsystem for Linux; 3) Docker di WSL2. Akses via http://127.0.0.1:3000',
|
||||
faqDockerDeployment: 'Deployment Docker terus gagal',
|
||||
faqDockerDeploymentAnswer:
|
||||
'Solusi umum: 1) Periksa Docker berjalan: "docker info"; 2) Pastikan memori cukup (minimal 2GB); 3) Periksa log: "docker compose logs -f".',
|
||||
faqManualInstallation:
|
||||
'Bagaimana cara menginstal manual untuk pengembangan?',
|
||||
faqManualInstallationAnswer:
|
||||
'Prasyarat: Go 1.21+, Node.js 18+, TA-Lib. Langkah: 1) Clone repo; 2) "go mod download"; 3) "cd web && npm install"; 4) "go build -o nofx"; 5) "./nofx"; 6) "cd web && npm run dev".',
|
||||
faqServerDeployment: 'Bagaimana cara deploy ke server remote?',
|
||||
faqServerDeploymentAnswer:
|
||||
'Jalankan skrip instal di server Anda. Akses via http://IP_SERVER:3000. Untuk HTTPS: Gunakan Cloudflare (gratis) dan aktifkan TRANSPORT_ENCRYPTION=true di .env.',
|
||||
faqUpdateNOFX: 'Bagaimana cara memperbarui NOFX?',
|
||||
faqUpdateNOFXAnswer:
|
||||
'Docker: "docker compose pull && docker compose up -d". Manual: "git pull && go build -o nofx" untuk backend, "cd web && npm install && npm run build" untuk frontend.',
|
||||
faqConfigureAIModels: 'Bagaimana cara mengonfigurasi model AI?',
|
||||
faqConfigureAIModelsAnswer:
|
||||
'Buka halaman Konfigurasi → bagian Model AI. Untuk setiap model: 1) Dapatkan API key dari penyedia; 2) Masukkan API key; 3) Opsional kustomisasi base URL dan nama model; 4) Simpan.',
|
||||
faqConfigureExchanges: 'Bagaimana cara mengonfigurasi koneksi bursa?',
|
||||
faqConfigureExchangesAnswer:
|
||||
'Buka halaman Konfigurasi → bagian Bursa. Klik "Tambah Bursa", pilih jenis, dan masukkan kredensial. Aktifkan hanya izin yang diperlukan (Trading Futures).',
|
||||
faqBinanceAPISetup: 'Bagaimana cara mengatur API Binance dengan benar?',
|
||||
faqBinanceAPISetupAnswer:
|
||||
'Langkah penting: 1) Buat API key di Binance → Manajemen API; 2) Aktifkan HANYA izin "Enable Futures"; 3) PENTING: Beralih ke Hedge Mode di pengaturan Futures; 4) Pastikan dana di dompet Futures.',
|
||||
faqHyperliquidSetup: 'Bagaimana cara mengatur Hyperliquid?',
|
||||
faqHyperliquidSetupAnswer:
|
||||
'Hyperliquid adalah bursa terdesentralisasi. Langkah: 1) Kunjungi app.hyperliquid.xyz; 2) Hubungkan wallet; 3) Buat API wallet; 4) Salin alamat dan private key; 5) Tambahkan di NOFX. Tanpa KYC.',
|
||||
faqCreateStrategy: 'Bagaimana cara membuat strategi trading?',
|
||||
faqCreateStrategyAnswer:
|
||||
'Buka Strategy Studio: 1) Sumber Koin; 2) Indikator teknikal; 3) Kontrol Risiko; 4) Prompt Kustom (opsional). Simpan dan tetapkan ke trader.',
|
||||
faqCreateTrader: 'Bagaimana cara membuat dan memulai trader?',
|
||||
faqCreateTraderAnswer:
|
||||
'Buka halaman Trader: 1) Klik "Buat Trader"; 2) Pilih Model AI; 3) Pilih Bursa; 4) Pilih Strategi; 5) Atur interval keputusan; 6) Simpan, lalu klik "Mulai".',
|
||||
faqHowAIDecides: 'Bagaimana AI membuat keputusan trading?',
|
||||
faqHowAIDecidesAnswer:
|
||||
'AI menggunakan penalaran Chain of Thought (CoT) dalam 4 langkah: 1) Analisis Posisi; 2) Penilaian Risiko; 3) Evaluasi Peluang; 4) Keputusan Akhir.',
|
||||
faqDecisionFrequency: 'Seberapa sering AI membuat keputusan?',
|
||||
faqDecisionFrequencyAnswer:
|
||||
'Dapat dikonfigurasi per trader, default 15 menit. Disarankan: 15-30 menit untuk trading aktif berbasis sinyal.',
|
||||
faqNoTradesExecuting: 'Mengapa trader saya tidak mengeksekusi trading?',
|
||||
faqNoTradesExecutingAnswer:
|
||||
'Penyebab umum: 1) AI memutuskan menunggu; 2) Saldo tidak cukup; 3) Batas posisi maks tercapai; 4) Masalah API bursa; 5) Batasan strategi terlalu ketat.',
|
||||
faqOnlyShortPositions: 'Mengapa AI hanya membuka posisi short?',
|
||||
faqOnlyShortPositionsAnswer:
|
||||
'Biasanya karena Mode Posisi Binance. Solusi: Beralih ke Hedge Mode di Binance Futures → Preferensi → Mode Posisi.',
|
||||
faqLeverageSettings: 'Bagaimana cara kerja pengaturan leverage?',
|
||||
faqLeverageSettingsAnswer:
|
||||
'Leverage diatur di Strategi → Kontrol Risiko: leverage BTC/ETH (biasanya 5-20x) dan leverage Altcoin (biasanya 3-10x).',
|
||||
faqStopLossTakeProfit: 'Apakah NOFX mendukung stop-loss dan take-profit?',
|
||||
faqStopLossTakeProfitAnswer:
|
||||
'AI dapat menyarankan level stop-loss/take-profit dalam keputusannya, tetapi ini bersifat panduan bukan order bursa yang dikodekan keras.',
|
||||
faqMultipleTraders: 'Bisakah saya menjalankan beberapa trader?',
|
||||
faqMultipleTradersAnswer:
|
||||
'Ya! NOFX mendukung 20+ trader bersamaan. Gunakan untuk A/B test strategi, bandingkan model AI, atau diversifikasi lintas bursa.',
|
||||
faqAICosts: 'Berapa biaya panggilan API AI?',
|
||||
faqAICostsAnswer:
|
||||
'Perkiraan biaya harian per trader (interval 5 menit): DeepSeek: $0.10-0.50; Qwen: $0.20-0.80; OpenAI: $2-5; Claude: $1-3.',
|
||||
faqPortInUse: 'Port 8080 atau 3000 sudah digunakan',
|
||||
faqPortInUseAnswer:
|
||||
'Periksa proses yang menggunakan port. Ubah port di .env: NOFX_BACKEND_PORT=8081, NOFX_FRONTEND_PORT=3001.',
|
||||
faqFrontendNotLoading: 'Frontend menampilkan "Memuat..." terus-menerus',
|
||||
faqFrontendNotLoadingAnswer:
|
||||
'Backend mungkin tidak berjalan. Periksa: "curl http://127.0.0.1:8080/api/health" harus mengembalikan {"status":"ok"}.',
|
||||
faqDatabaseLocked: 'Error database terkunci',
|
||||
faqDatabaseLockedAnswer:
|
||||
'Beberapa proses mengakses SQLite bersamaan. Hentikan semua, hapus file lock, restart.',
|
||||
faqTALibNotFound: 'TA-Lib tidak ditemukan saat build',
|
||||
faqTALibNotFoundAnswer:
|
||||
'Instal TA-Lib: macOS: "brew install ta-lib"; Ubuntu: "sudo apt-get install libta-lib0-dev".',
|
||||
faqAIAPITimeout: 'API AI timeout atau koneksi ditolak',
|
||||
faqAIAPITimeoutAnswer:
|
||||
'Periksa: 1) API key valid; 2) Jaringan bisa mengakses endpoint; 3) Penyedia tidak down; 4) VPN/firewall tidak memblokir.',
|
||||
faqBinancePositionMode: 'Kode error Binance -4061 (Mode Posisi)',
|
||||
faqBinancePositionModeAnswer:
|
||||
'Anda dalam mode One-way tetapi NOFX memerlukan Hedge Mode. Tutup semua posisi, beralih ke Hedge Mode, restart trader.',
|
||||
faqBalanceShowsZero: 'Saldo akun menunjukkan 0',
|
||||
faqBalanceShowsZeroAnswer:
|
||||
'Dana mungkin di dompet Spot, bukan dompet Futures. Transfer USDT dari Spot ke Futures.',
|
||||
faqDockerPullFailed: 'Penarikan image Docker gagal atau lambat',
|
||||
faqDockerPullFailedAnswer:
|
||||
'Konfigurasi mirror Docker di daemon.json atau gunakan GitHub Container Registry.',
|
||||
faqAPIKeyStorage: 'Bagaimana API key disimpan?',
|
||||
faqAPIKeyStorageAnswer:
|
||||
'API key dienkripsi menggunakan AES-256-GCM sebelum disimpan di database SQLite lokal. Jangan pernah bagikan file data.db atau .env Anda.',
|
||||
faqEncryptionDetails: 'Enkripsi apa yang digunakan NOFX?',
|
||||
faqEncryptionDetailsAnswer:
|
||||
'NOFX menggunakan: 1) AES-256-GCM untuk penyimpanan database; 2) RSA-2048 untuk enkripsi transport opsional; 3) JWT untuk token autentikasi.',
|
||||
faqSecurityBestPractices: 'Apa praktik terbaik keamanan?',
|
||||
faqSecurityBestPracticesAnswer:
|
||||
'Disarankan: 1) Gunakan API key dengan whitelist IP dan izin minimal; 2) Gunakan sub-akun khusus; 3) Aktifkan TRANSPORT_ENCRYPTION; 4) Gunakan HTTPS.',
|
||||
faqCanNOFXStealFunds: 'Bisakah NOFX mencuri dana saya?',
|
||||
faqCanNOFXStealFundsAnswer:
|
||||
'NOFX open-source (lisensi AGPL-3.0) - Anda bisa audit semua kode. API key disimpan lokal di mesin ANDA, tidak pernah dikirim ke server eksternal.',
|
||||
faqStrategyStudio: 'Apa itu Strategy Studio?',
|
||||
faqStrategyStudioAnswer:
|
||||
'Strategy Studio adalah pembangun strategi visual untuk konfigurasi: Sumber Koin, Indikator Teknikal, Kontrol Risiko, dan Prompt Kustom. Tanpa coding.',
|
||||
faqCompetitionMode: 'Apa itu Mode Kompetisi?',
|
||||
faqCompetitionModeAnswer:
|
||||
'Halaman kompetisi menampilkan papan peringkat realtime semua trader Anda. Bandingkan ROI, L/R, rasio Sharpe, win rate.',
|
||||
faqChainOfThought: 'Apa itu Chain of Thought (CoT)?',
|
||||
faqChainOfThoughtAnswer:
|
||||
'Chain of Thought adalah proses penalaran AI, terlihat di log keputusan. AI menjelaskan alasan di balik setiap keputusan.',
|
||||
faqWhichAIModelBest: 'Model AI mana yang sebaiknya saya gunakan?',
|
||||
faqWhichAIModelBestAnswer:
|
||||
'Direkomendasikan: DeepSeek untuk rasio biaya/performa terbaik. Alternatif: OpenAI untuk penalaran terbaik; Claude untuk analisis mendalam; Qwen harga kompetitif.',
|
||||
faqCustomAIAPI: 'Bisakah saya menggunakan API AI kustom?',
|
||||
faqCustomAIAPIAnswer:
|
||||
'Ya! NOFX mendukung API yang kompatibel dengan OpenAI. Masukkan URL endpoint, API key, dan nama model.',
|
||||
faqAIHallucinations: 'Bagaimana dengan halusinasi AI?',
|
||||
faqAIHallucinationsAnswer:
|
||||
'NOFX memitigasi dengan: prompt terstruktur, format output JSON, dan validasi order sebelum eksekusi. Namun trading AI tetap eksperimental.',
|
||||
faqCompareAIModels: 'Bagaimana cara membandingkan model AI yang berbeda?',
|
||||
faqCompareAIModelsAnswer:
|
||||
'Buat beberapa trader dengan model AI berbeda tapi strategi/bursa sama. Jalankan bersamaan dan bandingkan di halaman Kompetisi.',
|
||||
faqHowToContribute: 'Bagaimana cara berkontribusi ke NOFX?',
|
||||
faqHowToContributeAnswer:
|
||||
'NOFX open-source dan menyambut kontribusi! Cara: 1) Kode - perbaiki bug, tambah fitur; 2) Dokumentasi; 3) Laporan Bug; 4) Ide Fitur. Semua kontributor mungkin mendapat reward airdrop.',
|
||||
faqPRGuidelines: 'Apa panduan PR?',
|
||||
faqPRGuidelinesAnswer:
|
||||
'Proses PR: 1) Fork repo; 2) Buat branch fitur dari dev; 3) Buat perubahan, jalankan lint; 4) Commit dengan format Conventional Commits; 5) Push dan buat PR ke NoFxAiOS/nofx:dev.',
|
||||
faqBountyProgram: 'Apakah ada program bounty?',
|
||||
faqBountyProgramAnswer:
|
||||
'Ya! Kontributor mendapat reward airdrop berdasarkan kontribusi. Issue dengan label "bounty" memiliki reward uang tunai.',
|
||||
faqReportBugs: 'Bagaimana cara melaporkan bug?',
|
||||
faqReportBugsAnswer:
|
||||
'Buka GitHub Issue dengan: deskripsi masalah, langkah reproduksi, perilaku yang diharapkan vs aktual. Untuk kerentanan keamanan: DM @Web3Tinkle di Twitter.',
|
||||
|
||||
// Web Crypto Environment Check
|
||||
environmentCheck: {
|
||||
|
||||
@@ -6,13 +6,20 @@ import type {
|
||||
import { API_BASE, httpClient } from './helpers'
|
||||
import { ApiError } from '../httpClient'
|
||||
|
||||
// Create/update/start legitimately run long: stopping a live trader waits for
|
||||
// its in-flight cycle and monitors, and creation probes the exchange (~35s
|
||||
// worst case observed). The default 30s axios timeout aborts mid-operation and
|
||||
// reports a false failure, so these calls get their own generous ceiling.
|
||||
const TRADER_LIFECYCLE_TIMEOUT_MS = 120_000
|
||||
|
||||
function throwApiError(
|
||||
message: string,
|
||||
errorKey?: string,
|
||||
errorParams?: Record<string, string>,
|
||||
statusCode?: number
|
||||
statusCode?: number,
|
||||
errorData?: Record<string, any>
|
||||
): never {
|
||||
throw new ApiError(message, errorKey, errorParams, statusCode)
|
||||
throw new ApiError(message, errorKey, errorParams, statusCode, errorData)
|
||||
}
|
||||
|
||||
export const traderApi = {
|
||||
@@ -32,10 +39,11 @@ export const traderApi = {
|
||||
},
|
||||
|
||||
async createTrader(request: CreateTraderRequest): Promise<TraderInfo> {
|
||||
const result = await httpClient.post<TraderInfo>(
|
||||
`${API_BASE}/traders`,
|
||||
request
|
||||
)
|
||||
const result = await httpClient.request<TraderInfo>(`${API_BASE}/traders`, {
|
||||
method: 'POST',
|
||||
data: request,
|
||||
timeout: TRADER_LIFECYCLE_TIMEOUT_MS,
|
||||
})
|
||||
if (!result.success) {
|
||||
throwApiError(
|
||||
result.message || 'Failed to create trader',
|
||||
@@ -53,15 +61,17 @@ export const traderApi = {
|
||||
},
|
||||
|
||||
async startTrader(traderId: string): Promise<void> {
|
||||
const result = await httpClient.post(
|
||||
`${API_BASE}/traders/${traderId}/start`
|
||||
const result = await httpClient.request(
|
||||
`${API_BASE}/traders/${traderId}/start`,
|
||||
{ method: 'POST', timeout: TRADER_LIFECYCLE_TIMEOUT_MS }
|
||||
)
|
||||
if (!result.success) {
|
||||
throwApiError(
|
||||
result.message || 'Failed to start trader',
|
||||
result.errorKey,
|
||||
result.errorParams,
|
||||
result.statusCode
|
||||
result.statusCode,
|
||||
result.errorData
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -115,9 +125,9 @@ export const traderApi = {
|
||||
traderId: string,
|
||||
request: CreateTraderRequest
|
||||
): Promise<TraderInfo> {
|
||||
const result = await httpClient.put<TraderInfo>(
|
||||
const result = await httpClient.request<TraderInfo>(
|
||||
`${API_BASE}/traders/${traderId}`,
|
||||
request
|
||||
{ method: 'PUT', data: request, timeout: TRADER_LIFECYCLE_TIMEOUT_MS }
|
||||
)
|
||||
if (!result.success) {
|
||||
throwApiError(
|
||||
|
||||
47
web/src/lib/demo/demoUniverse.ts
Normal file
47
web/src/lib/demo/demoUniverse.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Shared constants for the dashboard demo/showcase mode. US-equity-led synthetic
|
||||
* universe (these are real xyz-dex markets so the cost/liq heatmap still resolves)
|
||||
* plus plausible seed prices for the synthetic order book / candle feeds.
|
||||
*
|
||||
* This drives the "Demo" presentation mode only. It never touches the backend,
|
||||
* the live trader, or any real account — it is purely a client-side animation
|
||||
* layer for product walkthroughs.
|
||||
*/
|
||||
|
||||
export const DEMO_UNIVERSE = [
|
||||
// longs (bullish book)
|
||||
'SP500', 'NVDA', 'MU', 'GOOGL', 'TSM', 'META', 'AMD', 'AAPL', 'MSFT', 'AMZN',
|
||||
'NFLX', 'TSLA', 'AVGO', 'NBIS', 'XYZ100', 'PLTR', 'TXN', 'LRCX', 'AMAT', 'MRVL',
|
||||
// short book (bearish names — see SHORT_SET in the engine)
|
||||
'INTC', 'SPCX', 'SKHX', 'SMCI', 'ARM', 'QCOM', 'COIN', 'ORCL', 'DRAM', 'CRM',
|
||||
'HOOD', 'SNOW',
|
||||
]
|
||||
|
||||
// Lead instrument for the price panels (real heatmap + resolvable book/candles).
|
||||
export const DEMO_ACTIVE_SYMBOL = 'SP500'
|
||||
|
||||
const DEMO_SEED_PX: Record<string, number> = {
|
||||
SP500: 6485, NVDA: 184.2, MU: 142.6, GOOGL: 351.8, TSM: 451.3, META: 723.5,
|
||||
AMD: 168.4, AAPL: 245.9, MSFT: 498.2, AMZN: 228.4, NFLX: 921.5, TSLA: 412.8,
|
||||
AVGO: 358.1, NBIS: 266.1, XYZ100: 1182, PLTR: 78.4, TXN: 205.3, LRCX: 102.6,
|
||||
AMAT: 218.7, MRVL: 118.2, INTC: 41.2, SPCX: 64.3, SKHX: 88.7, SMCI: 44.1,
|
||||
ARM: 162.4, QCOM: 178.9, COIN: 312.5, ORCL: 192.3, DRAM: 72.4, CRM: 342.1,
|
||||
HOOD: 58.6, SNOW: 198.4,
|
||||
}
|
||||
|
||||
/** Stable plausible seed price for a base symbol (deterministic fallback). */
|
||||
export function demoSeedPrice(base: string): number {
|
||||
const b = base.toUpperCase().replace(/^XYZ:/, '')
|
||||
if (DEMO_SEED_PX[b]) return DEMO_SEED_PX[b]
|
||||
let h = 0
|
||||
for (let i = 0; i < b.length; i++) h = (h * 31 + b.charCodeAt(i)) % 100000
|
||||
return 40 + (h % 900)
|
||||
}
|
||||
|
||||
/** Reasonable tick size for a price level given its magnitude. */
|
||||
export function demoTick(px: number): number {
|
||||
if (px >= 1000) return 1
|
||||
if (px >= 100) return 0.1
|
||||
if (px >= 10) return 0.05
|
||||
return 0.01
|
||||
}
|
||||
457
web/src/lib/demo/useDemoEngine.ts
Normal file
457
web/src/lib/demo/useDemoEngine.ts
Normal file
@@ -0,0 +1,457 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type {
|
||||
AccountInfo,
|
||||
Position,
|
||||
DecisionRecord,
|
||||
SystemStatus,
|
||||
TraderFullStats,
|
||||
PositionHistoryResponse,
|
||||
HistoricalPosition,
|
||||
SymbolStats,
|
||||
} from '../../types'
|
||||
import type {
|
||||
FlowMarketsResponse,
|
||||
FlowMarketItem,
|
||||
SignalRankingResponse,
|
||||
SignalRankItem,
|
||||
} from '../api/data'
|
||||
import { DEMO_UNIVERSE, DEMO_ACTIVE_SYMBOL, demoSeedPrice } from './demoUniverse'
|
||||
|
||||
/**
|
||||
* useDemoEngine — a client-side showcase data generator. When `active`, it
|
||||
* synthesises a fast-evolving, profitable-looking US-equity trading dataset that
|
||||
* mirrors the real dashboard data shapes, so every panel animates for a product
|
||||
* walkthrough. Returns null when inactive (the dashboard then uses real data).
|
||||
*
|
||||
* No network, no backend, no real account — pure presentation layer.
|
||||
*/
|
||||
|
||||
const TICK_MS = 200
|
||||
const INITIAL = 1_000_000
|
||||
|
||||
export interface DemoDataset {
|
||||
status: SystemStatus
|
||||
account: AccountInfo
|
||||
positions: Position[]
|
||||
decisions: DecisionRecord[]
|
||||
fullStats: TraderFullStats
|
||||
history: PositionHistoryResponse
|
||||
config: {
|
||||
scan_interval_minutes: number
|
||||
ai_model: string
|
||||
strategy_name: string
|
||||
btc_eth_leverage: number
|
||||
altcoin_leverage: number
|
||||
}
|
||||
flow: FlowMarketsResponse
|
||||
signalRank: SignalRankingResponse
|
||||
activeSymbol: string
|
||||
}
|
||||
|
||||
interface PosState {
|
||||
symbol: string
|
||||
side: 'long' | 'short'
|
||||
entry: number
|
||||
mark: number
|
||||
qty: number
|
||||
lev: number
|
||||
}
|
||||
interface TradeState {
|
||||
id: number
|
||||
symbol: string
|
||||
side: 'long' | 'short'
|
||||
entry: number
|
||||
exit: number
|
||||
qty: number
|
||||
pnl: number
|
||||
fee: number
|
||||
lev: number
|
||||
}
|
||||
|
||||
interface SimState {
|
||||
frame: number
|
||||
cycle: number
|
||||
realized: number
|
||||
fee: number
|
||||
wins: number
|
||||
losses: number
|
||||
grossWin: number
|
||||
grossLoss: number
|
||||
nextId: number
|
||||
decisionTs: number
|
||||
positions: PosState[]
|
||||
trades: TradeState[]
|
||||
// per-symbol flow noise so the bars jiggle independently
|
||||
flowNet: Record<string, number>
|
||||
signalScore: Record<string, number>
|
||||
}
|
||||
|
||||
const rnd = (a: number, b: number) => a + Math.random() * (b - a)
|
||||
const pick = <T,>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]
|
||||
|
||||
// A fixed "short book" — these symbols are short EVERYWHERE (flow outflow,
|
||||
// bearish signal, short positions, decision candidates) so the topology's
|
||||
// short row carries connected flow lines through every layer. The rest are long.
|
||||
const SHORT_SET = new Set([
|
||||
'INTC', 'SPCX', 'SKHX', 'SMCI', 'ARM', 'QCOM', 'COIN', 'ORCL', 'DRAM', 'CRM', 'HOOD', 'SNOW',
|
||||
])
|
||||
const LONG_POOL = DEMO_UNIVERSE.filter((s) => !SHORT_SET.has(s))
|
||||
|
||||
function sideFor(symbol: string): 'long' | 'short' {
|
||||
return SHORT_SET.has(symbol.toUpperCase()) ? 'short' : 'long'
|
||||
}
|
||||
|
||||
function newPosition(symbol: string): PosState {
|
||||
const entry = demoSeedPrice(symbol) * rnd(0.985, 1.015)
|
||||
const side = sideFor(symbol)
|
||||
const lev = pick([10, 10, 15, 20, 20])
|
||||
const notional = rnd(35_000, 110_000)
|
||||
return { symbol, side, entry, mark: entry, qty: notional / entry, lev }
|
||||
}
|
||||
|
||||
function initState(): SimState {
|
||||
// index 0 = lead (drives the price panels). Then a deep US-equity book — a
|
||||
// long bias plus a sizable short book so the topology fills both rows densely.
|
||||
const longs = [DEMO_ACTIVE_SYMBOL, 'NVDA', 'GOOGL', 'TSM', 'META', 'AMD', 'MSFT', 'AMZN', 'AVGO']
|
||||
const shorts = ['INTC', 'SPCX', 'SKHX', 'SMCI', 'ARM', 'QCOM']
|
||||
const positions = [...longs, ...shorts].map((s) => {
|
||||
const p = newPosition(s)
|
||||
// start each slightly in profit so the board opens green
|
||||
const fav = rnd(0.006, 0.03)
|
||||
p.mark = p.side === 'long' ? p.entry * (1 + fav) : p.entry * (1 - fav)
|
||||
return p
|
||||
})
|
||||
const flowNet: Record<string, number> = {}
|
||||
const signalScore: Record<string, number> = {}
|
||||
DEMO_UNIVERSE.forEach((s, i) => {
|
||||
flowNet[s] = rnd(120_000, 2_400_000) * (1 - i / (DEMO_UNIVERSE.length + 4))
|
||||
// short book scores negative (bearish), everything else positive (bullish)
|
||||
signalScore[s] = SHORT_SET.has(s) ? rnd(-1.6, -0.4) : rnd(0.4, 2.0)
|
||||
})
|
||||
return {
|
||||
frame: 0,
|
||||
cycle: 1,
|
||||
realized: 312_000,
|
||||
fee: 2840,
|
||||
wins: 412,
|
||||
losses: 214,
|
||||
grossWin: 690_000,
|
||||
grossLoss: 286_000,
|
||||
nextId: 5000,
|
||||
decisionTs: Date.now(),
|
||||
positions,
|
||||
trades: [],
|
||||
flowNet,
|
||||
signalScore,
|
||||
}
|
||||
}
|
||||
|
||||
function upnl(p: PosState): number {
|
||||
const notional = p.qty * p.entry
|
||||
const move = (p.mark - p.entry) / p.entry
|
||||
return notional * move * (p.side === 'long' ? 1 : -1)
|
||||
}
|
||||
|
||||
function step(S: SimState) {
|
||||
S.frame++
|
||||
|
||||
// drift each position's mark with a favourable bias so the book trends green
|
||||
for (const p of S.positions) {
|
||||
const bias = p.side === 'long' ? 0.00028 : -0.00028
|
||||
p.mark *= 1 + bias + rnd(-0.0011, 0.0011)
|
||||
// keep within a believable favourable band of entry
|
||||
const move = (p.mark - p.entry) / p.entry
|
||||
const dir = p.side === 'long' ? 1 : -1
|
||||
const signed = move * dir
|
||||
if (signed < -0.014) p.mark = p.entry * (1 - dir * 0.014)
|
||||
if (signed > 0.075) p.mark = p.entry * (1 + dir * 0.075)
|
||||
}
|
||||
|
||||
// roll a winning trade every ~16 frames. Only rotate a non-lead LONG slot:
|
||||
// index 0 (lead, drives price panels) and the short book stay fixed so the
|
||||
// order book stays on one symbol and the topology keeps its short flow lines.
|
||||
const rotatable = S.positions
|
||||
.map((p, i) => ({ p, i }))
|
||||
.filter((x) => x.i !== 0 && x.p.side === 'long')
|
||||
if (S.frame % 16 === 0 && rotatable.length) {
|
||||
let bestIdx = rotatable[0].i
|
||||
for (const x of rotatable) {
|
||||
if (upnl(x.p) > upnl(S.positions[bestIdx])) bestIdx = x.i
|
||||
}
|
||||
const isWin = Math.random() < 0.7
|
||||
const closed = S.positions[bestIdx]
|
||||
const raw = upnl(closed)
|
||||
const pnl = isWin ? Math.max(Math.abs(raw), rnd(600, 5200)) : -rnd(250, 2100)
|
||||
const fee = rnd(12, 95)
|
||||
S.trades.unshift({
|
||||
id: S.nextId++,
|
||||
symbol: closed.symbol,
|
||||
side: closed.side,
|
||||
entry: closed.entry,
|
||||
exit: closed.mark,
|
||||
qty: closed.qty,
|
||||
pnl,
|
||||
fee,
|
||||
lev: closed.lev,
|
||||
})
|
||||
if (S.trades.length > 40) S.trades.pop()
|
||||
S.realized += pnl - fee
|
||||
S.fee += fee
|
||||
if (pnl >= 0) {
|
||||
S.wins++
|
||||
S.grossWin += pnl
|
||||
} else {
|
||||
S.losses++
|
||||
S.grossLoss += -pnl
|
||||
}
|
||||
// reopen a fresh LONG US-equity position (avoid duplicates of current book)
|
||||
const held = new Set(S.positions.map((p) => p.symbol))
|
||||
const candidates = LONG_POOL.filter((s) => !held.has(s))
|
||||
S.positions[bestIdx] = newPosition(candidates.length ? pick(candidates) : closed.symbol)
|
||||
}
|
||||
|
||||
// new orchestration cycle every ~24 frames
|
||||
if (S.frame % 24 === 0) {
|
||||
S.cycle++
|
||||
S.decisionTs = Date.now()
|
||||
}
|
||||
|
||||
// jiggle flow + signal noise so those panels stay alive (short book stays
|
||||
// bearish, longs stay bullish — keeps signal/topology directions consistent)
|
||||
for (const s of DEMO_UNIVERSE) {
|
||||
S.flowNet[s] = Math.max(20_000, S.flowNet[s] * rnd(0.97, 1.035))
|
||||
const next = S.signalScore[s] + rnd(-0.08, 0.09)
|
||||
S.signalScore[s] = SHORT_SET.has(s)
|
||||
? Math.max(-2, Math.min(-0.1, next))
|
||||
: Math.max(0.1, Math.min(2.4, next))
|
||||
}
|
||||
}
|
||||
|
||||
function build(S: SimState): DemoDataset {
|
||||
const liveUpnl = S.positions.reduce((s, p) => s + upnl(p), 0)
|
||||
const equity = INITIAL + S.realized + liveUpnl
|
||||
const pnl = equity - INITIAL
|
||||
const marginUsed = S.positions.reduce((s, p) => s + (p.qty * p.entry) / p.lev, 0)
|
||||
const total = S.wins + S.losses
|
||||
|
||||
const account = {
|
||||
total_equity: equity,
|
||||
wallet_balance: equity - liveUpnl,
|
||||
unrealized_profit: liveUpnl,
|
||||
total_unrealized_profit: liveUpnl, // RiskRadar reads this extra field
|
||||
available_balance: Math.max(0, equity - marginUsed),
|
||||
total_pnl: pnl,
|
||||
total_pnl_pct: (pnl / INITIAL) * 100,
|
||||
initial_balance: INITIAL,
|
||||
daily_pnl: S.realized + liveUpnl,
|
||||
position_count: S.positions.length,
|
||||
margin_used: marginUsed,
|
||||
margin_used_pct: Math.min(82, (marginUsed / equity) * 100 * 1.6),
|
||||
} as unknown as AccountInfo
|
||||
|
||||
const positions: Position[] = S.positions.map((p) => {
|
||||
const u = upnl(p)
|
||||
const notional = p.qty * p.entry
|
||||
const margin = notional / p.lev
|
||||
const liq = p.side === 'long' ? p.entry * (1 - 0.9 / p.lev) : p.entry * (1 + 0.9 / p.lev)
|
||||
return {
|
||||
symbol: p.symbol,
|
||||
side: p.side,
|
||||
entry_price: p.entry,
|
||||
mark_price: p.mark,
|
||||
quantity: p.qty,
|
||||
leverage: p.lev,
|
||||
unrealized_pnl: u,
|
||||
unrealized_pnl_pct: (u / margin) * 100,
|
||||
liquidation_price: liq,
|
||||
margin_used: margin,
|
||||
}
|
||||
})
|
||||
|
||||
const winRate = total > 0 ? (S.wins / total) * 100 : 0
|
||||
const avgWin = S.wins > 0 ? S.grossWin / S.wins : 0
|
||||
const avgLoss = S.losses > 0 ? S.grossLoss / S.losses : 0
|
||||
const fullStats: TraderFullStats = {
|
||||
total_trades: total,
|
||||
win_trades: S.wins,
|
||||
loss_trades: S.losses,
|
||||
win_rate: winRate,
|
||||
profit_factor: S.grossLoss > 0 ? S.grossWin / S.grossLoss : S.grossWin,
|
||||
sharpe_ratio: 2.05 + Math.sin(S.frame / 90) * 0.12,
|
||||
total_pnl: pnl,
|
||||
total_fee: S.fee,
|
||||
avg_win: avgWin,
|
||||
avg_loss: avgLoss,
|
||||
// percent semantics (5.2 = -5.2%), matching the real stats endpoint
|
||||
max_drawdown_pct: 5.2,
|
||||
}
|
||||
|
||||
// recent closed trades
|
||||
const histPositions = S.trades.map(
|
||||
(t) =>
|
||||
({
|
||||
id: t.id,
|
||||
symbol: t.symbol,
|
||||
side: t.side,
|
||||
quantity: t.qty,
|
||||
entry_price: t.entry,
|
||||
exit_price: t.exit,
|
||||
exit_time: new Date(S.decisionTs - (S.nextId - t.id) * 47_000).toISOString(),
|
||||
realized_pnl: t.pnl,
|
||||
fee: t.fee,
|
||||
leverage: t.lev,
|
||||
status: 'closed',
|
||||
close_reason: t.pnl >= 0 ? 'take_profit' : 'stop_loss',
|
||||
}) as unknown as HistoricalPosition,
|
||||
)
|
||||
|
||||
// per-symbol aggregates
|
||||
const bySym = new Map<string, { n: number; w: number; pnl: number }>()
|
||||
for (const t of S.trades) {
|
||||
const e = bySym.get(t.symbol) || { n: 0, w: 0, pnl: 0 }
|
||||
e.n++
|
||||
if (t.pnl >= 0) e.w++
|
||||
e.pnl += t.pnl
|
||||
bySym.set(t.symbol, e)
|
||||
}
|
||||
const symbolStats: SymbolStats[] = [...bySym.entries()]
|
||||
.map(([symbol, e]) => ({
|
||||
symbol,
|
||||
total_trades: e.n,
|
||||
win_trades: e.w,
|
||||
win_rate: e.n > 0 ? (e.w / e.n) * 100 : 0,
|
||||
total_pnl: e.pnl,
|
||||
avg_pnl: e.n > 0 ? e.pnl / e.n : 0,
|
||||
avg_hold_mins: Math.round(rnd(6, 38)),
|
||||
}))
|
||||
.sort((a, b) => b.total_trades - a.total_trades)
|
||||
|
||||
const history = {
|
||||
positions: histPositions,
|
||||
stats: null,
|
||||
symbol_stats: symbolStats,
|
||||
direction_stats: [],
|
||||
} as unknown as PositionHistoryResponse
|
||||
|
||||
// flow markets — long names show net BUYING (inflow), the short book shows net
|
||||
// SELLING (outflow) so the topology's FLOW layer feeds the short row too.
|
||||
const mkItem = (s: string, net: number): FlowMarketItem => {
|
||||
const buyShare = net >= 0 ? 0.56 + Math.min(0.3, net / 6_000_000) : 0.4
|
||||
const gross = Math.abs(net)
|
||||
return {
|
||||
key: `xyz:${s}`,
|
||||
marketType: 'hip3_perp',
|
||||
symbol: s,
|
||||
netFlow: String(Math.round(net)),
|
||||
buyNotional: String(Math.round(gross * buyShare)),
|
||||
sellNotional: String(Math.round(gross * (1 - buyShare))),
|
||||
trades: Math.round(rnd(150, 9000)),
|
||||
latestPrice: String(demoSeedPrice(s)),
|
||||
}
|
||||
}
|
||||
const inflow: FlowMarketItem[] = LONG_POOL.slice()
|
||||
.sort((a, b) => S.flowNet[b] - S.flowNet[a])
|
||||
.map((s) => mkItem(s, S.flowNet[s]))
|
||||
const outflow: FlowMarketItem[] = [...SHORT_SET].map((s) => mkItem(s, -Math.abs(S.flowNet[s]) * 0.6))
|
||||
const flow: FlowMarketsResponse = { data: { by: 'netFlow', window: '1h', inflow, outflow } }
|
||||
|
||||
// signal ranking — mostly bullish US equities
|
||||
const ranked = [...DEMO_UNIVERSE].sort((a, b) => S.signalScore[b] - S.signalScore[a])
|
||||
const items: SignalRankItem[] = ranked.map((s, i) => {
|
||||
const score = S.signalScore[s]
|
||||
return {
|
||||
rank: i + 1,
|
||||
symbol: s,
|
||||
market_type: 'hip3_perp',
|
||||
bias: SHORT_SET.has(s) ? 'bearish' : 'bullish',
|
||||
score: Math.round(score * 100) / 100,
|
||||
category: 'us_equity',
|
||||
}
|
||||
})
|
||||
const signalRank: SignalRankingResponse = { items }
|
||||
|
||||
// decision candidates — top longs plus the short book, so the DECISION layer
|
||||
// (and execution log) carries shorts through to EXECUTE/HOLD.
|
||||
const candidates = [...new Set([...ranked.slice(0, 8), ...SHORT_SET])]
|
||||
const decisions: DecisionRecord[] = Array.from({ length: 4 }).map((_, k) => {
|
||||
const cyc = S.cycle - k
|
||||
const acts = S.positions.slice(0, 6).map((p) => ({
|
||||
action: 'hold',
|
||||
symbol: p.symbol,
|
||||
quantity: p.qty,
|
||||
leverage: p.lev,
|
||||
price: p.mark,
|
||||
confidence: Math.round(rnd(62, 88)),
|
||||
reasoning: 'Signal Lab confirms trend; cost/liq structure supports the level.',
|
||||
timestamp: new Date(S.decisionTs - k * 300_000).toISOString(),
|
||||
}))
|
||||
return {
|
||||
timestamp: new Date(S.decisionTs - k * 300_000).toISOString(),
|
||||
cycle_number: cyc,
|
||||
system_prompt: '',
|
||||
input_prompt: '',
|
||||
cot_trace:
|
||||
'US-equity tape is broadly bid: SP500 and semis (NVDA, MU, TSM) lead net inflow with bullish Signal Lab bias. Holding winners ≥ entry, trimming only on structure breaks.',
|
||||
decision_json: '',
|
||||
account_state: {} as never,
|
||||
positions: [],
|
||||
candidate_coins: candidates,
|
||||
decisions: acts as never,
|
||||
execution_log: S.positions
|
||||
.slice(0, 6)
|
||||
.map((p) => `${p.symbol} hold succeeded`),
|
||||
success: true,
|
||||
} as unknown as DecisionRecord
|
||||
})
|
||||
|
||||
const status = {
|
||||
is_running: true,
|
||||
call_count: S.cycle,
|
||||
scan_interval: '5m',
|
||||
ai_model: 'claw402',
|
||||
strategy_type: 'ai_trading',
|
||||
} as unknown as SystemStatus
|
||||
|
||||
return {
|
||||
status,
|
||||
account,
|
||||
positions,
|
||||
decisions,
|
||||
fullStats,
|
||||
history,
|
||||
config: {
|
||||
scan_interval_minutes: 5,
|
||||
ai_model: 'claw402',
|
||||
strategy_name: 'NOFX Claw402 Auto Strategy',
|
||||
btc_eth_leverage: 10,
|
||||
altcoin_leverage: 10,
|
||||
},
|
||||
flow,
|
||||
signalRank,
|
||||
activeSymbol: DEMO_ACTIVE_SYMBOL,
|
||||
}
|
||||
}
|
||||
|
||||
export function useDemoEngine(active: boolean): DemoDataset | null {
|
||||
const ref = useRef<SimState | null>(null)
|
||||
const [, tick] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
ref.current = null
|
||||
return
|
||||
}
|
||||
ref.current = initState()
|
||||
tick((n) => n + 1)
|
||||
const id = setInterval(() => {
|
||||
if (ref.current) {
|
||||
step(ref.current)
|
||||
tick((n) => n + 1)
|
||||
}
|
||||
}, TICK_MS)
|
||||
return () => clearInterval(id)
|
||||
}, [active])
|
||||
|
||||
if (!active || !ref.current) return null
|
||||
return build(ref.current)
|
||||
}
|
||||
@@ -22,24 +22,29 @@ export interface ApiResponse<T = any> {
|
||||
errorKey?: string
|
||||
errorParams?: Record<string, string>
|
||||
statusCode?: number
|
||||
/** Full error response body for endpoints that return structured details. */
|
||||
errorData?: Record<string, any>
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
errorKey?: string
|
||||
errorParams?: Record<string, string>
|
||||
statusCode?: number
|
||||
errorData?: Record<string, any>
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
errorKey?: string,
|
||||
errorParams?: Record<string, string>,
|
||||
statusCode?: number
|
||||
statusCode?: number,
|
||||
errorData?: Record<string, any>
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.errorKey = errorKey
|
||||
this.errorParams = errorParams
|
||||
this.statusCode = statusCode
|
||||
this.errorData = errorData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +259,8 @@ export class HttpClient {
|
||||
errorKey: errorData?.error_key,
|
||||
errorParams: errorData?.error_params,
|
||||
statusCode: error.response.status,
|
||||
errorData:
|
||||
errorData && typeof errorData === 'object' ? errorData : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
146
web/src/lib/launch/launch.test.ts
Normal file
146
web/src/lib/launch/launch.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
import {
|
||||
describeLaunchFailures,
|
||||
failedLaunchChecks,
|
||||
primarySetupTarget,
|
||||
setupTargetForCheck,
|
||||
} from './preflight'
|
||||
import { pickTradingExchange, pickTradingModel } from './resolve'
|
||||
import type { LaunchCheck, LaunchPreflightResult } from './types'
|
||||
import type { AIModel, Exchange } from '../../types'
|
||||
|
||||
vi.mock('../api', () => ({ api: {} }))
|
||||
vi.mock('../api/helpers', () => ({
|
||||
API_BASE: '',
|
||||
httpClient: { get: vi.fn(), post: vi.fn() },
|
||||
}))
|
||||
|
||||
function preflightResult(checks: LaunchCheck[]): LaunchPreflightResult {
|
||||
return {
|
||||
ready: checks.every((check) => check.status !== 'failed'),
|
||||
checks,
|
||||
min_ai_fee_usdc: 1,
|
||||
min_trading_usdc: 12,
|
||||
checked_at: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
describe('setupTargetForCheck', () => {
|
||||
it('routes AI wallet problems to claw402 setup', () => {
|
||||
expect(setupTargetForCheck({ id: 'ai_model', status: 'failed' })).toBe(
|
||||
'claw402'
|
||||
)
|
||||
expect(setupTargetForCheck({ id: 'ai_wallet', status: 'failed' })).toBe(
|
||||
'claw402'
|
||||
)
|
||||
expect(
|
||||
setupTargetForCheck({ id: 'ai_wallet_funds', status: 'failed' })
|
||||
).toBe('claw402')
|
||||
})
|
||||
|
||||
it('routes exchange config/account problems to hyperliquid setup', () => {
|
||||
expect(
|
||||
setupTargetForCheck({ id: 'exchange_config', status: 'failed' })
|
||||
).toBe('hyperliquid')
|
||||
expect(
|
||||
setupTargetForCheck({ id: 'exchange_account', status: 'failed' })
|
||||
).toBe('hyperliquid')
|
||||
})
|
||||
|
||||
it('routes funding shortfalls to the funds anchor', () => {
|
||||
expect(
|
||||
setupTargetForCheck({ id: 'exchange_funds', status: 'failed' })
|
||||
).toBe('hyperliquid-funds')
|
||||
})
|
||||
|
||||
it('has no anchor for strategy problems', () => {
|
||||
expect(setupTargetForCheck({ id: 'strategy', status: 'failed' })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('primarySetupTarget', () => {
|
||||
it('returns the anchor for the first failing check', () => {
|
||||
const result = preflightResult([
|
||||
{ id: 'ai_model', status: 'ok' },
|
||||
{ id: 'ai_wallet_funds', status: 'failed', message: 'AI wallet empty.' },
|
||||
{ id: 'exchange_funds', status: 'failed', message: 'Low margin.' },
|
||||
])
|
||||
expect(primarySetupTarget(result)).toBe('claw402')
|
||||
})
|
||||
|
||||
it('returns null when everything passes', () => {
|
||||
const result = preflightResult([
|
||||
{ id: 'ai_model', status: 'ok' },
|
||||
{ id: 'exchange_funds', status: 'warning' },
|
||||
])
|
||||
expect(primarySetupTarget(result)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('describeLaunchFailures / failedLaunchChecks', () => {
|
||||
it('collects only failed checks and joins their messages', () => {
|
||||
const result = preflightResult([
|
||||
{ id: 'ai_wallet_funds', status: 'failed', message: 'AI wallet empty.' },
|
||||
{ id: 'exchange_funds', status: 'warning', message: 'Testnet funds.' },
|
||||
{ id: 'exchange_account', status: 'failed', message: 'Bad key.' },
|
||||
{ id: 'strategy', status: 'skipped' },
|
||||
])
|
||||
expect(failedLaunchChecks(result)).toHaveLength(2)
|
||||
expect(describeLaunchFailures(result)).toBe('AI wallet empty. Bad key.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pickTradingModel', () => {
|
||||
const base: Partial<AIModel> = { enabled: true }
|
||||
|
||||
it('prefers claw402 over other enabled models', () => {
|
||||
const models = [
|
||||
{ ...base, id: 'openai', provider: 'openai', has_api_key: true },
|
||||
{ ...base, id: 'c402', provider: 'claw402', has_api_key: true },
|
||||
] as AIModel[]
|
||||
expect(pickTradingModel(models)?.id).toBe('c402')
|
||||
})
|
||||
|
||||
it('accepts a claw402 model with only a wallet address', () => {
|
||||
const models = [
|
||||
{ ...base, id: 'c402', provider: 'claw402', walletAddress: '0xabc' },
|
||||
] as AIModel[]
|
||||
expect(pickTradingModel(models)?.id).toBe('c402')
|
||||
})
|
||||
|
||||
it('returns null when nothing usable exists', () => {
|
||||
const models = [
|
||||
{ id: 'x', provider: 'openai', enabled: false, has_api_key: true },
|
||||
{ id: 'y', provider: 'deepseek', enabled: true },
|
||||
] as AIModel[]
|
||||
expect(pickTradingModel(models)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('pickTradingExchange', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('requires enabled + key + builder approval + wallet address', () => {
|
||||
const ready = {
|
||||
id: 'hl',
|
||||
exchange_type: 'hyperliquid',
|
||||
enabled: true,
|
||||
has_api_key: true,
|
||||
hyperliquidBuilderApproved: true,
|
||||
hyperliquidWalletAddr: '0x1',
|
||||
} as unknown as Exchange
|
||||
expect(pickTradingExchange([ready])?.id).toBe('hl')
|
||||
|
||||
const unapproved = {
|
||||
...ready,
|
||||
hyperliquidBuilderApproved: false,
|
||||
} as unknown as Exchange
|
||||
expect(pickTradingExchange([unapproved])).toBeNull()
|
||||
|
||||
const noAddr = {
|
||||
...ready,
|
||||
hyperliquidWalletAddr: ' ',
|
||||
} as unknown as Exchange
|
||||
expect(pickTradingExchange([noAddr])).toBeNull()
|
||||
})
|
||||
})
|
||||
199
web/src/lib/launch/launchAutopilot.test.ts
Normal file
199
web/src/lib/launch/launchAutopilot.test.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { launchAutopilot } from './launchAutopilot'
|
||||
import { ApiError } from '../httpClient'
|
||||
import type { LaunchPreflightResult } from './types'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
api: {
|
||||
getTraders: vi.fn(),
|
||||
createTrader: vi.fn(),
|
||||
updateTrader: vi.fn(),
|
||||
startTrader: vi.fn(),
|
||||
},
|
||||
runLaunchPreflight: vi.fn(),
|
||||
resolveLaunchModel: vi.fn(),
|
||||
resolveLaunchExchange: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../api', () => ({ api: mocks.api }))
|
||||
vi.mock('./preflight', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('./preflight')>()),
|
||||
runLaunchPreflight: mocks.runLaunchPreflight,
|
||||
}))
|
||||
vi.mock('./resolve', () => ({
|
||||
resolveLaunchModel: mocks.resolveLaunchModel,
|
||||
resolveLaunchExchange: mocks.resolveLaunchExchange,
|
||||
}))
|
||||
|
||||
function readyPreflight(): LaunchPreflightResult {
|
||||
return {
|
||||
ready: true,
|
||||
checks: [],
|
||||
min_ai_fee_usdc: 1,
|
||||
min_trading_usdc: 12,
|
||||
checked_at: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
function failedPreflight(): LaunchPreflightResult {
|
||||
return {
|
||||
ready: false,
|
||||
checks: [
|
||||
{
|
||||
id: 'ai_wallet_funds',
|
||||
status: 'failed',
|
||||
code: 'AI_WALLET_INSUFFICIENT_FUNDS',
|
||||
message: 'AI wallet needs 1 USDC.',
|
||||
},
|
||||
],
|
||||
min_ai_fee_usdc: 1,
|
||||
min_trading_usdc: 12,
|
||||
checked_at: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
describe('launchAutopilot', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.resolveLaunchModel.mockResolvedValue({ id: 'model-1' })
|
||||
mocks.resolveLaunchExchange.mockResolvedValue({
|
||||
exchange: { id: 'ex-1' },
|
||||
})
|
||||
mocks.api.getTraders.mockResolvedValue([])
|
||||
mocks.api.createTrader.mockResolvedValue({
|
||||
trader_id: 't-1',
|
||||
is_running: false,
|
||||
})
|
||||
mocks.api.startTrader.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
it('never touches the strategy when preflight fails', async () => {
|
||||
mocks.runLaunchPreflight.mockResolvedValue(failedPreflight())
|
||||
const ensureStrategy = vi.fn()
|
||||
|
||||
const outcome = await launchAutopilot({ ensureStrategy })
|
||||
|
||||
expect(ensureStrategy).not.toHaveBeenCalled()
|
||||
expect(mocks.api.createTrader).not.toHaveBeenCalled()
|
||||
expect(outcome.ok).toBe(false)
|
||||
if (outcome.ok || outcome.kind !== 'preflight') {
|
||||
throw new Error('expected a preflight failure outcome')
|
||||
}
|
||||
expect(outcome.setupTarget).toBe('claw402')
|
||||
expect(outcome.message).toContain('AI wallet needs 1 USDC.')
|
||||
})
|
||||
|
||||
it('creates and starts the trader after preflight passes', async () => {
|
||||
mocks.runLaunchPreflight.mockResolvedValue(readyPreflight())
|
||||
const ensureStrategy = vi.fn().mockResolvedValue('strat-1')
|
||||
|
||||
const outcome = await launchAutopilot({
|
||||
ensureStrategy,
|
||||
scanIntervalMinutes: 15,
|
||||
})
|
||||
|
||||
expect(ensureStrategy).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.api.createTrader).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
strategy_id: 'strat-1',
|
||||
scan_interval_minutes: 15,
|
||||
ai_model_id: 'model-1',
|
||||
exchange_id: 'ex-1',
|
||||
})
|
||||
)
|
||||
expect(mocks.api.startTrader).toHaveBeenCalledWith('t-1')
|
||||
expect(outcome).toEqual(
|
||||
expect.objectContaining({ ok: true, traderId: 't-1' })
|
||||
)
|
||||
})
|
||||
|
||||
it('updates the existing autopilot instead of creating a duplicate', async () => {
|
||||
mocks.runLaunchPreflight.mockResolvedValue(readyPreflight())
|
||||
mocks.api.getTraders.mockResolvedValue([
|
||||
{ trader_id: 't-old', trader_name: 'NOFX Autopilot', is_running: false },
|
||||
])
|
||||
mocks.api.updateTrader.mockResolvedValue({
|
||||
trader_id: 't-old',
|
||||
is_running: true,
|
||||
})
|
||||
|
||||
const outcome = await launchAutopilot({
|
||||
ensureStrategy: vi.fn().mockResolvedValue('strat-1'),
|
||||
})
|
||||
|
||||
expect(mocks.api.updateTrader).toHaveBeenCalled()
|
||||
expect(mocks.api.createTrader).not.toHaveBeenCalled()
|
||||
expect(mocks.api.startTrader).not.toHaveBeenCalled()
|
||||
expect(outcome).toEqual(
|
||||
expect.objectContaining({ ok: true, traderId: 't-old' })
|
||||
)
|
||||
})
|
||||
|
||||
it('treats a racing already-running rejection as success', async () => {
|
||||
mocks.runLaunchPreflight.mockResolvedValue(readyPreflight())
|
||||
mocks.api.getTraders.mockResolvedValue([
|
||||
{ trader_id: 't-1', trader_name: 'NOFX Autopilot', is_running: false },
|
||||
])
|
||||
mocks.api.updateTrader.mockResolvedValue({
|
||||
trader_id: 't-1',
|
||||
is_running: false,
|
||||
})
|
||||
mocks.api.startTrader.mockRejectedValue(
|
||||
new ApiError(
|
||||
'Trader is already running',
|
||||
'trader.start.already_running',
|
||||
undefined,
|
||||
400
|
||||
)
|
||||
)
|
||||
|
||||
const outcome = await launchAutopilot({
|
||||
ensureStrategy: vi.fn().mockResolvedValue('strat-1'),
|
||||
})
|
||||
|
||||
expect(outcome).toEqual(
|
||||
expect.objectContaining({ ok: true, traderId: 't-1' })
|
||||
)
|
||||
})
|
||||
|
||||
it('surfaces the server-side preflight result when start is rejected', async () => {
|
||||
mocks.runLaunchPreflight.mockResolvedValue(readyPreflight())
|
||||
mocks.api.startTrader.mockRejectedValue(
|
||||
new ApiError(
|
||||
'preflight failed',
|
||||
'trader.start.preflight_failed',
|
||||
undefined,
|
||||
400,
|
||||
{ preflight: failedPreflight() }
|
||||
)
|
||||
)
|
||||
|
||||
const outcome = await launchAutopilot({
|
||||
ensureStrategy: vi.fn().mockResolvedValue('strat-1'),
|
||||
})
|
||||
|
||||
expect(outcome.ok).toBe(false)
|
||||
if (outcome.ok || outcome.kind !== 'preflight') {
|
||||
throw new Error('expected a preflight failure outcome')
|
||||
}
|
||||
expect(outcome.setupTarget).toBe('claw402')
|
||||
})
|
||||
|
||||
it('routes missing exchange setup to the hyperliquid anchor', async () => {
|
||||
mocks.resolveLaunchExchange.mockResolvedValue({
|
||||
exchange: null,
|
||||
reason: 'No Hyperliquid account is connected.',
|
||||
})
|
||||
|
||||
const outcome = await launchAutopilot({ ensureStrategy: vi.fn() })
|
||||
|
||||
expect(outcome).toEqual(
|
||||
expect.objectContaining({
|
||||
ok: false,
|
||||
kind: 'setup',
|
||||
setupTarget: 'hyperliquid',
|
||||
})
|
||||
)
|
||||
expect(mocks.runLaunchPreflight).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
187
web/src/lib/launch/launchAutopilot.ts
Normal file
187
web/src/lib/launch/launchAutopilot.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { api } from '../api'
|
||||
import { ApiError } from '../httpClient'
|
||||
import {
|
||||
describeLaunchFailures,
|
||||
primarySetupTarget,
|
||||
runLaunchPreflight,
|
||||
} from './preflight'
|
||||
import { resolveLaunchExchange, resolveLaunchModel } from './resolve'
|
||||
import type { LaunchOutcome } from './types'
|
||||
|
||||
export const AUTOPILOT_TRADER_NAME = 'NOFX Autopilot'
|
||||
|
||||
export interface LaunchAutopilotOptions {
|
||||
/**
|
||||
* Provides the strategy id to trade. Called only AFTER preflight passes so
|
||||
* a failed launch never mutates or activates a strategy as a side effect.
|
||||
*/
|
||||
ensureStrategy: () => Promise<string>
|
||||
scanIntervalMinutes?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* The single Autopilot launch path shared by Strategy Studio and the guided
|
||||
* launch panel. Order matters: resolve → preflight (server-side, fresh
|
||||
* balances) → strategy → create/update trader → start. No side effects happen
|
||||
* before preflight passes.
|
||||
*/
|
||||
export async function launchAutopilot(
|
||||
options: LaunchAutopilotOptions
|
||||
): Promise<LaunchOutcome> {
|
||||
const { ensureStrategy, scanIntervalMinutes = 5 } = options
|
||||
|
||||
try {
|
||||
const model = await resolveLaunchModel()
|
||||
if (!model) {
|
||||
return {
|
||||
ok: false,
|
||||
kind: 'setup',
|
||||
message:
|
||||
'No enabled AI model is ready. Create or fund the Claw402 wallet first.',
|
||||
setupTarget: 'claw402',
|
||||
}
|
||||
}
|
||||
|
||||
const exchangeResult = await resolveLaunchExchange()
|
||||
if (!exchangeResult.exchange) {
|
||||
return {
|
||||
ok: false,
|
||||
kind: 'setup',
|
||||
message: exchangeResult.reason,
|
||||
setupTarget: 'hyperliquid',
|
||||
}
|
||||
}
|
||||
const exchange = exchangeResult.exchange
|
||||
|
||||
const preflight = await runLaunchPreflight({
|
||||
ai_model_id: model.id,
|
||||
exchange_id: exchange.id,
|
||||
})
|
||||
if (!preflight.ready) {
|
||||
return {
|
||||
ok: false,
|
||||
kind: 'preflight',
|
||||
message:
|
||||
describeLaunchFailures(preflight) ||
|
||||
'Launch prerequisites are not ready yet.',
|
||||
preflight,
|
||||
setupTarget: primarySetupTarget(preflight),
|
||||
}
|
||||
}
|
||||
|
||||
const strategyId = await ensureStrategy()
|
||||
|
||||
const traderRequest = {
|
||||
name: AUTOPILOT_TRADER_NAME,
|
||||
ai_model_id: model.id,
|
||||
exchange_id: exchange.id,
|
||||
strategy_id: strategyId,
|
||||
scan_interval_minutes: scanIntervalMinutes,
|
||||
is_cross_margin: true,
|
||||
show_in_competition: true,
|
||||
btc_eth_leverage: 10,
|
||||
altcoin_leverage: 10,
|
||||
}
|
||||
|
||||
// Re-fetch the live trader list before deciding create vs update. Stale
|
||||
// props/snapshots would create a duplicate "NOFX Autopilot" — paying the
|
||||
// slow first-create cost again and orphaning dashboards onto a deleted id.
|
||||
const existingTraders = await api.getTraders(true)
|
||||
const existing =
|
||||
existingTraders.find(
|
||||
(trader) => trader.trader_name === AUTOPILOT_TRADER_NAME
|
||||
) ||
|
||||
existingTraders.find((trader) =>
|
||||
(trader.strategy_name || '').toLowerCase().includes('claw402')
|
||||
) ||
|
||||
null
|
||||
|
||||
const autopilot = existing
|
||||
? await api.updateTrader(existing.trader_id, traderRequest)
|
||||
: await api.createTrader(traderRequest)
|
||||
|
||||
if (!autopilot.is_running) {
|
||||
try {
|
||||
await api.startTrader(autopilot.trader_id)
|
||||
} catch (err) {
|
||||
// Launch is idempotent: the update path restarts a running trader
|
||||
// asynchronously, so a racing "already running" rejection is success.
|
||||
const alreadyRunning =
|
||||
err instanceof ApiError &&
|
||||
err.errorKey === 'trader.start.already_running'
|
||||
if (!alreadyRunning) throw err
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
traderId: autopilot.trader_id,
|
||||
warning: autopilot.startup_warning,
|
||||
}
|
||||
} catch (err) {
|
||||
// The server re-runs preflight on start; surface its structured result if
|
||||
// readiness changed between our check and the start call.
|
||||
if (err instanceof ApiError && err.errorKey === 'trader.start.preflight_failed') {
|
||||
const preflight = err.errorData?.preflight
|
||||
if (preflight) {
|
||||
return {
|
||||
ok: false,
|
||||
kind: 'preflight',
|
||||
message: describeLaunchFailures(preflight) || err.message,
|
||||
preflight,
|
||||
setupTarget: primarySetupTarget(preflight),
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
kind: 'error',
|
||||
message:
|
||||
err instanceof Error ? err.message : 'Failed to launch NOFX Autopilot',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default strategy provisioning for the guided panel: reuse the active
|
||||
* Claw402 strategy, otherwise create and activate it.
|
||||
*/
|
||||
export async function ensureClaw402Strategy(): Promise<string> {
|
||||
const strategies = await api.getStrategies()
|
||||
const existing =
|
||||
strategies.find(
|
||||
(strategy) =>
|
||||
strategy.is_active &&
|
||||
strategy.config?.ai_config?.coin_source?.source_type === 'vergex_signal'
|
||||
) ||
|
||||
strategies.find((strategy) =>
|
||||
strategy.name.toLowerCase().includes('claw402')
|
||||
)
|
||||
|
||||
if (existing) {
|
||||
if (!existing.is_active) {
|
||||
await api.activateStrategy(existing.id)
|
||||
}
|
||||
return existing.id
|
||||
}
|
||||
|
||||
const config = await api.getDefaultStrategyConfig()
|
||||
const created = await api.createStrategy({
|
||||
name: 'NOFX Claw402 Auto Strategy',
|
||||
description:
|
||||
'Single built-in strategy: Claw402 board, per-symbol details, raw candles, then execution.',
|
||||
config,
|
||||
})
|
||||
if (created?.id) {
|
||||
await api.activateStrategy(created.id)
|
||||
return created.id
|
||||
}
|
||||
|
||||
const refreshed = await api.getStrategies()
|
||||
const fallback = refreshed.find((strategy) =>
|
||||
strategy.name.toLowerCase().includes('claw402')
|
||||
)
|
||||
if (!fallback) throw new Error('Failed to create Claw402 strategy')
|
||||
await api.activateStrategy(fallback.id)
|
||||
return fallback.id
|
||||
}
|
||||
90
web/src/lib/launch/preflight.ts
Normal file
90
web/src/lib/launch/preflight.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { API_BASE, httpClient } from '../api/helpers'
|
||||
import type {
|
||||
LaunchCheck,
|
||||
LaunchPreflightResult,
|
||||
SetupTarget,
|
||||
} from './types'
|
||||
|
||||
export interface LaunchPreflightRequest {
|
||||
ai_model_id: string
|
||||
exchange_id: string
|
||||
strategy_id?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side launch readiness checks. This is the single source of truth for
|
||||
* balances and minimums — never gate the UI on cached client-side values.
|
||||
*/
|
||||
export async function runLaunchPreflight(
|
||||
request: LaunchPreflightRequest
|
||||
): Promise<LaunchPreflightResult> {
|
||||
const result = await httpClient.post<LaunchPreflightResult>(
|
||||
`${API_BASE}/launch/preflight`,
|
||||
request
|
||||
)
|
||||
if (!result.success || !result.data) {
|
||||
throw new Error(result.message || 'Failed to run launch preflight')
|
||||
}
|
||||
return result.data
|
||||
}
|
||||
|
||||
export async function getTraderPreflight(
|
||||
traderId: string
|
||||
): Promise<LaunchPreflightResult> {
|
||||
const result = await httpClient.get<LaunchPreflightResult>(
|
||||
`${API_BASE}/traders/${traderId}/preflight`
|
||||
)
|
||||
if (!result.success || !result.data) {
|
||||
throw new Error(result.message || 'Failed to run launch preflight')
|
||||
}
|
||||
return result.data
|
||||
}
|
||||
|
||||
export function failedLaunchChecks(
|
||||
result: LaunchPreflightResult
|
||||
): LaunchCheck[] {
|
||||
return result.checks.filter((check) => check.status === 'failed')
|
||||
}
|
||||
|
||||
export function launchWarnings(result: LaunchPreflightResult): LaunchCheck[] {
|
||||
return result.checks.filter((check) => check.status === 'warning')
|
||||
}
|
||||
|
||||
/** Human-readable one-liner combining every failing check. */
|
||||
export function describeLaunchFailures(result: LaunchPreflightResult): string {
|
||||
return failedLaunchChecks(result)
|
||||
.map((check) => check.message)
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a failing check to the guided-setup anchor that fixes it. The traders
|
||||
* page opens the matching modal/section from the `?setup=` query param.
|
||||
*/
|
||||
export function setupTargetForCheck(check: LaunchCheck): SetupTarget | null {
|
||||
switch (check.id) {
|
||||
case 'ai_model':
|
||||
case 'ai_wallet':
|
||||
case 'ai_wallet_funds':
|
||||
return 'claw402'
|
||||
case 'exchange_config':
|
||||
case 'exchange_account':
|
||||
return 'hyperliquid'
|
||||
case 'exchange_funds':
|
||||
return 'hyperliquid-funds'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** The setup anchor for the first (highest-priority) failing check. */
|
||||
export function primarySetupTarget(
|
||||
result: LaunchPreflightResult
|
||||
): SetupTarget | null {
|
||||
for (const check of failedLaunchChecks(result)) {
|
||||
const target = setupTargetForCheck(check)
|
||||
if (target) return target
|
||||
}
|
||||
return null
|
||||
}
|
||||
136
web/src/lib/launch/resolve.ts
Normal file
136
web/src/lib/launch/resolve.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { api } from '../api'
|
||||
import type { AIModel, Exchange } from '../../types'
|
||||
|
||||
export function modelHasCredential(model: AIModel) {
|
||||
return Boolean(
|
||||
model.has_api_key ||
|
||||
model.apiKey ||
|
||||
(model.provider === 'claw402' && model.walletAddress)
|
||||
)
|
||||
}
|
||||
|
||||
export function exchangeHasKey(exchange: Exchange) {
|
||||
return Boolean(exchange.has_api_key || exchange.apiKey)
|
||||
}
|
||||
|
||||
export function isHyperliquidExchange(exchange: Exchange) {
|
||||
return exchange.exchange_type === 'hyperliquid'
|
||||
}
|
||||
|
||||
/** Prefers the claw402 model, falls back to any enabled model with a credential. */
|
||||
export function pickTradingModel(models: AIModel[]) {
|
||||
return (
|
||||
models.find(
|
||||
(model) =>
|
||||
model.provider === 'claw402' &&
|
||||
model.enabled &&
|
||||
modelHasCredential(model)
|
||||
) ||
|
||||
models.find((model) => model.enabled && modelHasCredential(model)) ||
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
export function pickTradingExchange(exchanges: Exchange[]) {
|
||||
return (
|
||||
exchanges.find(
|
||||
(exchange) =>
|
||||
isHyperliquidExchange(exchange) &&
|
||||
exchange.enabled &&
|
||||
exchangeHasKey(exchange) &&
|
||||
Boolean(exchange.hyperliquidBuilderApproved) &&
|
||||
(exchange.hyperliquidWalletAddr || '').trim() !== ''
|
||||
) || null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a launch-capable AI model, auto-provisioning the beginner claw402
|
||||
* wallet when none is configured yet. Returns null when nothing could be
|
||||
* resolved — the caller routes the user into claw402 setup.
|
||||
*/
|
||||
export async function resolveLaunchModel(): Promise<AIModel | null> {
|
||||
let models = await api.getModelConfigs()
|
||||
let model = pickTradingModel(models)
|
||||
if (model) return model
|
||||
|
||||
const onboarding = await api.prepareBeginnerOnboarding()
|
||||
models = await api.getModelConfigs()
|
||||
model =
|
||||
models.find(
|
||||
(item) =>
|
||||
item.id === onboarding.configured_model_id &&
|
||||
item.enabled &&
|
||||
modelHasCredential(item)
|
||||
) || pickTradingModel(models)
|
||||
if (model) return model
|
||||
|
||||
if (onboarding.configured_model_id && onboarding.private_key) {
|
||||
await api.updateModelConfigs({
|
||||
models: {
|
||||
[onboarding.configured_model_id]: {
|
||||
enabled: true,
|
||||
api_key: onboarding.private_key,
|
||||
custom_api_url: '',
|
||||
custom_model_name: onboarding.default_model,
|
||||
},
|
||||
},
|
||||
})
|
||||
models = await api.getModelConfigs()
|
||||
model =
|
||||
models.find(
|
||||
(item) =>
|
||||
item.id === onboarding.configured_model_id &&
|
||||
item.enabled &&
|
||||
modelHasCredential(item)
|
||||
) || pickTradingModel(models)
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a launch-capable exchange. Returns the exchange or a message
|
||||
* explaining the most specific missing prerequisite.
|
||||
*/
|
||||
export async function resolveLaunchExchange(): Promise<
|
||||
{ exchange: Exchange } | { exchange: null; reason: string }
|
||||
> {
|
||||
const exchanges = await api.getExchangeConfigs()
|
||||
const ready = pickTradingExchange(exchanges)
|
||||
if (ready) return { exchange: ready }
|
||||
|
||||
const hyperliquid = exchanges.find(isHyperliquidExchange)
|
||||
if (!hyperliquid) {
|
||||
return {
|
||||
exchange: null,
|
||||
reason:
|
||||
'No Hyperliquid account is connected. Connect Hyperliquid and authorize the NOFX agent first.',
|
||||
}
|
||||
}
|
||||
if (!hyperliquid.enabled) {
|
||||
return {
|
||||
exchange: null,
|
||||
reason: 'The Hyperliquid account is disabled. Enable it first.',
|
||||
}
|
||||
}
|
||||
if (!exchangeHasKey(hyperliquid)) {
|
||||
return {
|
||||
exchange: null,
|
||||
reason:
|
||||
'The Hyperliquid agent key is missing. Reconnect Hyperliquid and save the agent wallet.',
|
||||
}
|
||||
}
|
||||
if (!hyperliquid.hyperliquidBuilderApproved) {
|
||||
return {
|
||||
exchange: null,
|
||||
reason:
|
||||
'Hyperliquid builder authorization is not complete. Finish wallet authorization first.',
|
||||
}
|
||||
}
|
||||
return {
|
||||
exchange: null,
|
||||
reason:
|
||||
'The Hyperliquid wallet address is missing. Reconnect Hyperliquid first.',
|
||||
}
|
||||
}
|
||||
44
web/src/lib/launch/types.ts
Normal file
44
web/src/lib/launch/types.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export type LaunchCheckStatus = 'ok' | 'failed' | 'warning' | 'skipped'
|
||||
|
||||
export type LaunchCheckId =
|
||||
| 'ai_model'
|
||||
| 'ai_wallet'
|
||||
| 'ai_wallet_funds'
|
||||
| 'strategy'
|
||||
| 'exchange_config'
|
||||
| 'exchange_account'
|
||||
| 'exchange_funds'
|
||||
|
||||
export interface LaunchCheck {
|
||||
id: LaunchCheckId | string
|
||||
status: LaunchCheckStatus
|
||||
code?: string
|
||||
message?: string
|
||||
required?: number
|
||||
actual?: number
|
||||
asset?: string
|
||||
address?: string
|
||||
}
|
||||
|
||||
export interface LaunchPreflightResult {
|
||||
ready: boolean
|
||||
checks: LaunchCheck[]
|
||||
min_ai_fee_usdc: number
|
||||
min_trading_usdc: number
|
||||
checked_at: string
|
||||
}
|
||||
|
||||
/** Guided-setup anchor consumed by the traders page (`?setup=`). */
|
||||
export type SetupTarget = 'claw402' | 'hyperliquid' | 'hyperliquid-funds'
|
||||
|
||||
export type LaunchOutcome =
|
||||
| { ok: true; traderId: string; warning?: string }
|
||||
| {
|
||||
ok: false
|
||||
kind: 'preflight'
|
||||
message: string
|
||||
preflight: LaunchPreflightResult
|
||||
setupTarget: SetupTarget | null
|
||||
}
|
||||
| { ok: false; kind: 'setup'; message: string; setupTarget: SetupTarget }
|
||||
| { ok: false; kind: 'error'; message: string }
|
||||
@@ -58,6 +58,34 @@ export function BeginnerOnboardingPage() {
|
||||
void loadOnboarding(true)
|
||||
}, [])
|
||||
|
||||
// Poll the balance while the user is depositing so the page updates on its
|
||||
// own. Uses the lightweight current-wallet endpoint (server caches 30s), not
|
||||
// the heavier prepare call that re-writes .env.
|
||||
const walletAddress = data?.address
|
||||
useEffect(() => {
|
||||
if (!walletAddress) return
|
||||
let cancelled = false
|
||||
const timer = setInterval(() => {
|
||||
void api
|
||||
.getCurrentBeginnerWallet()
|
||||
.then((wallet) => {
|
||||
if (cancelled || !wallet.found || !wallet.balance_usdc) return
|
||||
setData((prev) =>
|
||||
prev && prev.address === wallet.address
|
||||
? { ...prev, balance_usdc: wallet.balance_usdc! }
|
||||
: prev
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
// transient — the manual refresh button still works
|
||||
})
|
||||
}, 15000)
|
||||
return () => {
|
||||
cancelled = true
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, [walletAddress])
|
||||
|
||||
const noticeText = useMemo(
|
||||
() =>
|
||||
isZh
|
||||
@@ -176,9 +204,19 @@ export function BeginnerOnboardingPage() {
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm text-nofx-text-muted">
|
||||
{isZh
|
||||
? '$5-$10 usually lasts a long time'
|
||||
: '$5-$10 usually lasts a long time'}
|
||||
$5–$10 usually lasts a long time · balance updates by
|
||||
itself after you deposit
|
||||
</div>
|
||||
|
||||
{/* the wall every true beginner hits: where does USDC come from? */}
|
||||
<div className="mt-5 rounded-2xl border border-nofx-gold/20 bg-nofx-gold/10 px-5 py-4 text-left text-[13px] leading-6 text-nofx-text">
|
||||
<div className="mb-1 font-semibold">
|
||||
Don't have USDC yet?
|
||||
</div>
|
||||
Buy USDC on Binance, OKX or Coinbase, then withdraw it to
|
||||
the address above — and pick the{' '}
|
||||
<b>Base network</b> when the exchange asks. It usually
|
||||
arrives in about a minute. Only send USDC on Base.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -270,9 +308,7 @@ export function BeginnerOnboardingPage() {
|
||||
isZh ? 'text-[20px]' : 'text-[16px] sm:text-[18px]'
|
||||
}`}
|
||||
>
|
||||
<span>
|
||||
{isZh ? 'Go to Traders' : 'Go to Traders'}
|
||||
</span>
|
||||
<span>Continue setup</span>
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
import { FAQLayout } from '../components/faq/FAQLayout'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
|
||||
/**
|
||||
* FAQ page
|
||||
*
|
||||
* HeaderBar and Footer are now provided by MainLayout
|
||||
*
|
||||
* All FAQ-related logic lives in child components:
|
||||
* - FAQLayout: overall layout and search logic
|
||||
* - FAQSearchBar: search box
|
||||
* - FAQSidebar: left-side table of contents
|
||||
* - FAQContent: right-side content area
|
||||
*
|
||||
* FAQ data is configured in data/faqData.ts
|
||||
* FAQ page — layout, search, sidebar and content live in components/faq/*;
|
||||
* the questions themselves are data in data/faqData.ts.
|
||||
*/
|
||||
export function FAQPage() {
|
||||
const { language } = useLanguage()
|
||||
|
||||
return <FAQLayout language={language} />
|
||||
return <FAQLayout />
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import type {
|
||||
Strategy,
|
||||
StrategyConfig,
|
||||
} from '../types'
|
||||
import type { AIModel, Exchange } from '../types'
|
||||
import { launchAutopilot } from '../lib/launch/launchAutopilot'
|
||||
import type {
|
||||
MarketSymbol,
|
||||
VergexHeatmapBin,
|
||||
@@ -36,7 +36,7 @@ import type {
|
||||
VergexSignalItem,
|
||||
VergexSignalLabResponse,
|
||||
} from '../lib/api/data'
|
||||
import { buildDashboardPath } from '../router/paths'
|
||||
import { buildDashboardPath, ROUTES } from '../router/paths'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || ''
|
||||
|
||||
@@ -79,48 +79,6 @@ const confidenceOptions = [65, 75, 82]
|
||||
const text = (language: string, zh: string, en: string) =>
|
||||
language === 'zh' ? zh : en
|
||||
|
||||
function modelHasCredential(model: AIModel) {
|
||||
return Boolean(model.has_api_key || model.apiKey)
|
||||
}
|
||||
|
||||
function pickTradingModel(models: AIModel[]) {
|
||||
return (
|
||||
models.find(
|
||||
(model) =>
|
||||
model.provider === 'claw402' &&
|
||||
model.enabled &&
|
||||
modelHasCredential(model)
|
||||
) ||
|
||||
models.find((model) => model.enabled && modelHasCredential(model)) ||
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
function isHyperliquidExchange(exchange: Exchange) {
|
||||
return exchange.exchange_type === 'hyperliquid'
|
||||
}
|
||||
|
||||
function hyperliquidWalletAddress(exchange: Exchange) {
|
||||
return exchange.hyperliquidWalletAddr || ''
|
||||
}
|
||||
|
||||
function pickTradingExchange(exchanges: Exchange[]) {
|
||||
return (
|
||||
exchanges.find(
|
||||
(exchange) =>
|
||||
isHyperliquidExchange(exchange) &&
|
||||
exchange.enabled &&
|
||||
modelHasExchangeKey(exchange) &&
|
||||
Boolean(exchange.hyperliquidBuilderApproved) &&
|
||||
hyperliquidWalletAddress(exchange).trim() !== ''
|
||||
) || null
|
||||
)
|
||||
}
|
||||
|
||||
function modelHasExchangeKey(exchange: Exchange) {
|
||||
return Boolean(exchange.has_api_key || exchange.apiKey)
|
||||
}
|
||||
|
||||
type Profile = 'careful' | 'balanced' | 'active'
|
||||
|
||||
const profileOptions: Array<{
|
||||
@@ -379,7 +337,8 @@ function signalBiasInfo(bias: string | undefined) {
|
||||
return {
|
||||
label: 'Neutral',
|
||||
hint: 'Neutral bias',
|
||||
classes: 'border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper text-nofx-text-muted',
|
||||
classes:
|
||||
'border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper text-nofx-text-muted',
|
||||
icon: Target,
|
||||
}
|
||||
}
|
||||
@@ -1388,7 +1347,11 @@ export function StrategyStudioPage() {
|
||||
notify.success(
|
||||
successMessage ||
|
||||
(activateAfter
|
||||
? text(language, 'Strategy saved and activated', 'Strategy saved and activated')
|
||||
? text(
|
||||
language,
|
||||
'Strategy saved and activated',
|
||||
'Strategy saved and activated'
|
||||
)
|
||||
: text(language, 'Strategy saved', 'Strategy saved'))
|
||||
)
|
||||
await loadStrategies(selectedStrategy.id)
|
||||
@@ -1426,10 +1389,12 @@ export function StrategyStudioPage() {
|
||||
risk_control: defaultRisk({
|
||||
...base.ai_config?.risk_control,
|
||||
max_positions: 2,
|
||||
btc_eth_max_leverage: 10,
|
||||
altcoin_max_leverage: 10,
|
||||
btc_eth_max_position_value_ratio: 10,
|
||||
altcoin_max_position_value_ratio: 10,
|
||||
btc_eth_max_leverage: 5,
|
||||
altcoin_max_leverage: 5,
|
||||
// Few, concentrated positions held for big moves. 5x leverage so a
|
||||
// wide (-5%) stop is survivable; 2 positions × 2.5x = 5x total.
|
||||
btc_eth_max_position_value_ratio: 2.5,
|
||||
altcoin_max_position_value_ratio: 2.5,
|
||||
max_margin_usage: 1.0,
|
||||
min_confidence: 78,
|
||||
min_risk_reward_ratio: 3,
|
||||
@@ -1441,143 +1406,48 @@ export function StrategyStudioPage() {
|
||||
return base
|
||||
}
|
||||
|
||||
const resolveOneClickModel = async () => {
|
||||
let models = await api.getModelConfigs()
|
||||
let model = pickTradingModel(models)
|
||||
if (model) return model
|
||||
|
||||
const onboarding = await api.prepareBeginnerOnboarding()
|
||||
models = await api.getModelConfigs()
|
||||
model =
|
||||
models.find(
|
||||
(item) =>
|
||||
item.id === onboarding.configured_model_id &&
|
||||
item.enabled &&
|
||||
modelHasCredential(item)
|
||||
) || pickTradingModel(models)
|
||||
|
||||
if (!model && onboarding.configured_model_id && onboarding.private_key) {
|
||||
await api.updateModelConfigs({
|
||||
models: {
|
||||
[onboarding.configured_model_id]: {
|
||||
enabled: true,
|
||||
api_key: onboarding.private_key,
|
||||
custom_api_url: '',
|
||||
custom_model_name: onboarding.default_model,
|
||||
},
|
||||
},
|
||||
})
|
||||
models = await api.getModelConfigs()
|
||||
model =
|
||||
models.find(
|
||||
(item) =>
|
||||
item.id === onboarding.configured_model_id &&
|
||||
item.enabled &&
|
||||
modelHasCredential(item)
|
||||
) || pickTradingModel(models)
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
throw new Error(
|
||||
'No enabled AI model is ready. Create or fund the Claw402 wallet first.'
|
||||
)
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
const resolveOneClickExchange = async () => {
|
||||
const exchanges = await api.getExchangeConfigs()
|
||||
const exchange = pickTradingExchange(exchanges)
|
||||
if (exchange) return exchange
|
||||
|
||||
const hyperliquid = exchanges.find(isHyperliquidExchange)
|
||||
if (!hyperliquid) {
|
||||
throw new Error(
|
||||
'No Hyperliquid account is ready. Connect Hyperliquid and authorize the NOFX agent first.'
|
||||
)
|
||||
}
|
||||
if (!hyperliquid.enabled) {
|
||||
throw new Error('The Hyperliquid account is disabled. Enable it first.')
|
||||
}
|
||||
if (!modelHasExchangeKey(hyperliquid)) {
|
||||
throw new Error(
|
||||
'The Hyperliquid agent key is missing. Reconnect Hyperliquid and save the agent wallet.'
|
||||
)
|
||||
}
|
||||
if (!hyperliquid.hyperliquidBuilderApproved) {
|
||||
throw new Error(
|
||||
'Hyperliquid builder authorization is not complete. Finish wallet authorization first.'
|
||||
)
|
||||
}
|
||||
if (!hyperliquidWalletAddress(hyperliquid).trim()) {
|
||||
throw new Error(
|
||||
'The Hyperliquid wallet address is missing. Reconnect Hyperliquid first.'
|
||||
)
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'No ready Hyperliquid account found. Check the wallet authorization.'
|
||||
)
|
||||
}
|
||||
|
||||
const startUnifiedClaw402Agent = async () => {
|
||||
if (!selectedStrategy) return
|
||||
const config = buildUnifiedClaw402Config()
|
||||
const traderName = 'NOFX Autopilot'
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
setEditingConfig(config)
|
||||
setHasChanges(true)
|
||||
|
||||
await api.updateStrategy(selectedStrategy.id, {
|
||||
name: selectedStrategy.name,
|
||||
description:
|
||||
selectedStrategy.description ||
|
||||
'Autonomous market selection powered by Claw402.ai Signal Lab, liquidation structure, and raw candles.',
|
||||
config,
|
||||
// The shared launcher runs the server-side preflight (fresh wallet and
|
||||
// exchange balances) BEFORE ensureStrategy, so a failed launch never
|
||||
// mutates or activates the strategy as a side effect.
|
||||
const outcome = await launchAutopilot({
|
||||
scanIntervalMinutes: 15,
|
||||
ensureStrategy: async () => {
|
||||
const config = buildUnifiedClaw402Config()
|
||||
setEditingConfig(config)
|
||||
await api.updateStrategy(selectedStrategy.id, {
|
||||
name: selectedStrategy.name,
|
||||
description:
|
||||
selectedStrategy.description ||
|
||||
'Autonomous market selection powered by Claw402.ai Signal Lab, liquidation structure, and raw candles.',
|
||||
config,
|
||||
})
|
||||
await api.activateStrategy(selectedStrategy.id)
|
||||
return selectedStrategy.id
|
||||
},
|
||||
})
|
||||
await api.activateStrategy(selectedStrategy.id)
|
||||
|
||||
const [model, exchange] = await Promise.all([
|
||||
resolveOneClickModel(),
|
||||
resolveOneClickExchange(),
|
||||
])
|
||||
|
||||
const traderRequest = {
|
||||
name: traderName,
|
||||
ai_model_id: model.id,
|
||||
exchange_id: exchange.id,
|
||||
strategy_id: selectedStrategy.id,
|
||||
scan_interval_minutes: 15,
|
||||
is_cross_margin: true,
|
||||
show_in_competition: true,
|
||||
if (!outcome.ok) {
|
||||
notify.error(outcome.message)
|
||||
const setupTarget =
|
||||
outcome.kind === 'error' ? null : outcome.setupTarget
|
||||
if (setupTarget) {
|
||||
navigate(`${ROUTES.traders}?setup=${setupTarget}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const existingTraders = await api.getTraders(true)
|
||||
const existingAutopilot = existingTraders.find(
|
||||
(trader) => trader.trader_name === traderName
|
||||
)
|
||||
const autopilot = existingAutopilot
|
||||
? await api.updateTrader(existingAutopilot.trader_id, traderRequest)
|
||||
: await api.createTrader(traderRequest)
|
||||
|
||||
if (autopilot.startup_warning) {
|
||||
notify.warning(autopilot.startup_warning)
|
||||
if (outcome.warning) {
|
||||
notify.warning(outcome.warning)
|
||||
}
|
||||
|
||||
if (!autopilot.is_running) {
|
||||
await api.startTrader(autopilot.trader_id)
|
||||
}
|
||||
notify.success(`${traderName} started`)
|
||||
notify.success('NOFX Autopilot started')
|
||||
setHasChanges(false)
|
||||
await loadStrategies(selectedStrategy.id)
|
||||
navigate(buildDashboardPath(autopilot.trader_id))
|
||||
} catch (err) {
|
||||
notify.error(
|
||||
err instanceof Error ? err.message : 'Failed to launch NOFX Autopilot'
|
||||
)
|
||||
navigate(buildDashboardPath(outcome.traderId))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -2381,7 +2251,11 @@ export function StrategyStudioPage() {
|
||||
<div className="rounded-lg border border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter p-4">
|
||||
<div className="mb-4 flex items-center gap-2 text-sm font-semibold text-nofx-text">
|
||||
<Shield className="h-4 w-4 text-nofx-success" />
|
||||
{text(language, 'Trading parameters', 'Trading parameters')}
|
||||
{text(
|
||||
language,
|
||||
'Trading parameters',
|
||||
'Trading parameters'
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<label className="space-y-2">
|
||||
@@ -2424,7 +2298,11 @@ export function StrategyStudioPage() {
|
||||
</label>
|
||||
<label className="space-y-2">
|
||||
<span className="text-xs text-nofx-text-muted">
|
||||
{text(language, 'Entry confidence', 'Entry confidence')}
|
||||
{text(
|
||||
language,
|
||||
'Entry confidence',
|
||||
'Entry confidence'
|
||||
)}
|
||||
</span>
|
||||
<select
|
||||
value={risk.min_confidence}
|
||||
|
||||
@@ -31,7 +31,6 @@ import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { useSystemConfig } from '../hooks/useSystemConfig'
|
||||
import { t } from '../i18n/translations'
|
||||
import { api } from '../lib/api'
|
||||
import { getUserMode } from '../lib/onboarding'
|
||||
import type {
|
||||
AccountInfo,
|
||||
DecisionRecord,
|
||||
@@ -446,12 +445,11 @@ export function AppRoutes() {
|
||||
<Route
|
||||
path={ROUTES.welcome}
|
||||
element={
|
||||
// The welcome overlay is the AI-wallet deposit page (QR +
|
||||
// auto-refreshing balance) — useful to every signed-in user, so
|
||||
// no legacy "beginner mode" gate here.
|
||||
isAuthenticated ? (
|
||||
getUserMode() === 'beginner' ? (
|
||||
<TradersRoute showBeginnerOnboarding />
|
||||
) : (
|
||||
<Navigate to={ROUTES.traders} replace />
|
||||
)
|
||||
<TradersRoute showBeginnerOnboarding />
|
||||
) : (
|
||||
<Navigate to={ROUTES.login} replace />
|
||||
)
|
||||
|
||||
@@ -13,6 +13,13 @@ export interface SystemStatus {
|
||||
ai_provider: string
|
||||
strategy_type?: 'ai_trading' | 'grid_trading'
|
||||
grid_symbol?: string
|
||||
/** Runtime health: true when AI failed repeatedly and no new positions open. */
|
||||
safe_mode?: boolean
|
||||
safe_mode_reason?: string
|
||||
/** Claw402 AI fee wallet health, observed by the run loop. */
|
||||
ai_wallet_status?: 'ok' | 'low' | 'empty' | 'unknown'
|
||||
ai_wallet_balance_usdc?: number
|
||||
ai_wallet_checked_at?: string
|
||||
}
|
||||
|
||||
export interface AccountInfo {
|
||||
@@ -103,6 +110,7 @@ export interface TraderFullStats {
|
||||
total_fee: number
|
||||
avg_win: number
|
||||
avg_loss: number
|
||||
/** Percent, not a fraction: 18.5 means -18.5% peak drawdown. */
|
||||
max_drawdown_pct: number
|
||||
}
|
||||
|
||||
@@ -178,10 +186,12 @@ export interface HistoricalPosition {
|
||||
entry_quantity: number
|
||||
entry_price: number
|
||||
entry_order_id: string
|
||||
entry_time: string
|
||||
/** Epoch milliseconds. */
|
||||
entry_time: number
|
||||
exit_price: number
|
||||
exit_order_id: string
|
||||
exit_time: string
|
||||
/** Epoch milliseconds. */
|
||||
exit_time: number
|
||||
realized_pnl: number
|
||||
fee: number
|
||||
leverage: number
|
||||
|
||||
Reference in New Issue
Block a user