major agent refactoring: wiki knowledge base, no RAG, no Qdrant, no Ollama
This commit is contained in:
@@ -1,188 +1,262 @@
|
||||
# Strategy Development Guide
|
||||
|
||||
Trading strategies in Dexorder define entry/exit rules and position management logic.
|
||||
Strategies on Dexorder are `PandasStrategy` subclasses that receive a live stream of OHLCV bars and call `self.buy()` / `self.sell()` / `self.flatten()` to place orders.
|
||||
|
||||
## Strategy Structure
|
||||
See [`../api-reference.md`](../api-reference.md) for the DataAPI and ChartingAPI used in research scripts. For indicator calculations, see [`../pandas-ta-reference.md`](../pandas-ta-reference.md).
|
||||
|
||||
---
|
||||
|
||||
## PandasStrategy API
|
||||
|
||||
```python
|
||||
class Strategy:
|
||||
def __init__(self, **params):
|
||||
"""Initialize strategy with parameters"""
|
||||
self.params = params
|
||||
from dexorder.nautilus.pandas_strategy import PandasStrategy, PandasStrategyConfig
|
||||
|
||||
def generate_signals(self, df):
|
||||
"""
|
||||
Generate trading signals
|
||||
class MyStrategy(PandasStrategy):
|
||||
|
||||
def evaluate(self, dfs: dict[str, pd.DataFrame]) -> None:
|
||||
"""Called after every new bar across all feeds.
|
||||
|
||||
Args:
|
||||
df: DataFrame with OHLCV + indicator columns
|
||||
|
||||
Returns:
|
||||
DataFrame with 'signal' column:
|
||||
1 = long entry
|
||||
-1 = short entry
|
||||
0 = no action
|
||||
dfs: dict mapping feed_key → pd.DataFrame
|
||||
Columns: timestamp (ns), open, high, low, close, volume,
|
||||
buy_vol, sell_vol, open_interest
|
||||
Rows accumulate over time — last row = latest bar.
|
||||
"""
|
||||
pass
|
||||
df = dfs.get("BTC/USDT.BINANCE:300")
|
||||
if df is None or len(df) < 20:
|
||||
return # not enough data yet
|
||||
|
||||
def calculate_position_size(self, capital, price, risk_pct):
|
||||
"""Calculate position size based on risk"""
|
||||
pass
|
||||
close = df["close"]
|
||||
# ... compute signals ...
|
||||
|
||||
def get_stop_loss(self, entry_price, direction):
|
||||
"""Calculate stop loss level"""
|
||||
pass
|
||||
|
||||
def get_take_profit(self, entry_price, direction):
|
||||
"""Calculate take profit level"""
|
||||
pass
|
||||
if buy_signal:
|
||||
self.buy(quantity=0.1)
|
||||
elif sell_signal:
|
||||
self.sell(quantity=0.1)
|
||||
```
|
||||
|
||||
## Example: Simple Moving Average Crossover
|
||||
### Feed key format
|
||||
|
||||
`"{SYMBOL.EXCHANGE}:{period_seconds}"` — e.g. `"BTC/USDT.BINANCE:900"` for 15-minute bars.
|
||||
|
||||
Access all feeds via `self.config.feed_keys` (tuple of strings).
|
||||
|
||||
### Order methods
|
||||
|
||||
```python
|
||||
class SMACrossoverStrategy:
|
||||
def __init__(self, fast_period=20, slow_period=50, risk_pct=0.02):
|
||||
self.fast_period = fast_period
|
||||
self.slow_period = slow_period
|
||||
self.risk_pct = risk_pct
|
||||
|
||||
def generate_signals(self, df):
|
||||
# Calculate moving averages
|
||||
df['sma_fast'] = df['close'].rolling(self.fast_period).mean()
|
||||
df['sma_slow'] = df['close'].rolling(self.slow_period).mean()
|
||||
|
||||
# Generate signals
|
||||
df['signal'] = 0
|
||||
|
||||
# Long when fast crosses above slow
|
||||
df.loc[
|
||||
(df['sma_fast'] > df['sma_slow']) &
|
||||
(df['sma_fast'].shift(1) <= df['sma_slow'].shift(1)),
|
||||
'signal'
|
||||
] = 1
|
||||
|
||||
# Short when fast crosses below slow
|
||||
df.loc[
|
||||
(df['sma_fast'] < df['sma_slow']) &
|
||||
(df['sma_fast'].shift(1) >= df['sma_slow'].shift(1)),
|
||||
'signal'
|
||||
] = -1
|
||||
|
||||
return df
|
||||
|
||||
def calculate_position_size(self, capital, price, atr):
|
||||
# Risk-based position sizing
|
||||
risk_amount = capital * self.risk_pct
|
||||
stop_distance = 2 * atr
|
||||
position_size = risk_amount / stop_distance
|
||||
return position_size
|
||||
|
||||
def get_stop_loss(self, entry_price, direction, atr):
|
||||
if direction == 1: # Long
|
||||
return entry_price - (2 * atr)
|
||||
else: # Short
|
||||
return entry_price + (2 * atr)
|
||||
|
||||
def get_take_profit(self, entry_price, direction, atr):
|
||||
if direction == 1: # Long
|
||||
return entry_price + (4 * atr) # 2:1 risk/reward
|
||||
else: # Short
|
||||
return entry_price - (4 * atr)
|
||||
self.buy(quantity: float, feed_key: str = None)
|
||||
self.sell(quantity: float, feed_key: str = None)
|
||||
self.flatten(feed_key: str = None) # close all open positions
|
||||
```
|
||||
|
||||
## Strategy Components
|
||||
If `feed_key` is omitted, the first feed in `feed_keys` is used. `quantity` is in base currency units (e.g. 0.1 BTC).
|
||||
|
||||
### Signal Generation
|
||||
Entry conditions based on:
|
||||
- Indicator crossovers
|
||||
- Price patterns
|
||||
- Volume confirmation
|
||||
- Multiple timeframe confluence
|
||||
### Available data
|
||||
|
||||
### Risk Management
|
||||
Essential elements:
|
||||
- **Position Sizing**: Based on account risk percentage
|
||||
- **Stop Losses**: ATR-based or support/resistance
|
||||
- **Take Profits**: Multiple targets or trailing stops
|
||||
- **Max Positions**: Limit concurrent trades
|
||||
Strategies may only use data in the `dfs` feeds: crypto OHLCV + buy/sell volume split + open interest. The following are **not available**:
|
||||
- TradFi data (equities, forex, bonds, options, macro indicators)
|
||||
- Alternative data (news, social sentiment, on-chain metrics, economic calendars)
|
||||
|
||||
### Filters
|
||||
Reduce false signals:
|
||||
- **Trend Filter**: Only trade with the trend
|
||||
- **Volatility Filter**: Avoid low volatility periods
|
||||
- **Time Filter**: Specific trading hours
|
||||
- **Volume Filter**: Minimum volume requirements
|
||||
---
|
||||
|
||||
### Exit Rules
|
||||
Multiple exit types:
|
||||
- **Stop Loss**: Protect capital
|
||||
- **Take Profit**: Lock in gains
|
||||
- **Trailing Stop**: Follow profitable moves
|
||||
- **Time Exit**: Close at end of period
|
||||
- **Signal Exit**: Opposite signal
|
||||
## Using pandas_ta
|
||||
|
||||
## Backtesting Considerations
|
||||
Use `import pandas_ta as ta` for all indicator calculations. Never write manual `rolling()` or `ewm()` implementations.
|
||||
|
||||
### Data Quality
|
||||
- Use clean, validated data
|
||||
- Handle missing data appropriately
|
||||
- Account for survivorship bias
|
||||
- Include realistic spreads and slippage
|
||||
```python
|
||||
import pandas_ta as ta
|
||||
|
||||
### Performance Metrics
|
||||
Track key metrics:
|
||||
- **Total Return**: Cumulative profit/loss
|
||||
- **Sharpe Ratio**: Risk-adjusted returns
|
||||
- **Max Drawdown**: Largest peak-to-trough decline
|
||||
- **Win Rate**: Percentage of profitable trades
|
||||
- **Profit Factor**: Gross profit / gross loss
|
||||
- **Expectancy**: Average $ per trade
|
||||
rsi = ta.rsi(df["close"], length=14)
|
||||
macd_df = ta.macd(df["close"], fast=12, slow=26, signal=9)
|
||||
hist = macd_df.iloc[:, 2] # histogram column
|
||||
|
||||
### Validation
|
||||
Prevent overfitting:
|
||||
- **Train/Test Split**: 70/30 or 60/40
|
||||
- **Walk-Forward**: Rolling windows
|
||||
- **Out-of-Sample**: Test on recent unseen data
|
||||
- **Monte Carlo**: Randomize trade order
|
||||
- **Paper Trading**: Live validation
|
||||
ema = ta.ema(df["close"], length=20)
|
||||
atr = ta.atr(df["high"], df["low"], df["close"], length=14)
|
||||
```
|
||||
|
||||
## Common Strategy Types
|
||||
See [`../pandas-ta-reference.md`](../pandas-ta-reference.md) for the full indicator catalog and multi-output column extraction patterns.
|
||||
|
||||
### Trend Following
|
||||
Follow sustained price movements:
|
||||
- Moving average crossovers
|
||||
- Breakout strategies
|
||||
- Trend channels
|
||||
- Works best in trending markets
|
||||
---
|
||||
|
||||
### Mean Reversion
|
||||
Profit from price returning to average:
|
||||
- Bollinger Band reversals
|
||||
- RSI extremes
|
||||
- Statistical arbitrage
|
||||
- Works best in ranging markets
|
||||
## Using Custom Indicators
|
||||
|
||||
### Momentum
|
||||
Trade in direction of strong moves:
|
||||
- Relative strength
|
||||
- Price acceleration
|
||||
- Volume surges
|
||||
- Breakout confirmation
|
||||
Prefer referencing a custom indicator that already exists in the `indicator` category rather than duplicating the logic inline. Custom indicators appear on the user's chart, making the signal transparent.
|
||||
|
||||
### Arbitrage
|
||||
Exploit price discrepancies:
|
||||
- Cross-exchange spreads
|
||||
- Funding rate arbitrage
|
||||
- Statistical pairs trading
|
||||
- Requires low latency
|
||||
```python
|
||||
import pandas_ta as ta
|
||||
|
||||
## Integration with Platform
|
||||
def evaluate(self, dfs):
|
||||
df = dfs.get("BTC/USDT.BINANCE:3600")
|
||||
if df is None or len(df) < 20:
|
||||
return
|
||||
|
||||
Store strategies in your git repository under `strategies/` directory.
|
||||
vw_rsi = ta.custom_vw_rsi(df["close"], df["volume"], length=14)
|
||||
if vw_rsi is None or vw_rsi.isna().all():
|
||||
return
|
||||
|
||||
Test using the backtesting tools provided by the platform.
|
||||
if vw_rsi.iloc[-1] < 30:
|
||||
self.buy(0.01)
|
||||
elif vw_rsi.iloc[-1] > 70:
|
||||
self.sell(0.01)
|
||||
```
|
||||
|
||||
Deploy live strategies through the execution engine with proper risk controls.
|
||||
Custom indicator names follow the pattern `ta.custom_{sanitized_name}`. See [`../indicators/indicator-development.md`](../indicators/indicator-development.md) for naming rules and how to create custom indicators.
|
||||
|
||||
Monitor performance and adjust parameters as market conditions change.
|
||||
---
|
||||
|
||||
## Strategy Metadata
|
||||
|
||||
When writing a strategy with `PythonWrite(category="strategy", ...)`, always provide:
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `description` | yes | One-sentence summary |
|
||||
| `details` | yes | Full markdown: algorithm, entry/exit logic, parameters, data feeds, position sizing. Enough detail to reproduce the code from scratch. |
|
||||
|
||||
```python
|
||||
PythonWrite(
|
||||
category="strategy",
|
||||
name="RSI Mean Reversion",
|
||||
description="Buy oversold, sell overbought based on RSI(14) on BTC/USDT 5m bars.",
|
||||
details="""## RSI Mean Reversion
|
||||
...""",
|
||||
code="""...""",
|
||||
metadata={
|
||||
"data_feeds": [
|
||||
{"symbol": "BTC/USDT.BINANCE", "period_seconds": 300, "description": "BTC/USDT 5m"}
|
||||
],
|
||||
"parameters": {
|
||||
"rsi_length": {"default": 14, "description": "RSI lookback period"},
|
||||
"oversold": {"default": 30, "description": "Buy threshold"},
|
||||
"overbought": {"default": 70, "description": "Sell threshold"},
|
||||
"trade_qty": {"default": 0.01, "description": "Trade quantity in BTC"}
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backtest Workflow
|
||||
|
||||
1. **Check existing indicators** first: `PythonList(category="indicator")` — reuse signals already on the chart.
|
||||
2. **Write** the strategy: `PythonWrite(...)` — runs against synthetic data automatically.
|
||||
3. **Run a backtest** targeting 100,000–200,000 bars (max 5 years):
|
||||
```
|
||||
BacktestStrategy(
|
||||
strategy_name="RSI Mean Reversion",
|
||||
feeds=[{"symbol": "BTC/USDT.BINANCE", "period_seconds": 900}],
|
||||
from_time="2023-01-01",
|
||||
to_time="2024-12-31",
|
||||
initial_capital=10000
|
||||
)
|
||||
```
|
||||
4. **Interpret results**:
|
||||
- `summary.total_return` — total fractional return (0.15 = +15%)
|
||||
- `summary.sharpe_ratio` — annualized Sharpe (>1.0 good, >2.0 excellent)
|
||||
- `summary.max_drawdown` — maximum peak-to-trough loss
|
||||
- `summary.win_rate` — fraction of profitable trades
|
||||
- `statistics.profit_factor` — gross profit / gross loss (>1.5 good)
|
||||
5. **Iterate** with `PythonEdit`, re-run backtest.
|
||||
6. **Activate** (paper first): `ActivateStrategy(..., paper=True)`
|
||||
|
||||
### Bar resolution and backtest window
|
||||
|
||||
Choose the resolution appropriate to the strategy's signal frequency, then set the date range to hit 100k–200k bars:
|
||||
|
||||
| Resolution | ~100k bars | ~200k bars |
|
||||
|---|---|---|
|
||||
| 5m | 1 year | 2 years |
|
||||
| 15m | 2.9 years | 5 years |
|
||||
| 1h | cap at 5 yr (≈44k bars) | — |
|
||||
| 4h | cap at 5 yr (≈11k bars) | — |
|
||||
|
||||
---
|
||||
|
||||
## Strategy Patterns
|
||||
|
||||
### Trend following
|
||||
|
||||
Follow sustained price movements using moving average crossovers, breakout of price channels, or trend-direction filters:
|
||||
|
||||
```python
|
||||
ema_fast = ta.ema(df["close"], length=20)
|
||||
ema_slow = ta.ema(df["close"], length=50)
|
||||
bullish = ema_fast.iloc[-1] > ema_slow.iloc[-1]
|
||||
crossover = ema_fast.iloc[-2] <= ema_slow.iloc[-2]
|
||||
|
||||
if bullish and crossover:
|
||||
self.buy(qty)
|
||||
```
|
||||
|
||||
### Mean reversion
|
||||
|
||||
Profit from price returning to an average after extremes:
|
||||
|
||||
```python
|
||||
rsi = ta.rsi(df["close"], length=14)
|
||||
if rsi.iloc[-1] < 30:
|
||||
self.buy(qty)
|
||||
elif rsi.iloc[-1] > 70:
|
||||
self.sell(qty)
|
||||
```
|
||||
|
||||
### Multi-timeframe confluence
|
||||
|
||||
Use a higher-timeframe trend filter with a lower-timeframe entry signal:
|
||||
|
||||
```python
|
||||
df_4h = dfs.get("BTC/USDT.BINANCE:14400")
|
||||
df_15m = dfs.get("BTC/USDT.BINANCE:900")
|
||||
if df_4h is None or df_15m is None:
|
||||
return
|
||||
|
||||
ema_4h = ta.ema(df_4h["close"], length=20)
|
||||
bullish_trend = df_4h["close"].iloc[-1] > ema_4h.iloc[-1]
|
||||
|
||||
macd_df = ta.macd(df_15m["close"])
|
||||
hist = macd_df.iloc[:, 2]
|
||||
|
||||
if bullish_trend and hist.iloc[-1] > 0 and hist.iloc[-2] <= 0:
|
||||
self.buy(qty, feed_key="BTC/USDT.BINANCE:900")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Important Rules
|
||||
|
||||
- **`evaluate()` must be fast, lightweight, and deterministic** — no model inference, file I/O, network calls, or randomness. It runs on every bar during backtests over potentially hundreds of thousands of bars.
|
||||
- **No LLM calls inside strategies** — strategies must be fully reproducible.
|
||||
- **Guard for insufficient data** — always check `len(df) >= min_required` before computing indicators with a lookback period.
|
||||
- **Use `.get()` for feeds** — multi-feed strategies may have feeds missing during warm-up.
|
||||
- **Size conservatively** — a typical trade quantity is `0.001–0.01 * initial_capital / price`.
|
||||
- **No `import` from `dexorder` inside `evaluate()`** — the strategy file is exec'd in a sandbox; PandasStrategy and pandas_ta are pre-loaded.
|
||||
|
||||
---
|
||||
|
||||
## Performance Metrics Reference
|
||||
|
||||
| Metric | Good | Excellent |
|
||||
|---|---|---|
|
||||
| Sharpe ratio | > 1.0 | > 2.0 |
|
||||
| Profit factor | > 1.5 | > 2.0 |
|
||||
| Max drawdown | < 20% | < 10% |
|
||||
| Win rate | context-dependent | — |
|
||||
|
||||
A strategy with a lower win rate can still be profitable if winners are larger than losers (profit factor > 1). Focus on Sharpe and max drawdown as primary quality metrics.
|
||||
|
||||
### Avoiding overfitting
|
||||
|
||||
- Do not optimize parameters on the same data used for validation
|
||||
- Use a held-out out-of-sample period to verify results
|
||||
- Prefer fewer parameters — simpler strategies generalize better
|
||||
- Walk-forward analysis: re-fit on a rolling window, evaluate on the next
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [`../pandas-ta-reference.md`](../pandas-ta-reference.md) — Indicator catalog and usage examples
|
||||
- [`../indicators/indicator-development.md`](../indicators/indicator-development.md) — Creating custom indicators
|
||||
- [`../api-reference.md`](../api-reference.md) — DataAPI reference (for research scripts)
|
||||
- [`../usage-examples.md`](../usage-examples.md) — Research script patterns
|
||||
|
||||
Reference in New Issue
Block a user