print out the trades its taking on chart import ccxt
import pandas as pd
import os
from datetime import datetime, timedelta
import backtrader as bt
import matplotlib.pyplot as plt

# ====================== Configuration ======================

# Define tokens and their announcement dates
TOKENS = {
    'ACT/USDT': '2024-11-11',
    'PNUT/USDT': '2024-11-11'
}

# Timeframes to fetch
TIMEFRAMES = ['1m', '3m']  # 1-minute and 3-minute

# Directory to save data
DATA_DIR = 'data'

# Backtest parameters
TAKE_PROFIT_DAYS_LIST = [1, 3, 5]  # Days to take profit
REENTRY_OPTIONS = [False, True]    # Re-entry enabled or not
INITIAL_CASH = 10000.0
COMMISSION = 0.001  # 0.1%

# ====================== Data Fetching ======================

def fetch_ohlcv(exchange, symbol, timeframe, since, limit=1000):
    """
    Fetch OHLCV data from the exchange.
    """
    all_ohlcv = []
    while True:
        try:
            data = exchange.fetch_ohlcv(symbol, timeframe=timeframe, since=since, limit=limit)
            if not data:
                break
            all_ohlcv += data
            since = data[-1][0] + 1  # Increment timestamp
            if len(data) < limit:
                break
        except ccxt.NetworkError as e:
            print(f"Network error: {e}. Retrying...")
            continue
        except ccxt.ExchangeError as e:
            print(f"Exchange error: {e}. Skipping...")
            break
    return all_ohlcv

def process_and_save_data():
    """
    Fetch and save historical data for each token and timeframe.
    """
    exchange = ccxt.binance()
    for symbol, announcement_date in TOKENS.items():
        for timeframe in TIMEFRAMES:
            print(f"Fetching {timeframe} data for {symbol}...")
            # Convert announcement_date to datetime object
            announcement_datetime = datetime.strptime(announcement_date, "%Y-%m-%d")
            # Fetch data starting 7 days before announcement to 7 days after
            start_datetime = announcement_datetime - timedelta(days=7)
            end_datetime = announcement_datetime + timedelta(days=7)
            since = int(exchange.parse8601(start_datetime.isoformat() + 'T00:00:00Z'))
            ohlcv = fetch_ohlcv(exchange, symbol, timeframe, since)
            if not ohlcv:
                print(f"No data fetched for {symbol} on timeframe {timeframe}. Skipping...")
                continue
            # Convert to DataFrame
            df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
            df['datetime'] = pd.to_datetime(df['timestamp'], unit='ms')
            df.set_index('datetime', inplace=True)
            df.drop('timestamp', axis=1, inplace=True)
            # Filter data within the desired range
            df = df.loc[start_datetime:end_datetime]
            # Check if DataFrame is empty after filtering
            if df.empty:
                print(f"DataFrame for {symbol} on {timeframe} is empty after filtering. Skipping...")
                continue
            # Ensure expected columns are present
            expected_columns = ['open', 'high', 'low', 'close', 'volume']
            if not all(column in df.columns for column in expected_columns):
                print(f"CSV file {symbol} on {timeframe} is missing required columns. Skipping...")
                continue
            # Create directory if not exists
            token_dir = os.path.join(DATA_DIR, symbol.replace('/', '-'))
            os.makedirs(token_dir, exist_ok=True)
            # Save to CSV without the index name
            file_path = os.path.join(token_dir, f"{timeframe}.csv")
            df.to_csv(file_path, index=True, header=True)
            print(f"Saved data to {file_path}")

# ====================== Strategy Definition ======================

class MemeTokenStrategy(bt.Strategy):
    params = (
        ('take_profit_days', 1),
        ('reentry', False),
        ('ma_period', 5),  # Period for Moving Average (if needed for dynamic entry)
        ('printlog', False),
    )

    def log(self, txt, dt=None):
        ''' Logging function '''
        if self.params.printlog:
            dt = dt or self.datas[0].datetime.datetime(0)
            print(f'{dt} - {txt}')

    def __init__(self):
        # Indicators
        self.dataclose = self.datas[0].close
        self.order = None
        self.entry_price = None
        self.entry_time = None
        self.take_profit_time = None
        self.stop_loss_triggered = False
        # For stop-loss on 3-minute candle, track the last 3 closes
        self.close_history = []

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Order is submitted/accepted - no action needed
            return

        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(f'BUY EXECUTED, Price: {order.executed.price}')
                self.entry_price = order.executed.price
                self.entry_time = self.datas[0].datetime.datetime(0)
                self.take_profit_time = self.entry_time + timedelta(days=self.params.take_profit_days)
            elif order.issell():
                self.log(f'SELL EXECUTED, Price: {order.executed.price}')
            self.order = None

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')
            self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return
        self.log(f'TRADE PROFIT, GROSS {trade.pnl}, NET {trade.pnlcomm}')

    def next(self):
        dt = self.datas[0].datetime.datetime(0)

        # Update close history
        self.close_history.append(self.dataclose[0])
        if len(self.close_history) > 3:
            self.close_history.pop(0)

        # Debug: Print last three closes
        if len(self.close_history) == 3:
            self.log(f'Last three closes: {self.close_history}')

        # Check if take profit time has been reached
        if self.entry_time and dt >= self.take_profit_time and self.position:
            self.log('TAKE PROFIT TIME REACHED, SELL CREATE')
            self.order = self.sell(size=5000)
            return

        if not self.position:
            if self.stop_loss_triggered and self.params.reentry:
                # Re-entry condition: price recovers above entry price within 24 hours
                if self.dataclose[0] > self.entry_price:
                    time_since_entry = dt - self.entry_time
                    if time_since_entry <= timedelta(hours=24):
                        self.log('RE-ENTERING, BUY CREATE')
                        self.order = self.buy(size=5000)
                        self.stop_loss_triggered = False

                        return
            else:
                if not self.entry_time:
                    # Initial entry: Buy at the first candle after announcement
                    self.log('BUY CREATE at {}'.format(self.dataclose[0]))
                    self.order = self.buy(size=5000)
        else:
            # Manage the open position
            if len(self.close_history) == 3:
                three_min_close = min(self.close_history)
                if three_min_close < self.entry_price:
                    self.log('STOP LOSS HIT, SELL CREATE')
                    self.order = self.sell(size=5000)
                    self.stop_loss_triggered = True

    def stop(self):
        self.log(f'(Take Profit Days {self.params.take_profit_days}) Ending Value %.2f' % (self.broker.getvalue()), dt=None)

# ====================== Backtesting Function ======================

def run_backtest(symbol, timeframe, take_profit_days, reentry):
    """
    Run a single backtest scenario using PandasData.
    """
    cerebro = bt.Cerebro()

    # Add strategy
    cerebro.addstrategy(
        MemeTokenStrategy,
        take_profit_days=take_profit_days,
        reentry=reentry,
        printlog=False
    )

    # Load data
    token_dir = os.path.join(DATA_DIR, symbol.replace('/', '-'))
    data_path = os.path.join(token_dir, f"{timeframe}.csv")
    if not os.path.exists(data_path):
        print(f"Data file {data_path} not found. Skipping...")
        return None

    # Read CSV into DataFrame
    try:
        df = pd.read_csv(data_path, parse_dates=['datetime'], index_col='datetime')
    except Exception as e:
        print(f"Error reading CSV for {symbol} on {timeframe}: {e}")
        return None

    # Ensure columns are ordered correctly
    expected_columns = ['open', 'high', 'low', 'close', 'volume']
    if not all(column in df.columns for column in expected_columns):
        print(f"CSV file {data_path} is missing required columns.")
        return None

    # Check if DataFrame is empty
    if df.empty:
        print(f"DataFrame for {symbol} on {timeframe} is empty after filtering. Skipping...")
        return None

    # Debug: Print first few rows
    print(f"First 5 rows of the DataFrame for {symbol} on {timeframe}:")
    print(df.head())

    # Create Backtrader data feed from DataFrame
    data = bt.feeds.PandasData(
        dataname=df,
        open='open',
        high='high',
        low='low',
        close='close',
        volume='volume',
        timeframe=bt.TimeFrame.Minutes,
        compression=1,
    )

    cerebro.adddata(data)

    # Set initial cash
    cerebro.broker.setcash(INITIAL_CASH)

    # Set commission
    cerebro.broker.setcommission(commission=COMMISSION)

    # Run backtest
    try:
        strategies = cerebro.run()
        final_value = cerebro.broker.getvalue()
        return final_value
    except Exception as e:
        print(f"Error running backtest for {symbol} on {timeframe}: {e}")
        return None

# ====================== Main Execution ======================

def main():
    # Step 1: Fetch and save data
    print("=== Fetching Historical Data ===")
    process_and_save_data()
    print("Data fetching complete.\n")

    # Step 2: Run backtests
    print("=== Running Backtests ===")
    results = []

    for symbol in TOKENS.keys():
        for take_profit_days in TAKE_PROFIT_DAYS_LIST:
            for reentry in REENTRY_OPTIONS:
                scenario = f"Token: {symbol}, Take Profit: {take_profit_days} day(s), Re-entry: {'ON' if reentry else 'OFF'}"
                print(f"Running Backtest - {scenario}")
                final_value = run_backtest(symbol, '1m', take_profit_days, reentry)
                if final_value is not None:
                    results.append({
                        'Token': symbol,
                        'Take_Profit_Days': take_profit_days,
                        'Reentry': reentry,
                        'Final_Portfolio_Value': final_value
                    })
                    print(f"Final Portfolio Value: {final_value:.2f}\n")
                else:
                    print(f"Backtest for {scenario} failed.\n")

    # Step 3: Analyze results
    if results:
        df_results = pd.DataFrame(results)
        print("=== Backtest Results ===")
        print(df_results)

        # Optionally, save results to CSV
        df_results.to_csv('backtest_results.csv', index=False)
        print("\nResults saved to backtest_results.csv")

        # Visualize results using a pivot table
        try:
            pivot = df_results.pivot_table(index=['Token', 'Take_Profit_Days'], columns='Reentry', values='Final_Portfolio_Value')
            pivot.plot(kind='bar', figsize=(10, 6))
            plt.title('Backtest Final Portfolio Values')
            plt.ylabel('Final Portfolio Value (USD)')
            plt.xticks(rotation=45)
            plt.tight_layout()
            plt.savefig('backtest_results.png')
            plt.show()
            print("Results plot saved to backtest_results.png")
        except Exception as e:
            print(f"Error plotting results: {e}")
    else:
        print("No results to display.")

if __name__ == "__main__":
    main()