Crypto trading bot: engine, brokers, backtester, dashboard, deploy
FastAPI backend with a single-strategy async engine, paper and Binance brokers, pure-python indicators, a backtester that shares the engine's portfolio math, and a Vue dashboard with a live WebSocket feed. Adds the Gitea Actions CI/CD workflow that deploys to Traefik via docker_default.
This commit is contained in:
1 parent
98028248c2
commit
9c3bee87ac
73 files changed
+9759
-1
No files matched your search
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from app.schemas import BacktestMetricsOut, BacktestPointOut, BacktestTradeOut
|
||||
|
||||
# Bars per year for annualising the Sharpe ratio. Unknown timeframes -> None.
|
||||
BARS_PER_YEAR: dict[str, float] = {
|
||||
"1m": 525_600,
|
||||
"3m": 175_200,
|
||||
"5m": 105_120,
|
||||
"15m": 35_040,
|
||||
"30m": 17_520,
|
||||
"1h": 8_760,
|
||||
"2h": 4_380,
|
||||
"4h": 2_190,
|
||||
"6h": 1_460,
|
||||
"12h": 730,
|
||||
"1d": 365,
|
||||
"3d": 121,
|
||||
"1w": 52,
|
||||
}
|
||||
|
||||
|
||||
def max_drawdown_pct(equities: Sequence[float]) -> float:
|
||||
peak = equities[0] if equities else 0.0
|
||||
worst = 0.0
|
||||
for value in equities:
|
||||
peak = max(peak, value)
|
||||
if peak > 0:
|
||||
worst = max(worst, (peak - value) / peak)
|
||||
return worst * 100
|
||||
|
||||
|
||||
def sharpe_ratio(equities: Sequence[float], timeframe: str) -> float | None:
|
||||
if len(equities) < 3:
|
||||
return None
|
||||
returns = [
|
||||
equities[i] / equities[i - 1] - 1 for i in range(1, len(equities)) if equities[i - 1] > 0
|
||||
]
|
||||
if len(returns) < 2:
|
||||
return None
|
||||
mean = sum(returns) / len(returns)
|
||||
variance = sum((r - mean) ** 2 for r in returns) / (len(returns) - 1)
|
||||
std = variance**0.5
|
||||
periods = BARS_PER_YEAR.get(timeframe)
|
||||
if std == 0 or not periods:
|
||||
return None
|
||||
return mean / std * (periods**0.5)
|
||||
|
||||
|
||||
def compute_metrics(
|
||||
curve: Sequence[BacktestPointOut],
|
||||
trades: Sequence[BacktestTradeOut],
|
||||
starting_balance: float,
|
||||
timeframe: str,
|
||||
) -> BacktestMetricsOut:
|
||||
equities = [point.equity for point in curve] or [starting_balance]
|
||||
final_equity = equities[-1]
|
||||
total_return = (final_equity / starting_balance - 1) * 100 if starting_balance else 0.0
|
||||
buy_hold = (
|
||||
(curve[-1].buy_hold / starting_balance - 1) * 100 if curve and starting_balance else 0.0
|
||||
)
|
||||
|
||||
returns = [trade.return_pct for trade in trades]
|
||||
gross_profit = sum(trade.pnl for trade in trades if trade.pnl > 0)
|
||||
gross_loss = -sum(trade.pnl for trade in trades if trade.pnl < 0)
|
||||
wins = sum(1 for trade in trades if trade.pnl > 0)
|
||||
|
||||
return BacktestMetricsOut(
|
||||
initial_equity=starting_balance,
|
||||
final_equity=final_equity,
|
||||
total_return_pct=total_return,
|
||||
buy_hold_return_pct=buy_hold,
|
||||
max_drawdown_pct=max_drawdown_pct(equities),
|
||||
num_trades=len(trades),
|
||||
win_rate_pct=(wins / len(trades) * 100) if trades else 0.0,
|
||||
profit_factor=(gross_profit / gross_loss) if gross_loss > 0 else None,
|
||||
avg_trade_pct=(sum(returns) / len(returns)) if returns else 0.0,
|
||||
best_trade_pct=max(returns) if returns else 0.0,
|
||||
worst_trade_pct=min(returns) if returns else 0.0,
|
||||
sharpe=sharpe_ratio(equities, timeframe),
|
||||
)
|
||||
Reference in new issue
Block a user