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:
Reynov Christian
2025-08-08 17:07:40 +08:00
parent 3112fc89f0
commit af142b5ddf
10 changed files with 137 additions and 42 deletions
+2 -7
View File
@@ -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
-1
View File
@@ -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):
+6 -7
View File
@@ -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
+1 -16
View File
@@ -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
+3 -3
View File
@@ -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}
+2
View File
@@ -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,
}
+102
View File
@@ -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
View File
+5 -7
View File
@@ -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()
+16 -1
View File
@@ -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>