fixx erorr and coplite code and missing part             #!/usr/bin/env python3
"""
triple_bot.py
One-file merge of:
1. Safe-Pivot futures bot (BODY)
2. Risk / margin / RRR calculator (MIND util)
3. Signal engine + plotter (ACTION util / CLI)
4. CLI helper (from leverage_tool.py)
"""
# -------------------------- 0.  IMPORTS --------------------------
import os
import json
import asyncio
import logging
import httpx
import pytz
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Set
from dataclasses import dataclass, field

import ccxt.async_support as ccxt
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from dotenv import dotenv_values
from rich.console import Console
from rich.live import Live
from rich.table import Table
from rich.panel import Panel
from rich.layout import Layout
from rich.align import Align
from rich.text import Text
from rich.logging import RichHandler

import argparse
import csv
import sys

# -------------------------- 1.  ENV + CONFIG ---------------------
config = dotenv_values(".env")
os.environ.update(config)

SYMBOL        = os.getenv("TRADING_PAIR", "TRB/USDT")
EMERG_STOP    = float(os.getenv("EMERGENCY_STOP_LOSS_USD", 8.0))
SESSION_START = (int(os.getenv("SESSION_START_H", 0)),
                 int(os.getenv("SESSION_START_M", 15)))
SESSION_STOP  = (int(os.getenv("SESSION_STOP_H", 23)),
                 int(os.getenv("SESSION_STOP_M", 45)))

pyramid_levels = []
for lvl in range(1, 6):
    inv = float(os.getenv(f"INVESTMENT_USD_LEVEL{lvl}", 0))
    lev = int(os.getenv(f"LEVERAGE_LEVEL{lvl}", 1))
    if inv:
        pyramid_levels.append({"notional_usd": inv, "leverage": lev})

PAIR_CONFIG = {SYMBOL: {"PYRAMID_LEVELS": pyramid_levels,
                        "EMERGENCY_STOP_LOSS_USD": EMERG_STOP}}

# -------------------------- 2.  RISK UTILS -----------------------
def calculate_margin(position_size: float, leverage: float) -> float:
    if leverage <= 0:
        raise ValueError("Leverage must be >0")
    return position_size / leverage

def estimate_liquidation_price(entry_price: float, leverage: float,
                               position_type: str) -> float:
    if position_type == "long":
        return entry_price * (1 - (1 / leverage))
    if position_type == "short":
        return entry_price * (1 + (1 / leverage))
    raise ValueError("position_type must be 'long' or 'short'")

def calculate_risk_to_reward(entry_price: float, stop_loss: float,
                             target_price: float, position_type: str) -> float:
    if position_type == "long":
        risk, reward = entry_price - stop_loss, target_price - entry_price
    elif position_type == "short":
        risk, reward = stop_loss - entry_price, entry_price - target_price
    else:
        raise ValueError("position_type must be 'long' or 'short'")
    if risk <= 0:
        raise ValueError("Invalid stop-loss")
    return reward / risk

def pivot_points(high, low, close):
    pp = (high + low + close) / 3
    r1 = 2 * pp - low
    r2 = pp + (high - low)
    r3 = r1 + (high - low)
    s1 = 2 * pp - high
    s2 = pp - (high - low)
    s3 = s1 - (high - low)
    return {"PP": pp, "R1": r1, "R2": r2, "R3": r3,
            "S1": s1, "S2": s2, "S3": s3}

def pyramid_entries(base_size, n_layers, scale_factor=0.5):
    sizes = [base_size]
    for i in range(1, n_layers):
        sizes.append(base_size * (scale_factor ** i))
    return sizes

# -------------------------- 3.  SIGNAL ENGINE --------------------
class SignalEngine:
    def __init__(self):
        pass

    @staticmethod
    def total_signal(df: pd.DataFrame, current_candle) -> int:
        pos = df.index.get_loc(current_candle)
        if pos < 2:
            return 0
        c1 = df['Low'].iloc[pos] > df['Low'].iloc[pos - 1]
        c2 = df['Low'].iloc[pos - 1] < df['Low'].iloc[pos - 2]
        c3 = (df['Close'].iloc[pos] - df['Close'].iloc[pos - 1]) / df['Close'].iloc[pos - 1] > 0.005
        if c1 and c2 and c3:
            return 2

        c1 = df['High'].iloc[pos] < df['High'].iloc[pos - 1]
        c2 = df['High'].iloc[pos - 1] > df['High'].iloc[pos - 2]
        c3 = (df['Close'].iloc[pos - 1] - df['Close'].iloc[pos]) / df['Close'].iloc[pos] > 0.005
        if c1 and c2 and c3:
            return 1
        return 0

    @staticmethod
    def add_total_signal(df: pd.DataFrame) -> pd.DataFrame:
        df['TotalSignal'] = df.apply(lambda row: SignalEngine.total_signal(df, row.name), axis=1)
        return df

    @staticmethod
    def add_pointpos(df: pd.DataFrame, signal_col: str = 'TotalSignal') -> pd.DataFrame:
        def _pos(row):
            if row[signal_col] == 2:
                return row['Low'] - 1e-4
            if row[signal_col] == 1:
                return row['High'] + 1e-4
            return np.nan
        df['pointpos'] = df.apply(_pos, axis=1)
        return df

    @staticmethod
    def plot_candlestick_with_signals(df: pd.DataFrame,
                                      start_index: int,
                                      num_rows: int) -> None:
        df_subset = df.iloc[start_index:start_index + num_rows]
        fig = make_subplots(rows=1, cols=1)
        fig.add_trace(go.Candlestick(
            x=df_subset.index,
            open=df_subset['Open'],
            high=df_subset['High'],
            low=df_subset['Low'],
            close=df_subset['Close'],
            name='Candles'), row=1, col=1)
        fig.add_trace(go.Scatter(
            x=df_subset.index,
            y=df_subset['pointpos'],
            mode="markers",
            marker=dict(size=10, color="MediumPurple", symbol='circle'),
            name="Entry Points"), row=1, col=1)
        fig.update_layout(
            width=1200, height=800,
            plot_bgcolor='black', paper_bgcolor='black',
            font=dict(color='white'),
            xaxis=dict(showgrid=False), yaxis=dict(showgrid=False),
            showlegend=True, legend=dict(x=0.01, y=0.99, bgcolor="black", bordercolor="gray"))
        fig.show()

# -------------------------- 4.  GATEWAY --------------------------
class ExchangeGateway:
    def __init__(self):
        self.ex = ccxt.binance({
            'apiKey': os.getenv('BINANCE_API_KEY'),
            'secret': os.getenv('BINANCE_API_SECRET'),
            'enableRateLimit': True,
            'options': {'defaultType': 'future'}
        })
        self.symbol = SYMBOL
        self.market = None

    async def __aenter__(self):
        await self.ex.load_markets()
        self.market = self.ex.market(self.symbol)
        return self
    
    async def __aexit__(self, *_): await self.ex.close()

    async def ticker(self): return await self.ex.fetch_ticker(self.symbol)
    async def ohlcv(self, tf='1h', limit=200):
        return await self.ex.fetch_ohlcv(self.symbol, tf, limit=limit)
    async def balance(self): return await self.ex.fetch_balance()
    async def positions(self):
        return [p for p in await self.ex.fetch_positions([self.symbol]) if p['contracts']]
    async def open_orders(self): return await self.ex.fetch_open_orders(self.symbol)
    async def create_market(self, side, qty, params=None):
        return await self.ex.create_market_order(self.symbol, side, qty, params=params or {})
    async def set_lev(self, lev): await self.ex.set_leverage(lev, self.symbol)
    async def cancel_all(self):
        for o in await self.open_orders():
            await self.ex.cancel_order(o['id'], self.symbol)
    
# -------------------------- 5.  TRADER BRAIN ---------------------
@dataclass
class TraderBrain:
    symbol: str
    levels: list
    max_entries: int = field(init=False)
    triggered: Set[str] = field(default_factory=set)
    entry_index: int = 0
    pivots: Dict[str, float] = field(default_factory=dict)
    def __post_init__(self): self.max_entries = len(self.levels)

    def load(self, file="bot_state.json"):
        if os.path.exists(file):
            with open(file) as f:
                d = json.load(f)
                self.triggered = set(d.get('triggered', []))
                self.entry_index = d.get('entry_index', 0)

    def save(self, file="bot_state.json"):
        with open(file, 'w') as f:
            json.dump({'triggered': list(self.triggered), 'entry_index': self.entry_index}, f)

    def clear(self, file="bot_state.json"):
        if os.path.exists(file):
            os.remove(file)
        self.triggered.clear()
        self.entry_index = 0

# -------------------------- 6.  NOTIFIER / DASH ------------------
class Notifier:
    def __init__(self):
        self.tok = os.getenv("TELEGRAM_TOKEN")
        self.chat = os.getenv("TELEGRAM_CHAT_ID")
        self.cli = httpx.AsyncClient() if self.tok else None
    async def __aenter__(self):
        return self
    async def __aexit__(self, *_):
        if self.cli: await self.cli.aclose()
    async def send(self, txt: str):
        if not self.cli: return
        url = f"https://api.telegram.org/bot{self.tok}/sendMessage"
        await self.cli.post(url, json={'chat_id': self.chat, 'text': txt, 'parse_mode': 'Markdown'})

class Dashboard:
    def __init__(self, brain, gw):
        self.brain, self.gw = brain, gw
    def layout(self, tick, bal, pos, orders):
        now = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')
        layout = Layout(name="root")
        layout.split(Layout(name="header", size=3), Layout(name="body"))
        layout["body"].split_row(Layout(name="L"), Layout(name="R"))
        layout["header"].update(Panel(
            Align.center(Text(f"Triple-Bot {now}", justify="center"), vertical="middle"),
            style="white on blue"))
        tbl = Table(show_header=False, box=None)
        tbl.add_column("k", style="cyan")
        tbl.add_column("v", style="white")
        tbl.add_row("Symbol", self.brain.symbol)
        tbl.add_row("Price", f"${tick['last']:.4f}")
        tbl.add_row("USDT Total", f"${bal['total'].get('USDT', 0):.2f}")
        tbl.add_row("Triggered", ",".join(sorted(self.brain.triggered)) or "None")
        tbl.add_row("Entry", f"{self.brain.entry_index}/{self.brain.max_entries}")
        layout["L"].update(Panel(tbl, title="[magenta]Status[/magenta]"))
        p_tbl = Table()
        p_tbl.add_column("Lvl")
        p_tbl.add_column("Price")
        for k, v in sorted(self.brain.pivots.items()):
            p_tbl.add_row(k, f"{v:.4f}")
        layout["R"].update(Panel(p_tbl, title="[green]Pivots[/green]"))
        return layout

# -------------------------- 7.  APP ORCHESTRATOR -----------------
class App:
    def __init__(self):
        self.cfg = PAIR_CONFIG[SYMBOL]
        self.brain = TraderBrain(SYMBOL, self.cfg["PYRAMID_LEVELS"])
        self.note = Notifier()

    async def run(self):
        async with ExchangeGateway() as gw, self.note:
            await gw.set_lev(self.cfg["PYRAMID_LEVELS"][0]["leverage"])
            self.brain.load()
            await self.note.send("🤖 *Bot Started*")

            while True:
                now = pytz.UTC.localize(datetime.utcnow())
                start = now.replace(hour=SESSION_START[0], minute=SESSION_START[1],
                                    second=0, microsecond=0)
                stop = now.replace(hour=SESSION_STOP[0], minute=SESSION_STOP[1],
                                   second=0, microsecond=0)
                if now < start:
                    await asyncio.sleep((start - now).total_seconds())
                    continue
                if not self.brain.pivots:
                    await self.calc_pivots(gw)
                dash = Dashboard(self.brain, gw)
                with Live(dash.layout(await gw.ticker(), await gw.balance(),
                                      await gw.positions(), await gw.open_orders()),
                          refresh_per_second=4, screen=True):
                    while start <= now < stop:
                        await self.tick(gw)
                        await asyncio.sleep(1)
                        now = pytz.UTC.localize(datetime.utcnow())
                await self.shutdown(gw, "Session ended")
                next_start = start + timedelta(days=1)
                await asyncio.sleep((next_start - now).total_seconds())

    async def calc_pivots(self, gw):
        ohlcv = await gw.ohlcv('1d', 2)
        _, o, h, l, c, _ = ohlcv[-2]
        p = (h + l + c) / 3
        self.brain.pivots = {k: v for k, v in {
            'P': p, 'R1': 2 * p - l, 'S1': 2 * p - h,
            'R2': p + (h - l), 'S2': p - (h - l),
            'R3': p + 2 * (h - l), 'S3': p - 2 * (h - l),
            'R4': p + 3 * (h - l), 'S4': p - 3 * (h - l),
            'R5': p + 4 * (h - l), 'S5': p - 4 * (h - l),
        }.items()}

    async def tick(self, gw):
        pos = next((p for p in await gw.positions() if float(p['contracts']) > 0), None)
        if pos and float(pos['unrealizedPnl']) <= -self.cfg['EMERGENCY_STOP_LOSS_USD']:
            await self.shutdown(gw, "Emergency stop-loss")
            raise asyncio.CancelledError("Emergency stop")
        await self.evaluate_entries(gw)

    async def evaluate_entries(self, gw):
        if self.brain.entry_index >= self.brain.max_entries:
            return
        tick = await gw.ticker()
        price = tick['last']
        level = self.cfg["PYRAMID_LEVELS"][self.brain.entry_index]
        for name, pivot in self.brain.pivots.items():
            if name in self.brain.triggered or name == 'P':
                continue
            # BUY on pivot bounce (example rule)
            if name.startswith('S') and price > pivot:
                await self.enter_trade(gw, name, price, level)
                break

    async def enter_trade(self, gw, pivot, price, level):
        qty = round(level['notional_usd'] / price, gw.market['precision']['amount'])
        await gw.set_lev(level['leverage'])
        await gw.create_market('buy', qty)
        self.brain.triggered.add(pivot)
        self.brain.entry_index += 1
        self.brain.save()
        msg = (f"📈 BUY {SYMBOL} @ {pivot} "
               f"(${level['notional_usd']} @ {level['leverage']}x)")
        await self.note.send(msg)
        await self.set_sl_tp(gw, price)

    async def set_sl_tp(self, gw, entry):
        pos = next((p for p in await gw.positions() if float(p['contracts']) > 0), None)
        if not pos:
            return
        qty = float(pos['contracts'])
        await gw.cancel_all()
        sl_price = round(entry * 0.97, 4)
        tp_price = round(entry * 1.05, 4)
        await gw.ex.create_order(
            SYMBOL, 'STOP_MARKET', 'sell', qty,
            params={'stopPrice': sl_price, 'closePosition': True, 'workingType': 'MARK_PRICE'})
        await gw.ex.create_order(
            SYMBOL, 'TAKE_PROFIT_MARKET', 'sell', qty,
            params={'stopPrice': tp_price, 'closePosition': True, 'workingType': 'MARK_PRICE'})
        await self.note.send("🔒 SL/TP set")

    async def shutdown(self, gw, reason: str):
        await self.note.send(f"🛑 {reason}. Closing.")
        await gw.cancel_all()
        pos = next((p for p in await gw.positions() if float(p['contracts']) > 0), None)
        if pos:
            await gw.create_market('sell', float(pos['contracts']), {'reduceOnly': True})
        self.brain.clear()
    
# -------------------------- 8.  CLI helper -----------------------
CSV_FILE = "triple_bot_risk_log.csv"

def log_to_csv(row_dict):
    file_exists = os.path.isfile(CSV_FILE)
    with open(CSV_FILE, "a", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=list(row_dict.keys()))
        if not file_exists:
            writer.writeheader()
        writer.writerow(row_dict)

def cli_mode():
    p = argparse.ArgumentParser(description="Risk / margin / RRR helper")
    p.add_argument("--side", choices=["long", "short"], required=True)
    p.add_argument("--account", type=float, required=True)
    p.add_argument("--risk", type=float, required=True, help="risk % (0-100)")
    p.add_argument("--lev", type=float, required=True)
    p.add_argument("--entry", type=float, required=True)
    p.add_argument("--sl", type=float, required=True)
    p.add_argument("--tp", type=float, required=True)
    p.add_argument("--tick", type=float, default=0.01)
    p.add_argument("--pyramid", type=int, default=0, help="extra pyramid layers")
    p.add_argument("--csv", action="store_true")
    args = p.parse_args()

    stop_pct = abs(args.entry - args.sl) / args.entry
    pos_size = args.account * (args.risk/100) / stop_pct
    margin   = calculate_margin(pos_size, args.lev)
    liq      = round(estimate_liquidation_price(args.entry, args.lev, args.side) /
                     args.tick) * args.tick
    rrr      = calculate_risk_to_reward(args.entry, args.sl, args.tp, args.side)

    sizes = pyramid_entries(pos_size, args.pyramid + 1)
    total_sz   = sum(sizes)
    total_marg = calculate_margin(total_sz, args.lev)

    print(f"size={pos_size:.2f} margin={margin:.2f} liq={liq:.4f} rrr={rrr:.2f}")
    if args.pyramid:
        print(f"pyramid_total_size={total_sz:.2f} pyramid_total_margin={total_marg:.2f}")
    if args.csv:
        log_to_csv({
            "datetime": datetime.utcnow().isoformat(timespec="seconds"),
            "side": args.side, "size": pos_size, "leverage": args.lev,
            "entry": args.entry, "sl": args.sl, "tp": args.tp,
            "liq": liq, "rrr": round(rrr, 2)
        })

# -------------------------- 9.  ENTRY POINT ----------------------
if __name__ == '__main__':
    logging.basicConfig(
        level=logging.INFO,
        format="%(message)s",
        datefmt="[%X]",
        handlers=[RichHandler(rich_tracebacks=True)]
    )
    if len(sys.argv) > 1 and any(arg.startswith("--") for arg in sys.argv[1:]):
        cli_mode()
    elif len(sys.argv) > 1 and os.path.isfile("leverage_tool.py"):
        # optional fallback to old interactive mode
        import leverage_tool as lt # type: ignore
        lt.interactive_mode()
    else:
        asyncio.run(App().run())