diff --git a/README.md b/README.md
index 2b4e73e..d21bead 100644
--- a/README.md
+++ b/README.md
@@ -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
-```
+[
](https://www.paypal.com/paypalme/rebarakaz)
---
## 📝 License
This project is licensed under the MIT License - see the LICENSE.md file for details.
-```bash
\ No newline at end of file
diff --git a/core/backtesting/engine.py b/core/backtesting/engine.py
index b731ad2..ed08090 100644
--- a/core/backtesting/engine.py
+++ b/core/backtesting/engine.py
@@ -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):
diff --git a/core/routes/api_backtest.py b/core/routes/api_backtest.py
index decb493..e8eb14b 100644
--- a/core/routes/api_backtest.py
+++ b/core/routes/api_backtest.py
@@ -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
\ No newline at end of file
diff --git a/core/routes/api_history.py b/core/routes/api_history.py
index 595fea0..96d05d1 100644
--- a/core/routes/api_history.py
+++ b/core/routes/api_history.py
@@ -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
\ No newline at end of file
diff --git a/core/strategies/bollinger_reversion.py b/core/strategies/bollinger_reversion.py
index 1cbd424..9c11b2d 100644
--- a/core/strategies/bollinger_reversion.py
+++ b/core/strategies/bollinger_reversion.py
@@ -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}
diff --git a/core/strategies/strategy_map.py b/core/strategies/strategy_map.py
index e72f098..f695f71 100644
--- a/core/strategies/strategy_map.py
+++ b/core/strategies/strategy_map.py
@@ -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,
}
\ No newline at end of file
diff --git a/core/strategies/turtle_breakout.py b/core/strategies/turtle_breakout.py
new file mode 100644
index 0000000..5dead2d
--- /dev/null
+++ b/core/strategies/turtle_breakout.py
@@ -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
\ No newline at end of file
diff --git a/engine.py b/engine.py
deleted file mode 100644
index e69de29..0000000
diff --git a/init_db.py b/init_db.py
index 7e7d8f9..170ae46 100644
--- a/init_db.py
+++ b/init_db.py
@@ -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()
\ No newline at end of file
diff --git a/static/js/backtest_history.js b/static/js/backtest_history.js
index c2c7280..f814397 100644
--- a/static/js/backtest_history.js
+++ b/static/js/backtest_history.js
@@ -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 = `
-
${item.strategy_name || 'Tidak Diketahui'}
+${item.strategy_name || 'Tidak Diketahui'} (${marketName})
${formatTimestamp(item.timestamp)}
Profit: ${parseFloat(item.total_profit_pips).toFixed(2)} pips
`; @@ -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 = ` +Strategi
${item.strategy_name || 'N/A'}
Pasar
${marketName}
Total Profit
${parseFloat(item.total_profit_pips).toFixed(2)} pips
Max Drawdown
${parseFloat(item.max_drawdown_percent).toFixed(2)}%
Win Rate
${parseFloat(item.win_rate_percent).toFixed(2)}%