Limit that API to 150: import requests
import pandas as pd
import numpy as np
import time
import mplfinance as mpf  # For plotting
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from matplotlib.dates import date2num
import matplotlib.dates as mdates

# List of cryptocurrency pairs and timeframes
symbols = [
    'BTCUSDT', 'ETHUSDT', 'XRPUSDT', 'DOGEUSDT', 'SOLUSDT',
    'BNBUSDT', 'SUIUSDT', 'CKBUSDT', 'FTMUSDT', 'RUNEUSDT',
    'UNIUSDT', 'AAVEUSDT', 'BCHUSDT', 'KEYUSDT', '1INCHUSDT'
]

timeframes = ['30m', '1h', '2h', '4h']

interval_mapping = {
    '30m': '30m',
    '1h': '1h',
    '2h': '2h',
    '4h': '4h'
}

def get_klines(symbol, interval, limit=1000):
    """
    Fetches historical kline data from Binance API.
    """
    base_url = 'https://api.binance.com'
    endpoint = '/api/v3/klines'
    params = {
        'symbol': symbol,
        'interval': interval,
        'limit': limit
    }
    response = requests.get(base_url + endpoint, params=params)
    data = response.json()
    if response.status_code != 200:
        print(f"Error fetching data for {symbol} at {interval}: {data}")
        return None
    df = pd.DataFrame(data, columns=[
        'Open Time', 'Open', 'High', 'Low', 'Close', 'Volume',
        'Close Time', 'Quote Asset Volume', 'Number of Trades',
        'Taker Buy Base Asset Volume', 'Taker Buy Quote Asset Volume', 'Ignore'
    ])
    # Convert timestamp to datetime
    df['Date'] = pd.to_datetime(df['Open Time'], unit='ms')
    df.set_index('Date', inplace=True)
    # Convert numeric columns to float
    numeric_cols = ['Open', 'High', 'Low', 'Close', 'Volume',
                    'Quote Asset Volume', 'Taker Buy Base Asset Volume',
                    'Taker Buy Quote Asset Volume']
    df[numeric_cols] = df[numeric_cols].astype(float)
    return df

def find_zones(df, N=5):
    """
    Identifies potential demand and supply zones by finding swing lows and highs.
    """
    # Identify swing highs
    df['Swing_High'] = df['High'][
        (df['High'] == df['High'].rolling(window=2*N+1, center=True).max()) &
        (df['High'].shift(1) != df['High'])  # Exclude flat tops
    ]
    # Identify swing lows
    df['Swing_Low'] = df['Low'][
        (df['Low'] == df['Low'].rolling(window=2*N+1, center=True).min()) &
        (df['Low'].shift(1) != df['Low'])  # Exclude flat bottoms
    ]

    # Clean up NaN values in swings
    swing_highs = df.dropna(subset=['Swing_High'])[['Swing_High']]
    swing_lows = df.dropna(subset=['Swing_Low'])[['Swing_Low']]

    return swing_highs, swing_lows

def filter_fresh_zones(swing_highs, swing_lows, df):
    """
    Filters swing highs and swing lows to keep only fresh (unviolated) zones.
    """
    fresh_swing_highs = []
    fresh_swing_lows = []

    for idx, row in swing_highs.iterrows():
        swing_high_price = row['Swing_High']
        after_swing = df.loc[idx:]
        after_swing = after_swing.iloc[1:]  # Exclude the bar where swing high was formed
        # Check if price revisits the swing high price
        if (after_swing['High'] >= swing_high_price).any():
            # Zone is used
            pass
        else:
            # Zone is fresh
            fresh_swing_highs.append((idx, swing_high_price))

    for idx, row in swing_lows.iterrows():
        swing_low_price = row['Swing_Low']
        after_swing = df.loc[idx:]
        after_swing = after_swing.iloc[1:]  # Exclude the bar where swing low was formed
        # Check if price revisits the swing low price
        if (after_swing['Low'] <= swing_low_price).any():
            # Zone is used
            pass
        else:
            # Zone is fresh
            fresh_swing_lows.append((idx, swing_low_price))

    # Create DataFrames from the lists
    fresh_swing_highs_df = pd.DataFrame(fresh_swing_highs, columns=['Date', 'Swing_High'])
    fresh_swing_highs_df.set_index('Date', inplace=True)
    fresh_swing_lows_df = pd.DataFrame(fresh_swing_lows, columns=['Date', 'Swing_Low'])
    fresh_swing_lows_df.set_index('Date', inplace=True)

    return fresh_swing_highs_df, fresh_swing_lows_df

def plot_chart(df, swing_highs, swing_lows, symbol, timeframe):
    """
    Plots the candlestick chart with demand and supply zones.
    """
    # Limit the data to last 500 rows to improve plot clarity
    df_plot = df.iloc[-500:]

    # Filter zones to only those within df_plot index
    swing_highs = swing_highs[swing_highs.index >= df_plot.index[0]]
    swing_lows = swing_lows[swing_lows.index >= df_plot.index[0]]

    # Prepare the plot style
    mc = mpf.make_marketcolors(
        up='green',
        down='red',
        edge='inherit',
        wick='inherit',
        volume='in',
    )
    s = mpf.make_mpf_style(marketcolors=mc)

    # Adjust the zones to extend horizontally across the chart
    ylim = df_plot['High'].max() - df_plot['Low'].min()
    y_offset = ylim * 0.005  # Small offset for visual clarity

    # Plot the candlestick chart
    fig, axlist = mpf.plot(
        df_plot,
        type='candle',
        style=s,
        title=f"{symbol} - {timeframe} Demand and Supply Zones",
        ylabel='Price',
        volume=False,
        returnfig=True,
        warn_too_much_data=10000  # Suppress the 'too much data' warning
    )

    ax = axlist[0]

    # Get x-axis limits
    x_min = date2num(df_plot.index[0])
    x_max = date2num(df_plot.index[-1])
    x_range = x_max - x_min

    # For each supply zone
    for idx, row in swing_highs.iterrows():
        x0 = date2num(idx)
        y0 = row['Swing_High'] - y_offset
        y1 = row['Swing_High'] + y_offset

        # Normalize x0 between 0 and 1
        xmin = (x0 - x_min) / x_range
        xmax = 1.0  # Extend to the right edge

        # Ensure xmin is within [0,1]
        if xmin < 0:
            continue  # Skip if the zone is before the plotted data
        elif xmin > 1.0:
            xmin = 1.0  # Adjust to right edge

        # Plot horizontal span
        ax.axhspan(
            y0,
            y1,
            xmin=xmin,
            xmax=xmax,
            facecolor='red',
            alpha=0.2
        )

    # For each demand zone
    for idx, row in swing_lows.iterrows():
        x0 = date2num(idx)
        y0 = row['Swing_Low'] - y_offset
        y1 = row['Swing_Low'] + y_offset

        # Normalize x0 between 0 and 1
        xmin = (x0 - x_min) / x_range
        xmax = 1.0  # Extend to the right edge

        # Ensure xmin is within [0,1]
        if xmin < 0:
            continue  # Skip if the zone is before the plotted data
        elif xmin > 1.0:
            xmin = 1.0  # Adjust to right edge

        # Plot horizontal span
        ax.axhspan(
            y0,
            y1,
            xmin=xmin,
            xmax=xmax,
            facecolor='green',
            alpha=0.2
        )

    plt.show()

def run_analysis():
    """
    Runs the demand and supply zone analysis for all symbols and timeframes.
    """
    for symbol in symbols:
        for timeframe in timeframes:
            print(f"Processing {symbol} on {timeframe} timeframe...")
            df = get_klines(symbol, interval_mapping[timeframe])
            if df is not None:
                swing_highs, swing_lows = find_zones(df)

                # Filter for fresh zones
                fresh_swing_highs, fresh_swing_lows = filter_fresh_zones(swing_highs, swing_lows, df)

                # Keep only the most recent 2 fresh swing highs and lows
                fresh_swing_highs = fresh_swing_highs.tail(2)
                fresh_swing_lows = fresh_swing_lows.tail(2)

                print(f"Found {len(fresh_swing_highs)} fresh swing highs and {len(fresh_swing_lows)} fresh swing lows for {symbol} on {timeframe}")

                # Plot the chart with demand and supply zones
                plot_chart(df, fresh_swing_highs, fresh_swing_lows, symbol, timeframe)

                # Pause to respect API rate limits
                time.sleep(0.5)  # Adjust sleep time as needed
            else:
                print(f"Skipping {symbol} on {timeframe} due to API error.")
            print("-" * 50)

if __name__ == "__main__":
    run_analysis()