mirror of
https://github.com/chrisnov-it/quantumbotx.git
synced 2026-07-29 03:37:45 +00:00
feat(backtesting, strategies, ui): Enhance backtesting, add new strategies, and improve UI/UX
This commit introduces significant improvements across the application, focusing on a robust backtesting experience, new trading strategies, and enhanced user interface. Backtesting Module: - Implemented comprehensive backtest history functionality, including detailed metrics, equity curve, parameters, and trade logs. - Resolved `NOT NULL` constraint errors for `wins` and `losses` by updating DB schema and `init_db.py` and `save_backtest_result` logic. - Consolidated `/api/backtest/history` route to `api_backtest.py`, removing duplication from `api_history.py`. - Ensured `value_per_pip` calculation in `engine.py` is accurate for all symbols (especially XAU/XAG). - Removed unused `profit` variable in `engine.py`. - Deleted redundant `engine.py` file from project root. Trading Strategies: - **Mercy Edge**: Activated and synchronized `analyze` (live) and `analyze_df` (backtest) methods, using SMA 200 as trend filter. - **Pulse Sync**: Re-implemented as a distinct strategy (RSI Crossover with SMA 100 trend filter), providing a more responsive alternative to Mercy Edge. - **RSI Breakout (now RSI Crossover)**: Transformed into a powerful RSI-MA Crossover strategy with SMA 50 trend filter, significantly improving performance on EURUSD and becoming a top performer on XAUUSD/USDJPY. - **Turtle Breakout**: Integrated as a new, classic trend-following strategy, fully functional for both live and backtesting. - Updated `strategy_map.py` to reflect new strategy names and additions (`BOLLINGER_REVERSION`, `RSI_CROSSOVER`, `TURTLE_BREAKOUT`). UI/UX Improvements: - Enhanced Backtest History UI to display strategy, market/pair, and all detailed metrics. - Updated `README.md` to reflect the new Backtester feature and streamlined donation section.
This commit is contained in:
@@ -13,6 +13,7 @@ Designed to be elegant, powerful, and flexible — whether you're a scalper, swi
|
||||
## 🚀 Features
|
||||
|
||||
- ✅ **Modular Strategy System**: Easily create and plug in your own trading strategies.
|
||||
- ✅ **Comprehensive Backtester**: Test your strategies against historical data with detailed performance metrics and visualizations.
|
||||
- ✅ **Real-Time Analysis**: Live dashboard with data visualization using Chart.js.
|
||||
- ✅ **Adaptive Logic**: Comes with a Hybrid strategy that adapts to trending or ranging markets.
|
||||
- ✅ **Automated Trading**: Full position handling (entry, exit, SL, TP) using bot-specific magic numbers.
|
||||
@@ -47,7 +48,6 @@ Designed to be elegant, powerful, and flexible — whether you're a scalper, swi
|
||||
## 📈 Roadmap
|
||||
|
||||
- [ ] **Advanced Strategy**: `MACD_STOCH_FILTER` for more precise, filtered entries.
|
||||
- [ ] **Backtesting Module**: A simple UI to test strategies against historical data.
|
||||
- [ ] **Telegram Notifications**: Get real-time alerts for trades and errors.
|
||||
- [ ] **Portfolio Analytics**: Deeper insights into your trading performance.
|
||||
|
||||
@@ -127,15 +127,10 @@ Concept, Logic & Execution: `@reynov` aka BabyDev
|
||||
|
||||
If you like this project, give it a ⭐ on GitHub, or buy me a coffee to support future versions:
|
||||
|
||||
// eslint-disable-next-line markdown/fenced-code-language
|
||||
```
|
||||
BTC Wallet: bc1qxxxxxxxxxxxxxx
|
||||
USDT TRC20: TRxxxxxxxxxxxx
|
||||
```
|
||||
[<img src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif" alt="Donate with PayPal" />](https://www.paypal.com/paypalme/rebarakaz)
|
||||
|
||||
---
|
||||
|
||||
## 📝 License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE.md file for details.
|
||||
```bash
|
||||
@@ -65,7 +65,6 @@ def run_backtest(strategy_id, params, historical_data_df):
|
||||
|
||||
# Cek SL/TP jika sedang dalam posisi
|
||||
if in_position:
|
||||
profit = 0
|
||||
if position_type == 'BUY':
|
||||
profit_pips = (current_price - entry_price) / pip_size
|
||||
if current_price <= entry_price - (sl_pips * pip_size):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# core/routes/api_backtest.py
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import json
|
||||
import logging
|
||||
@@ -11,10 +12,6 @@ from core.db.connection import get_db_connection
|
||||
api_backtest = Blueprint('api_backtest', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import numpy as np
|
||||
|
||||
# ... (kode lainnya)
|
||||
|
||||
def save_backtest_result(strategy_name, filename, params, results):
|
||||
# ... (kode di dalam fungsi)
|
||||
# Sanitasi data sebelum menyimpan
|
||||
@@ -28,8 +25,8 @@ def save_backtest_result(strategy_name, filename, params, results):
|
||||
cursor.execute("""
|
||||
INSERT INTO backtest_results (
|
||||
strategy_name, data_filename, total_profit_pips, total_trades,
|
||||
win_rate_percent, max_drawdown_percent, equity_curve, trade_log, parameters
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
win_rate_percent, max_drawdown_percent, wins, losses, equity_curve, trade_log, parameters
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
strategy_name,
|
||||
filename,
|
||||
@@ -37,6 +34,8 @@ def save_backtest_result(strategy_name, filename, params, results):
|
||||
results.get('total_trades', 0),
|
||||
results.get('win_rate_percent', 0),
|
||||
results.get('max_drawdown_percent', 0),
|
||||
results.get('wins', 0),
|
||||
results.get('losses', 0),
|
||||
json.dumps(results.get('equity_curve', [])),
|
||||
json.dumps(results.get('trades', [])),
|
||||
json.dumps(params)
|
||||
@@ -77,4 +76,4 @@ def get_history_route():
|
||||
history = get_all_backtest_history()
|
||||
return jsonify(history)
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Terjadi kesalahan saat mengambil riwayat: {str(e)}"}), 500
|
||||
return jsonify({"error": f"Terjadi kesalahan saat mengambil riwayat: {str(e)}"}), 500
|
||||
@@ -10,21 +10,6 @@ api_history = Blueprint('api_history', __name__)
|
||||
# Gunakan path absolut untuk file database untuk menghindari masalah CWD
|
||||
DB_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "bots.db")
|
||||
|
||||
@api_history.route('/api/backtest/history')
|
||||
def get_backtest_history():
|
||||
"""Mengambil semua hasil backtest yang tersimpan dari database."""
|
||||
try:
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
conn.row_factory = sqlite3.Row # Ini memungkinkan akses kolom berdasarkan nama
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM backtest_results ORDER BY timestamp DESC")
|
||||
rows = cursor.fetchall()
|
||||
# Ubah baris menjadi list of dictionaries
|
||||
results = [dict(row) for row in rows]
|
||||
return jsonify(results)
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Gagal mengambil riwayat backtest: {str(e)}"}), 500
|
||||
|
||||
@api_history.route('/api/history')
|
||||
def api_global_history():
|
||||
history = get_trade_history_mt5()
|
||||
@@ -65,4 +50,4 @@ def api_bot_history(bot_id):
|
||||
return jsonify(filtered)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Bot History {bot_id}: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
return jsonify({'error': str(e)}), 500
|
||||
@@ -38,17 +38,17 @@ class BollingerBandsStrategy(BaseStrategy):
|
||||
last = df.iloc[-1]
|
||||
price = last["close"]
|
||||
signal = "HOLD"
|
||||
explanation = f"Harga di dalam Bands atau tren tidak sesuai."
|
||||
explanation = "Harga di dalam Bands atau tren tidak sesuai."
|
||||
|
||||
is_uptrend = price > last[trend_filter_col]
|
||||
is_downtrend = price < last[trend_filter_col]
|
||||
|
||||
if is_uptrend and last['low'] <= last[bbl_col]:
|
||||
signal = "BUY"
|
||||
explanation = f"Uptrend & Oversold: Harga menyentuh Band Bawah."
|
||||
explanation = "Uptrend & Oversold: Harga menyentuh Band Bawah."
|
||||
elif is_downtrend and last['high'] >= last[bbu_col]:
|
||||
signal = "SELL"
|
||||
explanation = f"Downtrend & Overbought: Harga menyentuh Band Atas."
|
||||
explanation = "Downtrend & Overbought: Harga menyentuh Band Atas."
|
||||
|
||||
return {"signal": signal, "price": price, "explanation": explanation}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from .bollinger_squeeze import BollingerSqueezeStrategy
|
||||
from .mercy_edge import MercyEdgeStrategy
|
||||
from .quantum_velocity import QuantumVelocityStrategy
|
||||
from .pulse_sync import PulseSyncStrategy
|
||||
from .turtle_breakout import TurtleBreakoutStrategy
|
||||
|
||||
STRATEGY_MAP = {
|
||||
'MA_CROSSOVER': MACrossoverStrategy,
|
||||
@@ -18,4 +19,5 @@ STRATEGY_MAP = {
|
||||
'MERCY_EDGE': MercyEdgeStrategy,
|
||||
'quantum_velocity': QuantumVelocityStrategy,
|
||||
'PULSE_SYNC': PulseSyncStrategy,
|
||||
'TURTLE_BREAKOUT': TurtleBreakoutStrategy,
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
# core/strategies/turtle_breakout.py
|
||||
import pandas as pd
|
||||
from .base_strategy import BaseStrategy
|
||||
|
||||
class TurtleBreakoutStrategy(BaseStrategy):
|
||||
name = 'Turtle Breakout'
|
||||
description = 'Strategi trend-following klasik berdasarkan penembusan harga tertinggi/terendah N periode.'
|
||||
|
||||
@classmethod
|
||||
def get_definable_params(cls):
|
||||
return [
|
||||
{"name": "entry_period", "label": "Periode Channel Masuk", "type": "number", "default": 20},
|
||||
{"name": "exit_period", "label": "Periode Channel Keluar", "type": "number", "default": 10},
|
||||
]
|
||||
|
||||
def analyze(self, df):
|
||||
"""Metode untuk LIVE TRADING."""
|
||||
entry_period = self.params.get('entry_period', 20)
|
||||
exit_period = self.params.get('exit_period', 10)
|
||||
|
||||
if df is None or df.empty or len(df) < entry_period + 1:
|
||||
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup."}
|
||||
|
||||
# Hitung Channel (menggunakan shift(1) untuk menghindari look-ahead)
|
||||
df['entry_upper'] = df['high'].rolling(window=entry_period).max().shift(1)
|
||||
df['entry_lower'] = df['low'].rolling(window=entry_period).min().shift(1)
|
||||
df['exit_upper'] = df['high'].rolling(window=exit_period).max().shift(1)
|
||||
df['exit_lower'] = df['low'].rolling(window=exit_period).min().shift(1)
|
||||
|
||||
df.dropna(inplace=True)
|
||||
|
||||
if df.empty:
|
||||
return {"signal": "HOLD", "price": None, "explanation": "Indikator belum matang."}
|
||||
|
||||
last = df.iloc[-1]
|
||||
price = last["close"]
|
||||
signal = "HOLD"
|
||||
explanation = "Tidak ada sinyal."
|
||||
|
||||
# Logika Entry (hanya jika tidak ada posisi)
|
||||
# Dalam live trading, bot.in_position akan mengelola state
|
||||
# Kita hanya memberikan sinyal BUY/SELL jika kondisi terpenuhi
|
||||
if price > last['entry_upper']:
|
||||
signal = "BUY"
|
||||
explanation = f"Harga menembus {entry_period}-periode tertinggi."
|
||||
elif price < last['entry_lower']:
|
||||
signal = "SELL"
|
||||
explanation = f"Harga menembus {entry_period}-periode terendah."
|
||||
|
||||
return {"signal": signal, "price": price, "explanation": explanation}
|
||||
|
||||
def analyze_df(self, df):
|
||||
"""Metode untuk BACKTESTING (stateful)."""
|
||||
entry_period = self.params.get('entry_period', 20)
|
||||
exit_period = self.params.get('exit_period', 10)
|
||||
|
||||
# Hitung Channel (menggunakan shift(1) untuk menghindari look-ahead)
|
||||
df['entry_upper'] = df['high'].rolling(window=entry_period).max().shift(1)
|
||||
df['entry_lower'] = df['low'].rolling(window=entry_period).min().shift(1)
|
||||
df['exit_upper'] = df['high'].rolling(window=exit_period).max().shift(1)
|
||||
df['exit_lower'] = df['low'].rolling(window=exit_period).min().shift(1)
|
||||
|
||||
# Dropna untuk memastikan semua indikator terhitung
|
||||
df.dropna(inplace=True)
|
||||
df = df.reset_index(drop=True) # Reset index setelah dropna
|
||||
|
||||
signals = ['HOLD'] * len(df)
|
||||
in_position = False
|
||||
position_type = None # 'BUY' or 'SELL'
|
||||
|
||||
# Loop melalui data untuk mensimulasikan stateful trading
|
||||
for i in range(len(df)):
|
||||
current_bar = df.iloc[i]
|
||||
|
||||
# Pastikan channel values tersedia untuk bar saat ini
|
||||
if pd.isna(current_bar['entry_upper']) or pd.isna(current_bar['exit_lower']):
|
||||
continue # Lewati jika data indikator belum lengkap
|
||||
|
||||
# --- Logika Exit ---
|
||||
if in_position:
|
||||
if position_type == 'BUY' and current_bar['close'] < current_bar['exit_lower']:
|
||||
signals[i] = 'HOLD' # Sinyal untuk menutup posisi
|
||||
in_position = False
|
||||
position_type = None
|
||||
elif position_type == 'SELL' and current_bar['close'] > current_bar['exit_upper']:
|
||||
signals[i] = 'HOLD' # Sinyal untuk menutup posisi
|
||||
in_position = False
|
||||
position_type = None
|
||||
|
||||
# --- Logika Entry (Hanya jika tidak ada posisi) ---
|
||||
if not in_position:
|
||||
if current_bar['close'] > current_bar['entry_upper']:
|
||||
signals[i] = 'BUY'
|
||||
in_position = True
|
||||
position_type = 'BUY'
|
||||
elif current_bar['close'] < current_bar['entry_lower']:
|
||||
signals[i] = 'SELL'
|
||||
in_position = True
|
||||
position_type = 'SELL'
|
||||
|
||||
df['signal'] = signals
|
||||
return df
|
||||
+5
-7
@@ -59,9 +59,7 @@ def main():
|
||||
is_read INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (bot_id) REFERENCES bots (id) ON DELETE CASCADE
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Buat koneksi database
|
||||
conn = create_connection(DB_FILE)
|
||||
@@ -72,10 +70,8 @@ def main():
|
||||
create_table(conn, sql_create_bots_table)
|
||||
|
||||
print("\nMembuat tabel 'trade_history'...")
|
||||
create_table(conn, sql_create_history_table)
|
||||
create_table(conn, sql_create_history_table)
|
||||
|
||||
|
||||
|
||||
# --- TAMBAHKAN INI ---
|
||||
print("\nMembuat tabel 'backtest_results'...")
|
||||
sql_create_backtest_results_table = """
|
||||
@@ -88,6 +84,8 @@ def main():
|
||||
total_trades INTEGER NOT NULL,
|
||||
win_rate_percent REAL NOT NULL,
|
||||
max_drawdown_percent REAL NOT NULL,
|
||||
wins INTEGER NOT NULL,
|
||||
losses INTEGER NOT NULL,
|
||||
equity_curve TEXT, -- Disimpan sebagai JSON
|
||||
trade_log TEXT, -- Disimpan sebagai JSON
|
||||
parameters TEXT -- Disimpan sebagai JSON
|
||||
@@ -102,4 +100,4 @@ def main():
|
||||
print("Error! Tidak dapat membuat koneksi database.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
@@ -20,6 +20,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Fungsi untuk mengekstrak nama pasar dari nama file
|
||||
const extractMarketName = (filename) => {
|
||||
if (!filename) return 'N/A';
|
||||
const parts = filename.split('_');
|
||||
if (parts.length > 0) {
|
||||
return parts[0].toUpperCase();
|
||||
}
|
||||
return 'N/A';
|
||||
};
|
||||
|
||||
// Muat daftar riwayat backtest
|
||||
async function loadHistoryList() {
|
||||
try {
|
||||
@@ -38,10 +48,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
history.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
||||
|
||||
history.forEach(item => {
|
||||
const marketName = extractMarketName(item.data_filename);
|
||||
const itemElement = document.createElement('div');
|
||||
itemElement.className = 'p-3 mb-2 bg-gray-50 rounded cursor-pointer hover:bg-gray-100 border border-gray-200';
|
||||
itemElement.innerHTML = `
|
||||
<p class="font-medium text-gray-800">${item.strategy_name || 'Tidak Diketahui'}</p>
|
||||
<p class="font-medium text-gray-800">${item.strategy_name || 'Tidak Diketahui'} (${marketName})</p>
|
||||
<p class="text-xs text-gray-500">${formatTimestamp(item.timestamp)}</p>
|
||||
<p class="text-sm mt-1"><span class="font-semibold">Profit:</span> ${parseFloat(item.total_profit_pips).toFixed(2)} pips</p>
|
||||
`;
|
||||
@@ -61,12 +72,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
detailPlaceholder.classList.add('hidden');
|
||||
detailView.classList.remove('hidden');
|
||||
|
||||
const marketName = extractMarketName(item.data_filename);
|
||||
|
||||
// Isi data dasar
|
||||
detailId.textContent = item.id;
|
||||
detailTimestamp.textContent = formatTimestamp(item.timestamp);
|
||||
|
||||
// Isi ringkasan
|
||||
detailSummary.innerHTML = `
|
||||
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Strategi</p><p class="font-bold">${item.strategy_name || 'N/A'}</p></div>
|
||||
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Pasar</p><p class="font-bold">${marketName}</p></div>
|
||||
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Total Profit</p><p class="font-bold">${parseFloat(item.total_profit_pips).toFixed(2)} pips</p></div>
|
||||
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Max Drawdown</p><p class="font-bold">${parseFloat(item.max_drawdown_percent).toFixed(2)}%</p></div>
|
||||
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Win Rate</p><p class="font-bold">${parseFloat(item.win_rate_percent).toFixed(2)}%</p></div>
|
||||
|
||||
Reference in New Issue
Block a user