Zones are tested only when price come into base range after completing entire formation: import os
from flask import Flask, render_template, request
from binance.client import Client
from binance.exceptions import BinanceAPIException
import pandas as pd
import plotly.graph_objs as go
from plotly.offline import plot
import datetime
import time

# Initialize the Flask application
app = Flask(__name__)

# Configure Binance Client (No API key needed for public data)
client = Client()

# Symbols and Timeframes
symbols = [
    'BTCUSDT', 'ETHUSDT', 'XRPUSDT', 'DOGEUSDT', 'SOLUSDT',
    'BNBUSDT', 'SUIUSDT', 'CKBUSDT', 'FTMUSDT', 'RUNEUSDT',
    'UNIUSDT', 'AAVEUSDT', 'BCHUSDT', 'KEYUSDT', '1INCHUSDT'
]

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

# Rate limit parameters
API_RATE_LIMIT = 150  # Max 150 requests per minute
REQUEST_INTERVAL = 60 / API_RATE_LIMIT  # Interval between requests

# Function to fetch historical candlestick data
def get_historical_klines(symbol, interval, limit=500):
    try:
        # Sleep to respect the API rate limit
        time.sleep(REQUEST_INTERVAL)
        klines = client.get_klines(symbol=symbol, interval=interval, limit=limit)
        df = pd.DataFrame(klines, columns=[
            'timestamp', 'open', 'high', 'low', 'close', 'volume',
            'close_time', 'quote_asset_volume', 'number_of_trades',
            'taker_buy_base', 'taker_buy_quote', 'ignore'
        ])
        df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
        df[['open', 'high', 'low', 'close']] = df[['open', 'high', 'low', 'close']].astype(float)
        return df
    except BinanceAPIException as e:
        print(f"Error fetching data for {symbol}: {e}")
        return None

# Updated function to identify supply zones (DBD and RBD patterns)
def identify_supply_zones(df):
    supply_zones = []

    for i in range(1, len(df) - 1):
        # Base candle body size
        body_base = abs(df['close'].iloc[i] - df['open'].iloc[i])

        # For DBD Pattern: Drop-Base-Drop
        body_drop_before = abs(df['close'].iloc[i - 1] - df['open'].iloc[i - 1])
        body_drop_after = abs(df['close'].iloc[i + 1] - df['open'].iloc[i + 1])
        is_drop_before = df['close'].iloc[i - 1] < df['open'].iloc[i - 1]
        is_drop_after = df['close'].iloc[i + 1] < df['open'].iloc[i + 1]
        body_condition = body_drop_before > body_base and body_drop_after > body_base
        if is_drop_before and is_drop_after and body_condition:
            # For DBD, consider High of Base Candle Range to Low of Base Body
            upper_bound = df['high'].iloc[i]
            lower_bound = min(df['open'].iloc[i], df['close'].iloc[i])
            zone = {
                'start_time': df['timestamp'].iloc[i],
                'end_time': df['timestamp'].iloc[i],
                'upper_bound': upper_bound,
                'lower_bound': lower_bound,
                'type': 'DBD'
            }
            supply_zones.append(zone)

        # For RBD Pattern: Rally-Base-Drop
        body_rally = abs(df['close'].iloc[i - 1] - df['open'].iloc[i - 1])
        body_drop_after = abs(df['close'].iloc[i + 1] - df['open'].iloc[i + 1])
        is_rally_before = df['close'].iloc[i - 1] > df['open'].iloc[i - 1]
        is_drop_after = df['close'].iloc[i + 1] < df['open'].iloc[i + 1]
        body_condition = body_rally > body_base and body_drop_after > body_base
        if is_rally_before and is_drop_after and body_condition:
            # For RBD, use High of formation and Low of Base Body
            upper_bound = max(df['high'].iloc[i - 1], df['high'].iloc[i], df['high'].iloc[i + 1])
            lower_bound = min(df['open'].iloc[i], df['close'].iloc[i])
            zone = {
                'start_time': df['timestamp'].iloc[i - 1],
                'end_time': df['timestamp'].iloc[i + 1],
                'upper_bound': upper_bound,
                'lower_bound': lower_bound,
                'type': 'RBD'
            }
            supply_zones.append(zone)

    return supply_zones

# Function to check if the supply zone is fresh, tested, or violated
def check_zone_freshness(df, zone):
    zone_df = df[df['timestamp'] > zone['end_time']]

    if zone_df.empty:
        zone['status'] = 'Fresh'
        return zone

    # Check if price has re-entered the supply zone
    reentries = zone_df[
        (zone_df['high'] >= zone['lower_bound']) &
        (zone_df['low'] <= zone['upper_bound'])
    ]

    if reentries.empty:
        zone['status'] = 'Fresh'
    else:
        # Check if the zone is still valid (price hasn't closed above the zone)
        violations = reentries[reentries['close'] > zone['upper_bound']]
        if not violations.empty:
            zone['status'] = 'Violated'
        else:
            zone['status'] = 'Tested'
    return zone

# Function to plot the candlestick chart with supply zones
def plot_chart(df, zones, symbol, interval):
    fig = go.Figure(data=[go.Candlestick(
        x=df['timestamp'],
        open=df['open'],
        high=df['high'],
        low=df['low'],
        close=df['close'],
        name='Candlesticks'
    )])

    # Add supply zones
    for zone in zones:
        if zone['status'] == 'Fresh':
            color = 'red'
            opacity = 0.2
        elif zone['status'] == 'Tested':
            color = 'orange'
            opacity = 0.2
        else:
            # Skip violated zones
            continue

        fig.add_shape(type='rect',
                      x0=zone['start_time'], y0=zone['lower_bound'],
                      x1=df['timestamp'].iloc[-1], y1=zone['upper_bound'],
                      fillcolor=color, opacity=opacity,
                      line=dict(width=0))

        fig.add_annotation(
            x=zone['start_time'],
            y=zone['upper_bound'],
            xref="x",
            yref="y",
            text=f"{zone['type']} ({zone['status']})",
            showarrow=True,
            arrowhead=1,
            font=dict(color=color)
        )

    fig.update_layout(title=f"{symbol} - {interval} Chart with Supply Zones",
                      xaxis_title='Time',
                      yaxis_title='Price',
                      xaxis_rangeslider_visible=False)

    return plot(fig, output_type='div')

# Define the Flask routes
@app.route('/', methods=['GET'])
def index():
    return '''
    <h1>Supply Zone Finder</h1>
    <form action="/chart" method="post">
        Symbol: <select name="symbol">
    ''' + ''.join([f'<option value="{s}">{s}</option>' for s in symbols]) + '''
        </select><br>
        Timeframe: <select name="interval">
    ''' + ''.join([f'<option value="{t}">{t}</option>' for t in timeframes]) + '''
        </select><br>
        <input type="submit" value="Get Chart">
    </form>
    '''

@app.route('/chart', methods=['POST'])
def chart():
    symbol = request.form['symbol']
    interval = request.form['interval']
    df = get_historical_klines(symbol, interval, limit=150)  # Set limit to 150
    if df is not None:
        zones = identify_supply_zones(df)
        zones = [check_zone_freshness(df, zone) for zone in zones if zone is not None]
        # Filter out violated zones
        zones = [zone for zone in zones if zone['status'] != 'Violated']

        # Sort zones by start_time in descending order to get the most recent zones first
        zones.sort(key=lambda x: x['start_time'], reverse=True)

        # Get the last 2 Fresh zones
        fresh_zones = [zone for zone in zones if zone['status'] == 'Fresh'][:2]

        # Get the last 1 Tested zone
        tested_zones = [zone for zone in zones if zone['status'] == 'Tested'][:1]

        # Combine the selected zones
        selected_zones = fresh_zones + tested_zones

        chart_div = plot_chart(df, selected_zones, symbol, interval)
        return f'''
        <h1>{symbol} - {interval} Chart</h1>
        {chart_div}
        <a href="/">Back</a>
        '''
    else:
        return f'''
        <h1>Error fetching data for {symbol}</h1>
        <a href="/">Back</a>
        '''

# Run the Flask application
if __name__ == '__main__':
    app.run(debug=True)