23 Commits

Author SHA1 Message Date
tinkle-community
39eac5aca7 config: stop the churn — hold for big moves, wide TP/SL, low leverage
Live decomposition of the losing streak: 23% win rate with avg win +$1.23 /
avg loss -$1.04 on ~0.3-0.5% price moves, where the ~0.14% round-trip fee ate
30-50% of every tiny winner. Death by small-move grinding. The AI was closing
positions on ±0.5% noise after the 60m min-hold, capping winners at ~0.86%.

Redesign to 'few big positions, held for big moves':
- Throttle: min hold 60m->4h, noise-close window 90m->8h, reentry 30m->3h,
  opens/hour 30->3, opens/cycle 6->2. Noise band widened -1%..+2% -> -4%..+6%
  so small moves can no longer trigger a close.
- Exits: stop bypass -2.5% -> -5%, take-profit bypass +5% -> +12% (wide,
  asymmetric — let winners run, cut losers only on a real move).
- Leverage 20x -> 5x: a -5% stop at 20x is instant liquidation; at 5x it is
  -25% of margin, survivable. 2 positions x 2.5x = 5x total (full margin,
  ~20% cushion) instead of 4x5x=20x.
- Prompt now instructs the AI to set wide stops (~-5%) and distant targets
  (~+10-12%), hold multi-hour, and never scalp 0.5% moves.

Live strategy updated (maxPos=2, lev=5, ratio=2.5).
2026-07-21 15:02:23 +09:00
tinkle-community
0f3e71560c fix: fetch all Hyperliquid fills — UserFillsByTime is capped at 100
Root cause of the dashboard over-reporting profit while the account lost
money: the fill sync used UserFillsByTime, which hard-caps at 100 fills per
response. At 20x/4-position frequency the account does >100 fills/24h, so
~20% of fills were silently dropped — and the dropped ones skewed toward
losers, inflating recorded PnL and under-counting fees. Verified: over a 46h
window the DB showed net +$39 while Hyperliquid official was -$18.6, and the
equity drop ($176 -> $156) confirmed the loss.

Switch GetTrades to UserFills (returns up to 2000 recent fills), filtering to
startTime client-side, and widen the sync lookback 24h -> 7d so gaps
backfill. Verified live: a sync now pulls 675 fills where it previously
always received exactly 100.

Note: this stops future drift; already-corrupted historical position rows are
not retroactively rebuilt (dedup blocks re-processing). Account equity remains
the authoritative scoreboard.
2026-07-16 11:11:56 +09:00
tinkle-community
eabd279d10 feat: show hold duration in the recent trades panel
Adds a HOLD column (45m / 2h10 / 1d3h) plus a header row to the dashboard's
recent-closes table. Makes the hold-time-vs-PnL pattern (short holds bleed,
1h+ holds carry the edge) visible per trade instead of only in the aggregate
Edge Profile panel.
2026-07-15 19:36:58 +09:00
tinkle-community
09b7ac9e92 config: autopilot to 4x 5x notional at 20x leverage; short floor 0.75 -> 0.4
Two independent changes bundled:
- Position sizing back to 4 positions x 5x equity notional at 20x leverage
  (20x total account notional, ~5% liquidation cushion) — aggressive book
  size by operator choice, for bigger single positions on a small account.
- Forced short-coverage signal floor lowered 0.75 -> 0.4. At 0.75, a
  long-leaning board left every bearish candidate below the bar, so no short
  ever opened and the book became a one-directional long bet. 0.4 keeps
  genuine directional signals so the book actually hedges — which matters
  more, not less, at 20x.
2026-07-14 12:11:29 +09:00
tinkle-community
dc68884559 feat: autopilot book to 4 positions x 5x notional at 20x leverage
Was 2 positions x ~equity×1.2 — too few holdings and no room for shorts
(the 2 slots filled with the strongest signals, which were long-leaning, so
the balanced candidate pool never got expressed as short trades).

Now: max 4 positions, each sized at equity×5 notional, 20x leverage. Four
positions × 5x = 20x total account notional = full margin at 20x — the
operator's requested ceiling. This gives room for ~2 long + 2 short and
bigger single positions; the existing direction-balanced candidate selection
plus long/short coverage fills both sides when strong bearish signals exist.

Applied across all three config sources (default template, quick-create
preset, studio unified) with matching test assertions and 20x prompt copy.
2026-07-11 11:20:33 +09:00
tinkle-community
21407030ea fix: reconcile local positions against the live exchange book — stop lost PnL
Root cause of the dashboard under-reporting: any missed or unmatched fill
(position flip, sync gap, liquidation) left a 'zombie' OPEN row. Every later
close on that symbol landed as a partial close against the zombie, so the
row never reached CLOSED and its realized PnL never entered the closed-trade
statistics — Edge Profile, realized P/L, win rate, and the AI's own
track-record context all silently under-reported.

Fix: after each Hyperliquid order sync, reconcile local OPEN rows against the
exchange's live book (core perps + xyz dex), scoped by exchange account so
rows left by prior autopilot incarnations (each relaunch mints a fresh
trader_id sharing one exchange) are healed too. Rows the exchange no longer
holds are closed with their accumulated PnL; oversized rows are trimmed to
the live quantity. Live run confirmed: 9 zombie rows closed across
incarnations, book now matches the exchange exactly.

Also removes the header language switcher (desktop + mobile) and the
login/setup LanguageSwitcher — the product UI is English-only.

New store methods GetOpenPositionsByExchange + ReconcileOpenPositionsWithLive
with cross-incarnation test coverage.
2026-07-10 20:04:57 +09:00
tinkle-community
7a66d048f3 feat: position sizing follows the strategy config — default 4x equity notional
Positions were stuck at ~equity×1.2 (~$420 on a $500 account) no matter
what the strategy said: both the manager load path and the per-cycle config
reload hardcoded vergex strategies to a 6-position × equity×1.2 book,
silently overriding the configured 10x ratio. Both overrides removed —
sizing now comes from the strategy's own RiskControl (ClampLimits bounds it,
margin auto-reduce keeps the book solvent).

Defaults aligned across all three config sources (backend default template,
quick-create preset, studio unified config): 2 positions × 4x equity
notional at 10x leverage ≈ 80% margin when both are open — concentrated per
the operator's request. Live strategy updated via relaunch; loader log
confirms maxPos=2 posRatio=4.0.
2026-07-10 19:13:12 +09:00
tinkle-community
8c8cd9b61f feat: show entry price and notional size in the live positions panel
The panel only showed symbol/side/leverage/PnL — entry and position size
are the first things a trader checks. Side and leverage share a column to
make room; prices format with magnitude-aware precision (64,187 · 184.2 ·
0.0067); size is |qty| × mark notional.
2026-07-10 18:56:15 +09:00
tinkle-community
d84e22ab82 docs: describe the real decision data stack in all READMEs
'candles, EMA/MACD/RSI/ATR/Bollinger' undersold what the model actually
reads. All seven READMEs (en, zh-CN, ja, ko, ru, uk, vi) now describe the
Claw402.ai · Vergex stack: the live signal board with directional bias and
signal strength, per-symbol Signal Lab deep signals, cost-basis and
liquidation heatmaps, and real-time net flow — cross-checked against raw
candles and the trader's own track record.
2026-07-07 23:26:12 +09:00
tinkle-community
221cf3d61d docs: sync all six i18n READMEs with the rewritten English README
zh-CN, ja, ko, ru, uk, vi fully retranslated to mirror the new English
README section-for-section: intro, first-run steps, exchange table, risk
runtime table, terminal overview, Claw402 model access, markets,
architecture, install (Linux/macOS, Railway, Docker, Windows, source),
docs/community/contributing, and the risk disclaimer.

Verified across all six: identical section structure (13 headings),
byte-identical architecture diagram and code blocks, every relative link
resolves (banner, exchange icons, screenshots, docs pages — zh-CN links the
zh-CN getting-started/FAQ/troubleshooting variants), correct language
selector rows.
2026-07-07 23:20:02 +09:00
tinkle-community
390db72cdb feat: remove the last beginner walls — USDC sourcing help, reachable deposit page, first-run reassurance
- The /welcome deposit page (QR + auto-refreshing balance + key backup) was
  gated behind a legacy 'beginner mode' localStorage flag that nothing sets
  anymore — every deposit link silently bounced to /traders. Gate removed:
  any signed-in user reaches it.
- 'Don't have USDC yet?' helper on the deposit page: buy on Binance/OKX/
  Coinbase, withdraw picking the Base network, arrives in ~1 minute. This is
  the wall every true newcomer hits first.
- Autopilot step 3 now says exactly where trading money goes
  (app.hyperliquid.xyz → Deposit, USDC on Arbitrum).
- Register page: 'NEW_USER ONBOARDING / Initializing Registration Sequence'
  jargon replaced with what actually happens next (guided launch, ~$13,
  five minutes).
- Dashboard shows a calm first-run note while a fresh autopilot runs its
  first cycle ('reading the whole market — first decision lands in a minute
  or two'), so the initial silence doesn't read as broken.
- Hyperliquid connect steps renamed to plain language ('Approve it in your
  wallet (trade-only, cannot withdraw)' etc.).
2026-07-07 23:03:20 +09:00
tinkle-community
c1b660bb45 feat: beginner-first funnel — plain-language guidance from landing to first trade
A newcomer who can't get from install to a running trade in minutes churns.
This pass removes every jargon wall on that path:

- Landing hero: primary CTA is now 'START THE AUTOPILOT' (routes into the
  guided flow; logged-out users get the guest walkthrough) with a
  plain-language promise line — self-hosted, ~$13 to start, no API keys.
  'CREATE STOCK TRADER' demoted to a 'SEE IT WORK' scroll button.
- Deploy section: 'high-frequency trading node / sandboxed execution env'
  copy replaced with plain words plus the first-five-minutes checklist
  (register → fund $1 + $12 → press start).
- Config page: the guided launch panel now renders FIRST for anyone without
  a running autopilot; the config grid becomes detail below. Steps renamed
  to 'Step 1 · Fund the AI wallet ($1+)' etc., each explained in one plain
  sentence with explicit dollar amounts and safety notes ('it can never
  withdraw your money').
- Every AI-wallet action (deposit, create, preflight fix-links) now lands on
  the /welcome page with the QR code and auto-refreshing balance instead of
  the key-config modal.
- README: first-run three-step guide under the install command (banner asset
  included); six dead landing components removed.

Tests: 128 web tests green; funnel click-verified logged-out and logged-in.
2026-07-07 20:22:59 +09:00
tinkle-community
c53a563bff feat: data-driven autopilot profitability tuning + edge profile panel
Analysis of 410 live closed trades found the edge is real but was being
destroyed by execution: gross +$267 vs $245 fees; trades held <1h were net
negative (the <15m bucket alone: -$48 on $66 fees) while 1h+ holds carried
+$78; shorts lost $72 while longs made $94 — and both the prompt and the
engine were manufacturing those losers.

Changes, each tied to the data:
- Prompt: removed the 'MUST open at least one long AND one short every
  cycle' mandate (forced weak-signal shorts); direction is now data-driven
  with 'never open just to balance the book'. Added fee-awareness (round
  trip ≈ 0.1% notional, require expected move ≥ 3x cost) and aligned the
  hold discipline with the backend throttle.
- Forced book-balance opens now require |board z-score| ≥ 0.75 — the engine
  previously force-opened full-size 10x positions on near-neutral signals
  with hardcoded confidence 70. DirectionalCandidates now carries scores.
- Min AI-managed hold raised 45m → 60m (the 15-60m bucket still bled after
  the earlier throttle landed).
- Legacy prompt hygiene: vergex path drops long-only-era custom prompts and
  zh-era configs fall back wholesale to built-in English sections — fixes
  the two long-failing kernel prompt tests.
- New Edge Profile dashboard panel: net after fees by hold-time bucket and
  side, computed from recent closed trades, with an automatic takeaway line
  — the fee/churn regression detector, always visible. Fixed the
  HistoricalPosition timestamp types (epoch ms, not strings).
2026-07-07 19:21:49 +09:00
tinkle-community
e593874912 docs: add Autopilot pipeline and data-source entries to FAQ
Two entries the FAQ was missing: the strategy's four-stage decision funnel
(Claw402 ranking universe → Signal Lab + cost/liq verification → 15m candle
timing → confidence/risk-reward gated sizing, plus the hard risk-control
layer outside the AI) and a breakdown of every data source with which parts
are paid per call from the AI fee wallet.
2026-07-06 15:13:50 +09:00
tinkle-community
e9873b5b86 feat: rewrite FAQ with current product content and cream terminal design
Content: replaces the stale i18n-keyed answers (manual API-key setup, old
Binance-era flows) with 34 answers matching the shipping product — Autopilot
launch chain, AI fee wallet and Hyperliquid agent authorization, preflight
checks and minimums, safe mode and fee-wallet-empty recovery, dashboard
metric semantics (realized vs unrealized, fee drag, real-baseline drawdown),
security model, self-hosting, and contributing. English-only, matching the UI.

Format: FAQ answers are now typed content blocks (paragraphs, bullets,
numbered steps, notes, link chips) rendered generically — the two giant
hardcoded JSX special cases are gone. Restyled from the broken dark
glassmorphism (unreadable prose-invert on the cream theme) to the terminal
design language: small-caps mono category strips, hairline dividers, gold
accents, code chips, bordered TOC with active tracking. Search filters the
full block text; ~480 lines of dead faq* translation keys removed (faqNav
kept for the nav label).
2026-07-06 14:31:26 +09:00
tinkle-community
026c5d3089 fix: dashboard metrics — real drawdown baseline, realized/unrealized split, risk radar fields
- Max drawdown was double-broken: the backend built the equity curve on a
  hardcoded $10k baseline (understating a small account's drawdown ~20x) and
  the frontend multiplied the already-percent value by 100 again (0.87% shown
  as -86.9%). The curve now starts from the trader's real initial balance
  (10k fallback when unknown) and the UI renders the percent once; the demo
  engine and type docs are aligned to percent semantics.
- Header now separates equity-based 'Total P/L (incl. unrealized)' from
  'Realized P/L (closed trades)' so it no longer contradicts profit factor /
  win rate, and the stats strip shows the fee-drag chain
  (gross - fees = net) plus a per-trade-labelled sharpe.
- Risk radar read a non-existent account field (total_unrealized_profit) so
  unrealized PnL always showed $0; small PnLs also render with cents now.
- Nav 'Connect Hyperliquid' turns into a green connected chip when the server
  already holds a fully-authorized exchange, instead of nagging forever from
  a browser without the localStorage flow state.
2026-07-06 00:01:44 +09:00
tinkle-community
ee5917adc6 fix: raise timeout for trader lifecycle calls to survive slow stop/restart
Live click-through showed PUT /traders/:id taking 33.8s (stopping the running
instance waits for its in-flight cycle and monitor goroutines) while the axios
default timeout is 30s — the frontend aborted a request that succeeded
server-side and reported a false launch failure. Create/update/start now use a
120s ceiling.
2026-07-05 23:42:35 +09:00
tinkle-community
9b70455e7f fix: report accurate is_running after trader update to stop redundant start
Live verification of the relaunch (update) branch surfaced this: the update
response omitted is_running, so the launcher fired a redundant start that got
400 'Trader is already running', showing an error toast and skipping the
dashboard navigation even though the relaunch succeeded.

- PUT /traders/:id now reports is_running (a running trader is restarted with
  the new config, so it stays running).
- The already-running rejection carries error_key trader.start.already_running
  and the shared launcher treats it as success (launch is idempotent).
2026-07-05 23:36:53 +09:00
tinkle-community
a744328313 feat: unify autopilot launch on server preflight with live balance polling
- New shared launch module (web/src/lib/launch): preflight client, model/
  exchange resolution, and a single launchAutopilot orchestrator used by both
  Strategy Studio and the guided launch panel — the two previously duplicated
  and divergent launch implementations are gone.
- Preflight runs BEFORE any strategy mutation, so a failed launch no longer
  rewrites/activates the strategy as a side effect; readiness and minimums
  come from the server (live balances) instead of stale client caches.
- Guided panel polls preflight every 20s so deposits are detected without
  manual refresh; step metas show live balances and required minimums; the
  beginner wallet page polls its balance while the deposit screen is open.
- Every failure now routes to a guided fix: hyperliquid-funds gets its own
  setup anchor (scrolls to the launch panel), start rejections surface the
  server's preflight reason (including manual restarts from the trader list),
  and structured error bodies flow through ApiError.errorData.
- Terminal dashboard shows a persistent runtime banner when the AI fee wallet
  runs low/empty or the trader enters safe mode.
- Vitest coverage for setup-target routing, readiness helpers, and the
  launch orchestration order (no side effects before preflight).
2026-07-05 20:38:51 +09:00
tinkle-community
b17d97effc feat: server-side launch preflight with structured checks and runtime AI wallet health
- POST /api/launch/preflight and GET /api/traders/:id/preflight run every
  launch prerequisite (AI model credential, claw402 wallet key + Base USDC
  balance, strategy, exchange config, live account state, funding minimums)
  and return a structured checklist with stable codes; minimums (1 USDC AI
  fee / 12 USDC trading) now live server-side and are exposed in the response.
- POST /traders/:id/start enforces the preflight (400 with error_key
  trader.start.preflight_failed + full checklist; ?force=true to override).
  Funding gates use max(available, equity) so restarting a bot with deployed
  margin is not blocked; indeterminate probes (RPC outage) degrade to
  warnings instead of blocking.
- Run loop now surfaces runtime health via GetStatus: safe_mode(+reason) and
  ai_wallet_status/balance (ok|low|empty|unknown), including typed detection
  of claw402 ErrInsufficientFunds instead of burying it in logs; safe-mode
  state is mutex-guarded for API readers.
- Onboarding wallet endpoints switch to the cached, error-aware balance query
  and report balance_status so the UI can tell an RPC outage from an empty
  wallet.
2026-07-05 20:38:38 +09:00
tinkle-community
e61f12d307 chore: bump vite, form-data, @babel/core, js-yaml to patched versions
Resolves all open Dependabot alerts (2 high, 2 moderate, 1 low):
vite 6.4.3, form-data 4.0.6, @babel/core 7.29.7, js-yaml 4.3.0.
All within existing semver ranges; build and 111 tests pass.
2026-07-05 19:23:04 +09:00
tinkle-community
30f3feb586 feat: verify wallet and exchange balances before autopilot launch
Requires the Claw402 fee wallet to exist with at least 1 USDC on Base
and the Hyperliquid account to show at least 12 USDC available before
launch; failures route back to the matching guided-setup step.
2026-07-05 19:13:50 +09:00
tinkle-community
49613830ce feat: client-side simulation layer for terminal dashboard panels
Adds a local synthetic data engine that can drive all dashboard panels
(candles, order book, liq ladder, stats) without a backend connection,
plus de-synced beam animation phases in the orchestration topology.
2026-07-05 19:13:44 +09:00
82 changed files with 5661 additions and 4390 deletions

323
README.md
View File

@@ -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) |
[![Deploy on Railway](https://railway.com/button.svg)](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**
[![Deploy on Railway](https://railway.com/button.svg)](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.
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date)
## License
[AGPL-3.0](LICENSE)
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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 "", ""
}

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

View File

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

View 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

View File

@@ -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) |
[![Deploy on Railway](https://railway.com/button.svg)](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**
[![Deploy on Railway](https://railway.com/button.svg)](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 レコード → サーバー IPProxied を有効化)
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 が役に立ったら、スターを付けていただけると、他のトレーダーがこのプロジェクトを見つけやすくなります。
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date)
## ライセンス
[AGPL-3.0](../../../LICENSE)
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date)
<sub>自動売買には大きなリスクが伴います。AI 駆動の戦略は実験的なものであり、損失を出す可能性があります。適切なポジションサイズを守り、各取引所の仕組みを理解し、失っても差し支えのない資金以外では決して取引しないでください。詳細は[免責事項](../../../DISCLAIMER.md)をご覧ください。</sub>

View File

@@ -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) |
[![Deploy on Railway](https://railway.com/button.svg)](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**
[![Deploy on Railway](https://railway.com/button.svg)](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가 도움이 되었다면, 스타 하나가 다른 트레이더들이 이 프로젝트를 발견하는 데 큰 힘이 됩니다.
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date)
## 라이선스
[AGPL-3.0](../../../LICENSE)
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date)
<sub>자동매매에는 상당한 위험이 따릅니다. AI 기반 전략은 실험적이며 손실이 발생할 수 있습니다. 포지션 규모를 적절히 관리하고, 각 거래소의 구조를 이해하며, 잃어도 감당할 수 있는 자금으로만 거래하세요. 전체 [면책 조항](../../../DISCLAIMER.md)을 확인하세요.</sub>

View File

@@ -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) |
[![Deploy on Railway](https://railway.com/button.svg)](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**
[![Deploy on Railway](https://railway.com/button.svg)](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 вам полезен, звезда помогает другим трейдерам его найти.
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date)
## Лицензия
[AGPL-3.0](../../../LICENSE)
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date)
<sub>Автоматическая торговля сопряжена со значительным риском. Стратегии на основе AI экспериментальны и могут терять деньги. Разумно выбирайте размер позиций, разбирайтесь в устройстве каждой площадки и никогда не торгуйте средствами, потерю которых не можете себе позволить. Полный [дисклеймер](../../../DISCLAIMER.md).</sub>

View File

@@ -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) |
[![Deploy on Railway](https://railway.com/button.svg)](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).
[![Deploy on Railway](https://railway.com/button.svg)](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 вам корисний, зірка допоможе іншим трейдерам його знайти.
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date)
## Ліцензія
[AGPL-3.0](../../../LICENSE)
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date)
<sub>Автоматизована торгівля пов'язана зі значним ризиком. Стратегії на основі AI експериментальні й можуть втрачати гроші. Обирайте розмір позицій розважливо, розумійте кожен майданчик і ніколи не торгуйте коштами, втрату яких не можете собі дозволити. Повний [дисклеймер](../../../DISCLAIMER.md).</sub>

View File

@@ -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) |
[![Deploy on Railway](https://railway.com/button.svg)](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**
[![Deploy on Railway](https://railway.com/button.svg)](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ó.
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date)
## Giấy phép
[AGPL-3.0](../../../LICENSE)
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](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>

View File

@@ -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) |
[![Deploy on Railway](https://railway.com/button.svg)](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**
[![Deploy on Railway](https://railway.com/button.svg)](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 能让更多交易者发现它。
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date)
## 许可证
[AGPL-3.0](../../../LICENSE)
[![Star History Chart](https://api.star-history.com/svg?repos=NoFxAiOS/nofx&type=Date)](https://star-history.com/#NoFxAiOS/nofx&Date)
<sub>自动化交易存在重大风险。AI 驱动的策略尚处实验阶段,可能造成亏损。请合理控制仓位规模,充分了解每个交易场所,切勿投入无法承受损失的资金。完整[免责声明](../../../DISCLAIMER.md)。</sub>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -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.510, 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 &amp; open source · about $13 is enough to start · guided
setup, no API keys first trade in minutes
</p>
{/* Community Stats Row */}
<CommunityStats />

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 &amp; side · after fees</span>
</div>
<EdgeProfile positions={history?.positions} />
</div>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

@@ -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 515 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 3060 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 wallets 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 cant 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',
},
],
},
],
},
],
},

View File

@@ -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推荐性价比、阿里云通义千问、OpenAIGPT-5.2、Anthropic Claude、Google Gemini、xAI Grok、Kimi月之暗面。您也可以使用任何 OpenAI 兼容的 API 端点。每个模型各有优势 - DeepSeek 性价比高OpenAI 能力强但贵Claude 擅长推理。',
faqSystemRequirements: '系统要求是什么?',
faqSystemRequirementsAnswer:
'最低配置2 核 CPU2GB 内存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:
'三种方式1Docker Desktop推荐- 安装 Docker Desktop然后在 PowerShell 中运行 "docker compose -f docker-compose.prod.yml up -d"2WSL2 - 安装 Windows 子系统 Linux然后按 Linux 方式安装3WSL2 + Docker - 两全其美,在 WSL2 终端运行安装脚本。通过 http://127.0.0.1:3000 访问。',
faqDockerDeployment: 'Docker 部署一直失败',
faqDockerDeploymentAnswer:
'常见解决方案1检查 Docker 是否运行:"docker info"2确保足够内存最少 2GB3如果卡在 "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 访问。配置 HTTPS1使用 Cloudflare免费- 添加域名,创建 A 记录指向服务器 IPSSL 设为"灵活"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 KeyOKX 还需要 Passphrase。DEXHyperliquid/Aster/Lighter需要钱包地址和私钥。务必只启用必要权限合约交易并考虑 IP 白名单。',
faqBinanceAPISetup: '如何正确设置币安 API',
faqBinanceAPISetupAnswer:
'重要步骤1在币安 → API 管理中创建 API 密钥2仅启用"启用合约"权限3考虑添加 IP 白名单增强安全4关键在合约设置 → 偏好设置 → 持仓模式中切换为双向持仓模式5确保资金在合约钱包不是现货。-4061 错误表示需要双向持仓模式。',
faqHyperliquidSetup: '如何设置 Hyperliquid',
faqHyperliquidSetupAnswer:
'Hyperliquid 是去中心化交易所需要钱包认证。步骤1访问 app.hyperliquid.xyz2连接钱包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:
'常见原因1AI 决定等待查看决策日志了解原因2合约账户余额不足3达到最大持仓数限制默认34交易所 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.20Qwen$0.07-0.30OpenAI$0.70-2Claude$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:
'检查1API 密钥有效(用 curl 测试2网络能访问 API 端点ping/curl3API 提供商未宕机查看状态页4VPN/防火墙未阻止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重启 Docker3重试拉取。或使用 GitHub Container Registryghcr.io在您的地区可能连接更好。',
// ===== 安全相关 =====
faqAPIKeyStorage: 'API 密钥如何存储?',
faqAPIKeyStorageAnswer:
'API 密钥使用 AES-256-GCM 加密后存储在本地 SQLite 数据库中。加密密钥DATA_ENCRYPTION_KEY存储在您的 .env 文件中。密钥仅在 API 调用需要时在内存中解密。切勿分享您的 data.db 或 .env 文件。',
faqEncryptionDetails: 'NOFX 使用什么加密?',
faqEncryptionDetailsAnswer:
'NOFX 使用多层加密1AES-256-GCM 用于数据库存储API 密钥、密钥2RSA-2048 用于可选的传输加密浏览器到服务器3JWT 用于认证令牌。密钥在安装时生成。HTTPS 环境启用 TRANSPORT_ENCRYPTION=true。',
faqSecurityBestPractices: '安全最佳实践是什么?',
faqSecurityBestPracticesAnswer:
'建议1使用带 IP 白名单和最小权限(仅合约交易)的交易所 API 密钥2为 NOFX 使用专用子账户3远程部署启用 TRANSPORT_ENCRYPTION4切勿分享 .env 或 data.db 文件5使用有效证书的 HTTPS6定期轮换 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-5Claude 适合细致分析Qwen 价格有竞争力。您可以运行多个交易员使用不同模型进行比较。查看竞赛页面看哪个对您的策略表现最好。',
faqCustomAIAPI: '可以使用自定义 AI API 吗?',
faqCustomAIAPIAnswer:
'可以NOFX 支持任何 OpenAI 兼容的 API。在配置 → AI 模型 → 自定义 API 中1输入 API 端点 URL如 https://your-api.com/v12输入 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 Issues2文档 - 改进指南、翻译3Bug 报告 - 详细报告问题4功能建议 - 提出改进意见。从标记为"good first issue"的问题开始。所有贡献者可能获得空投奖励。',
faqPRGuidelines: 'PR 指南是什么?',
faqPRGuidelinesAnswer:
'PR 流程1Fork 仓库到您的账户2从 dev 创建功能分支:"git checkout -b feat/your-feature"3修改代码运行 lint"npm --prefix web run lint"4使用 Conventional Commits 格式提交5推送并创建 PR 到 NoFxAiOS/nofx:dev6关联相关 issueCloses #1237等待审核。保持 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: {

View File

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

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

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

View File

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

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

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

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

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

View 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.',
}
}

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

View File

@@ -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&apos;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>

View File

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

View File

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

View File

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

View File

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