# -*- coding: utf-8 -*-
# =================================================================================
# KODE BOT TRADING AI - VERSI REAL-TIME DENGAN FINNHUB WEBSOCKET
# PERINGATAN: Sangat tidak stabil jika dijalankan di Pydroid/Ponsel.
# Direkomendasikan untuk dijalankan di PC/VPS.
# =================================================================================
import os
import asyncio
import logging
import json
import nest_asyncio
import numpy as np
import pandas as pd
import finnhub
import websocket # Library: websocket-client
import threading
import time
from datetime import datetime, timezone
# Terapkan patch untuk Pydroid
nest_asyncio.apply()
from sklearn.model_selection import train_test_split
from [Link] import RandomForestClassifier
from [Link] import accuracy_score
import [Link] as genai
from openai import OpenAI
from telegram import Update
from [Link] import Application, CommandHandler, ContextTypes
# --- KONFIGURASI (GANTI DENGAN KUNCI API ANDA) ---
TELEGRAM_BOT_TOKEN = "GANTI_DENGAN_TOKEN_BOT_TELEGRAM_ANDA"
GEMINI_API_KEY = "GANTI_DENGAN_API_KEY_GEMINI_BARU_ANDA"
GROQ_API_KEY = "GANTI_DENGAN_API_KEY_GROQ_ANDA"
FINNHUB_API_KEY = "GANTI_DENGAN_API_KEY_FINNHUB_ANDA" # <-- BARU: API Key Finnhub
# --- Konfigurasi Sinyal Trading---
# Daftar pair yang akan dipantau oleh WebSocket. Finnhub butuh prefix OANDA untuk
forex.
SYMBOLS_TO_WATCH = ['OANDA:EUR_USD', 'OANDA:GBP_USD', 'OANDA:XAU_USD']
INTERVAL = 5 # Interval dalam menit untuk agregasi candle
CANDLE_HISTORY_SIZE = 500 # Jumlah candle historis yang disimpan
# --- Variabel Global untuk State Management ---
# Dictionary untuk menyimpan data candle setiap simbol
# Contoh: {'OANDA:EUR_USD': DataFrame, 'OANDA:GBP_USD': DataFrame}
candle_data = {symbol: [Link]() for symbol in SYMBOLS_TO_WATCH}
# Dictionary untuk menyimpan timestamp candle terakhir setiap simbol
last_candle_ts = {symbol: 0 for symbol in SYMBOLS_TO_WATCH}
# Dictionary untuk menyimpan hasil sinyal terakhir
latest_signals = {symbol: "Belum ada sinyal." for symbol in SYMBOLS_TO_WATCH}
# Variabel untuk menyimpan chat_id pertama yang memulai bot
TARGET_CHAT_ID = None
# --- Inisialisasi Klien API ---
try:
gemini_json_config =
[Link](response_mime_type="application/json")
[Link](api_key=GEMINI_API_KEY)
gemini_model = [Link]('gemini-1.5-flash-latest',
generation_config=gemini_json_config)
groq_client = OpenAI(api_key=GROQ_API_KEY,
base_url="[Link]
finnhub_client = [Link](api_key=FINNHUB_API_KEY)
except Exception as e:
[Link](f"Gagal menginisialisasi API: {e}")
exit()
# --- Setup Logging ---
[Link](format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=[Link])
[Link]("httpx").setLevel([Link])
logger = [Link](__name__)
# --- MODEL MACHINE LEARNING ---
model_ml = RandomForestClassifier(n_estimators=100, random_state=42)
async def train_and_predict(symbol: str):
"""Fungsi inti yang melatih model dan menghasilkan prediksi untuk satu
simbol."""
[Link](f"Memulai analisis untuk {symbol}...")
try:
df = candle_data[symbol].copy()
if len(df) < 50: # Butuh cukup data untuk menghitung indikator
[Link](f"Data historis untuk {symbol} tidak cukup untuk
analisis.")
return None
# Perhitungan Indikator
df['SMA_10'] = df['close'].rolling(window=10).mean()
df['SMA_30'] = df['close'].rolling(window=30).mean()
delta = df['close'].diff(1); gain = [Link](delta > 0, 0); loss = -
[Link](delta < 0, 0)
avg_gain = [Link](window=14).mean(); avg_loss =
[Link](window=14).mean()
rs = avg_gain / avg_loss; df['RSI'] = 100 - (100 / (1 + rs))
low_14, high_14 = df['low'].rolling(window=14).min(),
df['high'].rolling(window=14).max()
df['stoch_k'] = 100 * ((df['close'] - low_14) / (high_14 - low_14))
df['stoch_d'] = df['stoch_k'].rolling(window=3).mean()
ema_12, ema_26 = df['close'].ewm(span=12, adjust=False).mean(),
df['close'].ewm(span=26, adjust=False).mean()
df['macd'] = ema_12 - ema_26; df['macd_signal'] = df['macd'].ewm(span=9,
adjust=False).mean(); df['macd_hist'] = df['macd'] - df['macd_signal']
high_low = df['high'] - df['low']; high_prev_close = [Link](df['high'] -
df['close'].shift(1)); low_prev_close = [Link](df['low'] - df['close'].shift(1))
tr = [Link](high_low, [Link](high_prev_close, low_prev_close));
df['atr'] = [Link](window=14).mean()
df['future_price'] = df['close'].shift(-5); [Link](inplace=True);
df['target'] = (df['future_price'] > df['close']).astype(int)
if [Link]:
[Link](f"Data tidak cukup untuk training {symbol} setelah
dihitung.")
return None
features = ['SMA_10', 'SMA_30', 'RSI', 'stoch_k', 'stoch_d', 'macd',
'macd_hist', 'atr']
X, y = df[features], df['target']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,
random_state=42, shuffle=False)
model_ml.fit(X_train, y_train)
# Prediksi pada data terakhir
last_data_point = [Link][-1:]
prediction_encoded = model_ml.predict(last_data_point[features])[0]
signal_ml = "BUY" if prediction_encoded == 1 else "SELL"
confidence_ml = max(model_ml.predict_proba(last_data_point[features])[0]) *
100
return {
"signal_ml": signal_ml,
"confidence_ml": confidence_ml,
"last_data": last_data_point
}
except Exception as e:
[Link](f"Error saat training model untuk {symbol}: {e}")
return None
async def get_ai_analysis(symbol, ml_result):
"""Meminta analisis dari Gemini dan Groq, lalu membuat kesimpulan."""
last_data = ml_result['last_data']
current_price = last_data['close'].iloc[0]
base_prompt = (f"Analisis data trading untuk {symbol} M5.
Harga={current_price:.5f}, RSI={last_data['RSI'].iloc[0]:.2f},
Stoch(K={last_data['stoch_k'].iloc[0]:.2f}, D={last_data['stoch_d'].iloc[0]:.2f}),
MACD Hist={last_data['macd_hist'].iloc[0]:.6f}, ATR={last_data['atr'].iloc[0]:.5f}.
Prediksi ML: {ml_result['signal_ml']} ({ml_result['confidence_ml']:.2f}%). Berikan
analisis dalam JSON: "
f'{{"signal": "BUY/SELL/HOLD", "entry": harga, "tp": harga,
"sl": harga, "confidence_percent": persentase, "reason": "alasan singkat"}}.
Pastikan semua nilai adalah angka.')
groq_analysis_json, gemini_analysis_json = None, None
try:
groq_response = groq_client.[Link](model="llama3-8b-8192",
messages=[{"role": "user", "content": base_prompt}], response_format={"type":
"json_object"})
groq_analysis_json = groq_response.choices[0].[Link]
except Exception as e: [Link](f"Gagal hubungi Groq: {e}")
try:
gemini_response = gemini_model.generate_content(base_prompt)
gemini_analysis_json = [Link](gemini_response.text)
except Exception as e: [Link](f"Gagal hubungi Gemini: {e}")
summary_prompt = (f"Anda manajer trading. Simpulkan 2 analisis ini menjadi 1
keputusan final. Analisis 1: {gemini_analysis_json}. Analisis 2:
{groq_analysis_json}. Data ML: {ml_result['signal_ml']}
({ml_result['confidence_ml']:.2f}%). Berikan kesimpulan dalam format JSON: "
f'{{"final_signal": "BUY/SELL/HOLD", "final_entry": harga,
"final_tp": harga, "final_sl": harga, "final_confidence_percent": persentase}}.')
final_summary_json = None
try:
summary_response = groq_client.[Link](model="llama3-8b-
8192", messages=[{"role": "user", "content": summary_prompt}],
response_format={"type": "json_object"})
final_summary_json = summary_response.choices[0].[Link]
except Exception as e: [Link](f"Gagal membuat kesimpulan: {e}")
return gemini_analysis_json, groq_analysis_json, final_summary_json
def format_final_message(symbol, gemini_json, groq_json, summary_json):
"""Memformat pesan akhir yang akan dikirim ke Telegram."""
def format_single_analysis(title, analysis_json):
if not analysis_json: return f"**{title}:**\nAnalisis tidak tersedia."
try:
data = analysis_json if isinstance(analysis_json, dict) else
[Link](analysis_json)
return (f"**{title}:**\nSinyal: **{[Link]('signal', 'N/A')}**
({[Link]('confidence_percent', 0)}%)\nTP: {[Link]('tp', 'N/A')} | SL:
{[Link]('sl', 'N/A')}\nAlasan: *{[Link]('reason', 'N/A')}*")
except: return f"**{title}:**\nAnalisis tidak valid."
final_summary_text = "**Kesimpulan Akhir:**\nTidak dapat membuat kesimpulan."
if summary_json:
try:
data = [Link](summary_json)
final_summary_text = (f"**⭐ Kesimpulan Akhir:**\n"
f"Sinyal: **{[Link]('final_signal', 'N/A')}**
({[Link]('final_confidence_percent', 0)}%)\n"
f"Entry: **{[Link]('final_entry', 'N/A')}**\n"
f"TP: {[Link]('final_tp', 'N/A')} | SL:
{[Link]('final_sl', 'N/A')}")
except: pass
return (f"🚨 **Analisis Sinyal Real-time: {[Link]('OANDA:', '')}** 🚨\n\
n"
f"🤖 **Analisis Gemini:**\n{format_single_analysis('Gemini',
gemini_json)}\n\n"
f"⚡ **Analisis Groq (Llama 3):**\n{format_single_analysis('Groq',
groq_json)}\n\n"
f"----------------------------------------\n{final_summary_text}\
n----------------------------------------\n"
f"⚠️ *Disclaimer: Selalu lakukan riset Anda sendiri.*")
# --- Logika WebSocket ---
def on_message(ws, message):
"""Fungsi ini dipanggil setiap kali ada data harga baru dari Finnhub."""
global last_candle_ts
data = [Link](message)
if data['type'] != 'trade':
return
for trade in data['data']:
symbol = trade['s']
price = trade['p']
# Konversi timestamp milidetik ke detik
ts_seconds = trade['t'] / 1000
dt_object = [Link](ts_seconds, tz=[Link])
# Tentukan bucket 5 menit untuk trade ini
current_bucket_ts = int(ts_seconds - (ts_seconds % (INTERVAL * 60)))
# Jika ini adalah candle pertama untuk simbol ini
if last_candle_ts.get(symbol, 0) == 0:
last_candle_ts[symbol] = current_bucket_ts
# Jika trade masuk ke bucket (candle) yang baru
if current_bucket_ts > last_candle_ts[symbol]:
[Link](f"Candle M{INTERVAL} baru terdeteksi untuk {symbol} pada
{dt_object}. Memicu analisis.")
last_candle_ts[symbol] = current_bucket_ts
# Jalankan analisis di thread terpisah agar tidak memblokir WebSocket
analysis_thread = [Link](target=[Link],
args=(run_full_analysis_cycle(symbol),))
analysis_thread.start()
# Update data candle saat ini (agregasi)
df = candle_data[symbol]
if not [Link]:
last_idx = [Link][-1]
[Link][last_idx, 'close'] = price
[Link][last_idx, 'high'] = max([Link][last_idx, 'high'], price)
[Link][last_idx, 'low'] = min([Link][last_idx, 'low'], price)
else: # Jika DataFrame masih kosong, buat baris pertama
new_row = {'open': price, 'high': price, 'low': price, 'close': price,
'timestamp': current_bucket_ts}
candle_data[symbol] = [Link]([df, [Link]([new_row])],
ignore_index=True)
def on_error(ws, error):
[Link](f"WebSocket Error: {error}")
def on_close(ws, close_status_code, close_msg):
[Link]("### WebSocket ditutup ###")
def on_open(ws):
[Link]("### WebSocket Terbuka ###")
for symbol in SYMBOLS_TO_WATCH:
[Link](f"Berlangganan data untuk {symbol}")
[Link]([Link]({'type':'subscribe', 'symbol':symbol}))
def run_websocket():
"""Menjalankan WebSocket dalam loop tak terbatas."""
while True:
ws = [Link](f"[Link]
on_message=on_message,
on_error=on_error,
on_close=on_close)
ws.on_open = on_open
ws.run_forever()
[Link]("Mencoba menyambung ulang WebSocket dalam 10 detik...")
[Link](10)
# --- FUNGSI BOT TELEGRAM ---
async def run_full_analysis_cycle(symbol):
"""Menjalankan seluruh siklus: train, predict, AI analysis, dan kirim pesan."""
if TARGET_CHAT_ID is None:
[Link]("Belum ada chat_id target, analisis tidak dikirim.")
return
# 1. Train model dan dapatkan prediksi
ml_result = await train_and_predict(symbol)
if not ml_result:
latest_signals[symbol] = f"Gagal menganalisis {symbol}: Prediksi ML gagal."
await [Link].send_message(TARGET_CHAT_ID,
text=latest_signals[symbol])
return
# 2. Dapatkan analisis dari AI
gemini_json, groq_json, summary_json = await get_ai_analysis(symbol, ml_result)
# 3. Format dan kirim pesan
final_message = format_final_message(symbol, gemini_json, groq_json,
summary_json)
latest_signals[symbol] = final_message # Simpan sinyal terakhir
await [Link].send_message(TARGET_CHAT_ID, text=final_message,
parse_mode='Markdown')
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
global TARGET_CHAT_ID
user = update.effective_user
chat_id = [Link].chat_id
# Simpan chat_id pertama sebagai target untuk pengiriman sinyal otomatis
if TARGET_CHAT_ID is None:
TARGET_CHAT_ID = chat_id
[Link](f"Chat ID target telah diatur ke: {chat_id}")
await [Link].reply_text("Anda telah ditetapkan sebagai penerima
sinyal otomatis.")
await [Link].reply_html(
f"Halo {user.mention_html()}!\n\nBot sekarang berjalan dalam mode **Real-
time**.\n"
f"Sinyal akan dikirim secara otomatis setiap kali candle M{INTERVAL}
selesai untuk pair yang dipantau.\n\n"
f"<b>/signal [pair]</b> - Lihat sinyal terakhir (contoh: /signal EURUSD)."
)
async def signal_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
None:
"""Mengirim sinyal terakhir yang tersimpan."""
symbol_arg = "EUR_USD" # Default
if [Link]:
symbol_arg = [Link][0].upper().replace('/', '_')
finnhub_symbol = f"OANDA:{symbol_arg}"
if finnhub_symbol in latest_signals:
await [Link].reply_text(latest_signals[finnhub_symbol],
parse_mode='Markdown')
else:
await [Link].reply_text(f"Simbol {symbol_arg} tidak dipantau.")
def fetch_initial_data():
"""Mengambil data historis awal untuk semua simbol."""
[Link]("Mengambil data historis awal...")
for symbol in SYMBOLS_TO_WATCH:
try:
# Finnhub menggunakan timestamp UNIX dalam detik
to_ts = int([Link]())
from_ts = to_ts - (CANDLE_HISTORY_SIZE * INTERVAL * 60 * 2) # Ambil
data lebih banyak untuk cadangan
res = finnhub_client.forex_candles(symbol, str(INTERVAL), from_ts,
to_ts)
if res['s'] == 'ok':
df = [Link](res)
df = [Link](columns={'o': 'open', 'h': 'high', 'l': 'low', 'c':
'close', 't': 'timestamp'})
df = df[['timestamp', 'open', 'high', 'low', 'close']]
# Ambil hanya sejumlah yang dibutuhkan
candle_data[symbol] =
[Link](CANDLE_HISTORY_SIZE).reset_index(drop=True)
[Link](f"Berhasil mengambil {len(candle_data[symbol])} candle
untuk {symbol}.")
else:
[Link](f"Gagal mengambil data awal untuk {symbol}: {res}")
except Exception as e:
[Link](f"Exception saat mengambil data awal untuk {symbol}: {e}")
# --- MAIN EXECUTION ---
def main() -> None:
global application
application = [Link]().token(TELEGRAM_BOT_TOKEN).build()
# Handler
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("signal", signal_handler))
# ask handler bisa ditambahkan jika perlu
# 1. Ambil data historis dulu
fetch_initial_data()
# 2. Jalankan WebSocket di thread terpisah
ws_thread = [Link](target=run_websocket)
ws_thread.daemon = True # Agar thread mati saat program utama berhenti
ws_thread.start()
# 3. Jalankan bot
[Link]("Bot Telegram mulai berjalan...")
application.run_polling()
if __name__ == "__main__":
main()