Skip to content

Repository files navigation

AI Perp Trading Agent

A non-custodial perp-trading agent for SOL-PERP on Drift. Paper-mode by default; multiple safety gates required before any real transaction can be sent.

No funds at risk in the default configuration. The agent simulates every trade in memory until you explicitly enable live execution — and even then it refuses to touch mainnet without a separate acknowledgment env var. See Modes.


Quick start

# 1. Install (one-time)
npm install

# 2. Run the agent in PAPER mode (default — no network calls to Drift, no wallet needed)
npm start

The dashboard is at http://localhost:3000. The agent loop logs to stdout.

To stop the agent: write a KILL flag file or hit the Kill button on the dashboard.

npm run kill    # macOS / Linux: touches ./KILL
npm run unkill  # remove the flag and resume on the next loop tick

Modes

The agent has four reachable modes, each requiring strictly more env-var consent than the last. The default is Paper-only.

Mode PAPER_TRADING PERP_DATA_SOURCE PERP_LIVE_EXECUTION PERP_DRY_RUN PERP_DRIFT_ENV What happens
Paper-only (default) true jupiter false Sim runs against Jupiter spot price + a static funding constant. Zero contact with Drift. No wallet needed.
Paper + Drift reads true drift false devnet Sim runs against real Drift mark + Drift's hourly funding rate. Read-only Drift connection — never sends a tx. Needs the perp keypair to subscribe.
Dry-run false drift true true devnet Builds every order tx including SL+TP brackets, passes every safety gate, but never broadcasts. Synthetic tx sigs only. Useful for end-to-end validation against a real Drift connection without sending anything.
Live (devnet) false drift true false devnet Real txs broadcast to Drift devnet. Devnet has no real-world value — devnet USDC and SOL are free play money — but the txs are real.
Live (mainnet) false drift true false mainnet-beta + I_UNDERSTAND_MAINNET=true Real money. Refuses to start without the explicit acknowledgment.

You can flip any of these by editing .env (see .env.example).

Why separate PERP_LIVE_EXECUTION from PAPER_TRADING?

Defense in depth. Setting PAPER_TRADING=false is something you might do for many reasons (LP, spot if it existed, integration testing). Sending real perp transactions requires also setting PERP_LIVE_EXECUTION=true. Two switches, two intentional decisions.


Bootstrap (only needed for Drift modes)

If you stay in Paper-only mode you do not need any of this. Skip to Run the tests.

For Paper+Drift, Dry-run, or Live modes, the agent needs a dedicated perp wallet and (for live) a Drift user account.

# 1. Generate a fresh perp keypair (creates wallet-perp-devnet.json — DEVNET ONLY)
npx ts-node scripts/generatePerpWallet.ts

# 2. (Live only) Airdrop devnet SOL for tx fees
solana airdrop 2 <pubkey-from-step-1> --url devnet
# or use https://faucet.solana.com

# 3. (Live only) Initialize the Drift user account on devnet
npx ts-node scripts/setupDriftDevnet.ts

# 4. (Live only) Get devnet USDC and deposit it as collateral
#    via Drift's UI: https://app.drift.trade/?cluster=devnet

The keypair lives in wallet-perp-devnet.json (gitignored). Alternatively set PERP_WALLET_PRIVATE_KEY (base58) in .env.


Phase 3c smoke test

Before flipping live execution on, run the dry-run smoke test against real Drift devnet. It exercises the full open + bracket + close flow without broadcasting anything:

PERP_LIVE_EXECUTION=true PAPER_TRADING=false \
  npx ts-node scripts/smokeTestPerpExecutor.ts

(The --live flag actually sends devnet txs. Don't pass it until you're sure.)


Risk controls (enforced in code)

Control Where Notes
Max leverage perpRiskManager.check PERP_MAX_LEVERAGE (default 2x)
Max position size perpRiskManager PERP_MAX_POSITION_USD (default $200)
Mandatory stop-loss perpRiskManager + perpExecutor.openWithBracket Atomic open+SL+TP — never naked
Liquidation buffer perpRiskManager SL must be ≥PERP_LIQUIDATION_BUFFER_PCT (default 15%) of entry away from liquidation price
Funding-cost gate perpSimulator.checkFundingGate Auto-close if adverse funding exceeds PERP_FUNDING_CLOSE_THRESHOLD_HOURLY
Stale-oracle gate driftClient.getOracleStalenessMs Skip the tick if Drift's oracle hasn't updated in PERP_MAX_ORACLE_STALENESS_MS (default 30s)
Live-execution gate perpExecutor.checkLiveGate PERP_LIVE_EXECUTION=true AND !PAPER_TRADING AND (devnet OR mainnet-acknowledged)
Mainnet acknowledgment boot + checkLiveGate PERP_DRIFT_ENV=mainnet-beta requires I_UNDERSTAND_MAINNET=true
Dry-run override perpExecutor PERP_DRY_RUN=true builds every tx but never broadcasts — synthetic sigs
Wallet isolation loadPerpKeypair Perp agent uses its own keypair file. Do not share it with any other wallet.
Kill switch perpTrader.runLoop + killSwitchFlush Flag file ./KILL halts the loop. In live mode, also cancels orders + flattens position once per kill cycle.

Run the tests

npm test

Runs 14 test scripts (~2 minutes). Mocks every network dep — no Drift, Jupiter, Ollama, or Solana RPC calls during the suite.

npm run backtest:perp -- --days=90 --leverage=2

Backtests the strategy against cached CoinGecko data.


Configuration

See .env.example for every env var with default and inline doc.

The ones that matter most:

  • PAPER_TRADINGtrue is the safe default
  • PERP_DATA_SOURCEjupiter (default) for offline-friendly paper, drift for live data
  • PERP_LIVE_EXECUTION — required to send real txs
  • PERP_DRY_RUN — overrides live execution into a "build but never broadcast" mode
  • ENABLE_PERP — agent on/off (default true)

File map

src/
├── agents/perpTrader.ts        # the loop
├── skills/
│   ├── perpSimulator.ts        # paper accounting + auto-close logic
│   ├── perpStrategy.ts         # rule-based momentum decision
│   ├── perpComposite.ts        # rule + LLM blender
│   ├── perpReasonerLlm.ts      # Ollama wrapper
│   ├── perpRiskManager.ts      # pre-trade hard gate
│   ├── perpExecutor.ts         # Drift live-trade surface (or dry-run)
│   ├── driftClient.ts          # @drift-labs/sdk wrapper
│   ├── marketDataFetcher.ts    # Jupiter price (paper-mode fallback)
│   ├── priceHistory.ts         # rolling pct-change window
│   ├── killSwitch.ts           # ./KILL flag check
│   ├── wallet.ts               # perp keypair loader
│   ├── retry.ts                # backoff for transient HTTP errors
│   ├── historicalData.ts       # CoinGecko cache for backtests
│   └── logger.ts               # console + Telegram
├── config.ts                   # env → AgentConfig
├── types.ts                    # type defs
├── state.ts                    # dashboard event bus
├── server.ts                   # Express dashboard server
└── index.ts                    # boot
scripts/
├── runAllTests.ts              # `npm test`
├── backtestPerp.ts             # `npm run backtest:perp`
├── generatePerpWallet.ts       # creates wallet-perp-devnet.json
├── setupDriftDevnet.ts         # one-time Drift account init
├── smokeTestDrift.ts           # read-only Drift devnet smoke test
├── smokeTestPerpExecutor.ts    # Phase 3c dry-run/live executor smoke test
├── postinstallDriftStub.js     # Windows shim for helius-laserstream
└── test*.ts, _*.ts             # the 14 test scripts + helpers
public/                          # dashboard frontend (Chart.js + vanilla JS)
PLAN.md                          # design doc (read this for the why)

Known gaps

Tracked in PLAN.md §5 (phases) and §11 (TODOs):

  • Phase 3c — dry-run script is built; the --live round-trip on devnet is unrun.
  • Phase 4 — operational soak (≥1 week on devnet) hasn't happened.
  • Tx confirmationplaceOrders returns a sig but the executor doesn't await connection.confirmTransaction. The next loop's reconcile catches the failure mode but it's reactive.
  • Compute budget / priority fees — not configured. May matter on busy mainnet.
  • Retry on Drift writes — transient RPC failures during open propagate as exceptions; loop's outer catch logs and skips. Fine for now.
  • Real-fill price reconcile — sim records entryPrice = oraclePrice at decision time; Drift's auction may fill at a different price. Phase 3c will surface how big the gap is.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages