Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions BETA.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ You're ready when the frontend logs `Starting nginx`.

## 4. Your first bot (5 minutes, no exchange account needed)

1. Go to **Algorithms** and click **Load example strategy** (or *Import* and pick a file from the `examples/` folder in the repo — `Supertrend_Trend_1d.apex.json` is a good start).
1. Go to **Algorithms** and click **Load example strategy** (or *Import* and pick a file from the `examples/` folder in the repo — `Supertrend_Trend_1d.apex.json` is a good start; `Supertrend_LongShort_Perp_1d.apex.json` is the long/short perpetual version).
2. Leave the bot in **Paper** mode with *Run Historical Backtest* enabled.
3. Click **Start**. The engine downloads historical candles (a few minutes the first time) and runs a full backtest. Open the **Console** on the bot card to watch it work.
4. Explore the results in **Trades** (equity curve, drawdown, positions) and on the **Chart** (buy/sell markers).
Expand Down Expand Up @@ -77,7 +77,20 @@ The repo contains `STRATEGY_CONTEXT.md`. Paste that file into any capable AI ass
- **`Max order value` is mandatory for live bots** — the app refuses to start a live bot without this hard cap per order. Set it low.
- Set **Max drawdown** (e.g. 10–15%): by default the bot then automatically closes its positions and stops if the equity curve drops that far from its peak. You can switch **On max drawdown** to *Block new entries* instead (exits keep working, no forced liquidation) — if you do, also set **Max capital loss %** as the hard stop, since blocking entries alone does not cap losses on open positions.
- Fill in your exchange's real **fee** (e.g. 0.1%) in the trade settings — backtests without fees are misleadingly optimistic.
- After any backend restart the bot reconciles its open positions with your exchange balances before going live and stops with an error if they don't match — still glance at the exchange yourself before letting it continue.
- After any backend restart the bot reconciles its open positions with your exchange balances (or, on perpetual swaps, the exchange's position list) before going live and stops with an error if they don't match or cannot be checked — still glance at the exchange yourself before letting it continue.
- **`Max order value` is a cap on the quote notional** (e.g. USDT for `BTC/USDT`), not on the margin you put up — at 5× leverage a 500 cap lets the bot risk a 100 margin. It applies in backtest and forward test too, so changing it counts as a new strategy variant.

### Perpetual swaps: known limitations of the simulation

Backtest and forward test model leverage, margin, fees, slippage, liquidation, funding and the exchange's maintenance-margin tiers, with these caveats:

- **Funding history** — the bot fetches the funding-rate history when it starts and stores it next to the candles, but exchanges only serve a limited window (OKX ≈ 3 months, Binance years). Older parts of a backtest run without funding; the console and the summary (`funding: simulated | partial | no data`) say so. Paper/live positions are charged by the exchange itself and not booked by ApexAlgo.
- **Maintenance-margin tiers** — fetched once a week per pair; without them (exchange unreachable, no tier endpoint) the flat 0.5% applies (`mmr_source` in the summary).
- **Cross margin** — simulated as one account: `backtest_capital` is the whole margin wallet, a breach liquidates every open position of the bot and empties that pool (`profit_pct` can go below −100). On the exchange the *real* wallet — including balances of other bots and keys — is what gets liquidated.
- **Spot margin / borrowing** — not supported; spot bots only ever spend the cash they hold.
- **Hedge mode** — not supported; keep the account in one-way position mode. The bot never holds a long and a short on the same pair at once, and reconciliation nets both sides into one number, so hedged positions would be misread.

Whatever you do on the exchange yourself (manual trades, changing the leverage, switching position mode) is invisible to the bot until the next restart.

### If you suspect a leak or anything weird

Expand Down
94 changes: 68 additions & 26 deletions README.md

Large diffs are not rendered by default.

208 changes: 186 additions & 22 deletions STRATEGY_CONTEXT.md

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions backend/core/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,64 @@ def run_migrations():
if 'exchange' not in pos_cols:
conn.execute(text("ALTER TABLE positions ADD COLUMN exchange TEXT DEFAULT 'okx'"))
logger.info("Migration: added 'exchange' to positions")
# Derivatives support: existing rows are spot at 1x
if 'market_type' not in pos_cols:
conn.execute(text("ALTER TABLE positions ADD COLUMN market_type TEXT DEFAULT 'spot'"))
logger.info("Migration: added 'market_type' to positions")
if 'leverage' not in pos_cols:
conn.execute(text("ALTER TABLE positions ADD COLUMN leverage REAL DEFAULT 1"))
logger.info("Migration: added 'leverage' to positions")
if 'contracts' not in pos_cols:
conn.execute(text("ALTER TABLE positions ADD COLUMN contracts REAL"))
logger.info("Migration: added 'contracts' to positions")
# Currency-aware books: the currency the position is funded in
# and the contract kind/size its `amount` is expressed in
_backfill_ccy = 'cash_currency' not in pos_cols
if _backfill_ccy:
conn.execute(text("ALTER TABLE positions ADD COLUMN cash_currency TEXT"))
logger.info("Migration: added 'cash_currency' to positions")
if 'contract_kind' not in pos_cols:
conn.execute(text("ALTER TABLE positions ADD COLUMN contract_kind TEXT"))
logger.info("Migration: added 'contract_kind' to positions")
if 'contract_size' not in pos_cols:
conn.execute(text("ALTER TABLE positions ADD COLUMN contract_size REAL"))
logger.info("Migration: added 'contract_size' to positions")
if 'funding_paid' not in pos_cols:
conn.execute(text("ALTER TABLE positions ADD COLUMN funding_paid REAL"))
logger.info("Migration: added 'funding_paid' to positions")
if 'funding_until' not in pos_cols:
conn.execute(text("ALTER TABLE positions ADD COLUMN funding_until DATETIME"))
logger.info("Migration: added 'funding_until' to positions")
_backfill_position_currency(conn)

# ── orders ───────────────────────────────────────────────────────────────
if "orders" in existing_tables:
ord_cols = {c['name'] for c in inspector.get_columns('orders')}
if 'exchange' not in ord_cols:
conn.execute(text("ALTER TABLE orders ADD COLUMN exchange TEXT DEFAULT 'okx'"))
logger.info("Migration: added 'exchange' to orders")
if 'market_type' not in ord_cols:
conn.execute(text("ALTER TABLE orders ADD COLUMN market_type TEXT DEFAULT 'spot'"))
logger.info("Migration: added 'market_type' to orders")
if 'reduce_only' not in ord_cols:
conn.execute(text("ALTER TABLE orders ADD COLUMN reduce_only INTEGER DEFAULT 0"))
logger.info("Migration: added 'reduce_only' to orders")
# `fee` stays the fee in the position's cash currency (what the
# books use); `fee_currency`/`fee_cash` record what the exchange
# actually charged, when known
if 'fee_currency' not in ord_cols:
conn.execute(text("ALTER TABLE orders ADD COLUMN fee_currency TEXT"))
logger.info("Migration: added 'fee_currency' to orders")
if 'fee_cash' not in ord_cols:
conn.execute(text("ALTER TABLE orders ADD COLUMN fee_cash REAL"))
logger.info("Migration: added 'fee_cash' to orders")

# ── exchange_keys ────────────────────────────────────────────────────────
if "exchange_keys" in existing_tables:
key_cols = {c['name'] for c in inspector.get_columns('exchange_keys')}
if 'market_type' not in key_cols:
conn.execute(text("ALTER TABLE exchange_keys ADD COLUMN market_type TEXT DEFAULT 'spot'"))
logger.info("Migration: added 'market_type' to exchange_keys")

# ── candles ──────────────────────────────────────────────────────────────
# The unique constraint must include 'exchange'. SQLite cannot alter
Expand Down Expand Up @@ -190,6 +241,28 @@ def run_migrations():
_sweep_orphaned_bot_rows(conn, existing_tables)


def _backfill_position_currency(conn) -> None:
"""Fill `positions.cash_currency` / `contract_kind` from the symbol for
rows that predate the columns (spot → quote, ``:SETTLE`` → settle;
``BTC/USD:BTC`` → inverse). Idempotent: only NULL rows are touched."""
from backend.engine.contracts import spec_from_symbol
rows = conn.execute(text(
"SELECT DISTINCT symbol FROM positions WHERE symbol IS NOT NULL AND (cash_currency IS NULL OR contract_kind IS NULL)"
)).fetchall()
for (symbol,) in rows:
try:
spec = spec_from_symbol(symbol)
except Exception:
continue
conn.execute(text(
"UPDATE positions SET cash_currency = COALESCE(cash_currency, :ccy), contract_kind = COALESCE(contract_kind, :kind) "
"WHERE symbol = :sym AND (cash_currency IS NULL OR contract_kind IS NULL)"
), {"ccy": spec.cash_currency, "kind": spec.kind, "sym": symbol})
if rows:
conn.commit()
logger.info("Migration: backfilled cash_currency/contract_kind on positions for %d symbol(s)", len(rows))


def _sweep_orphaned_bot_rows(conn, existing_tables) -> None:
"""Delete per-bot rows whose bot no longer exists.

Expand Down
Loading
Loading