v0.6: Contrarian Buy/Sell (Mean Reversion) - Buy on market -2%, sell on market +2%
This commit is contained in:
parent
7ca9756998
commit
e78b2ffada
209
src/main_ml.py
209
src/main_ml.py
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Trading Bot v0.5.1 - RSI + Bollinger Bands HYBRID (Confidence Filter)"""
|
||||
"""Trading Bot v0.6 - Contrarian Buy/Sell (Mean Reversion) Strategy"""
|
||||
import os, json, time, logging, sqlite3
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from dotenv import load_dotenv
|
||||
from binance.client import Client
|
||||
|
||||
|
|
@ -23,15 +23,17 @@ MAX_POSITION_PCT = 0.07
|
|||
TAKE_PROFIT_PCT = 0.015
|
||||
STOP_LOSS_PCT = -0.008
|
||||
CYCLE_SEC = 60
|
||||
BB_PERIOD = 20
|
||||
BB_STD_DEV = 2.0
|
||||
RSI_PERIOD = 14
|
||||
RSI_THRESHOLD = 35
|
||||
|
||||
class TradingBotV051:
|
||||
# CONTRARIAN THRESHOLDS
|
||||
CONTRARIAN_BUY_THRESHOLD = -2.0 # Buy when market DOWN 2%+
|
||||
CONTRARIAN_SELL_THRESHOLD = +2.0 # Sell when market UP 2%+
|
||||
LOOKBACK_HOURS = 24 # Compare last 24h return
|
||||
|
||||
class TradingBotV06:
|
||||
def __init__(self):
|
||||
self.client = Client(API_KEY, API_SECRET)
|
||||
self.price_history = {sym: [] for sym in SYMBOLS}
|
||||
self.daily_opens = {} # Store 24h ago prices
|
||||
self.active_trades = {}
|
||||
self.portfolio_value = 0
|
||||
self.max_trade_usdt = 0
|
||||
|
|
@ -59,88 +61,52 @@ class TradingBotV051:
|
|||
except Exception as e:
|
||||
logger.warning(f"Recovery failed: {e}")
|
||||
|
||||
logger.info("[v0.5.1 INIT] RSI + Bollinger Bands HYBRID (Confidence Filter)")
|
||||
logger.info("[v0.6 INIT] Contrarian Buy/Sell (Mean Reversion) Strategy")
|
||||
|
||||
def calculate_rsi(self, prices):
|
||||
"""Calculate RSI (14-period standard)"""
|
||||
if len(prices) < RSI_PERIOD + 1:
|
||||
return None
|
||||
def calculate_market_return(self):
|
||||
"""Calculate 24h market-wide return (Average of all symbols)"""
|
||||
returns = []
|
||||
|
||||
recent = prices[-RSI_PERIOD-1:]
|
||||
deltas = [recent[i+1] - recent[i] for i in range(len(recent)-1)]
|
||||
for symbol in SYMBOLS:
|
||||
if len(self.price_history[symbol]) < 2:
|
||||
continue
|
||||
|
||||
gains = [d if d > 0 else 0 for d in deltas]
|
||||
losses = [abs(d) if d < 0 else 0 for d in deltas]
|
||||
current = self.price_history[symbol][-1]
|
||||
# Get price from ~24h ago (or earliest if less than 24h data)
|
||||
reference_idx = max(0, len(self.price_history[symbol]) - 1440) # 1440 = 24h * 60min
|
||||
reference = self.price_history[symbol][reference_idx]
|
||||
|
||||
avg_gain = sum(gains) / RSI_PERIOD
|
||||
avg_loss = sum(losses) / RSI_PERIOD
|
||||
if reference > 0:
|
||||
ret = ((current - reference) / reference) * 100
|
||||
returns.append(ret)
|
||||
|
||||
if avg_loss == 0:
|
||||
return 100.0 if avg_gain > 0 else 0.0
|
||||
if returns:
|
||||
avg_return = sum(returns) / len(returns)
|
||||
return avg_return
|
||||
|
||||
rs = avg_gain / avg_loss
|
||||
rsi = 100 - (100 / (1 + rs))
|
||||
return 0.0
|
||||
|
||||
return rsi
|
||||
def is_contrarian_buy_signal(self, symbol):
|
||||
"""Buy when MARKET DOWN 2%+ (Mean Reversion: expect bounce)"""
|
||||
market_return = self.calculate_market_return()
|
||||
|
||||
def calculate_bollinger_bands(self, prices):
|
||||
"""Calculate 20-EMA +/- 2*StdDev"""
|
||||
if len(prices) < BB_PERIOD:
|
||||
return None, None, None
|
||||
buy_signal = market_return < CONTRARIAN_BUY_THRESHOLD
|
||||
|
||||
# EMA-20
|
||||
ema = prices[-1]
|
||||
alpha = 2.0 / (BB_PERIOD + 1)
|
||||
for price in prices[-BB_PERIOD:]:
|
||||
ema = (price * alpha) + (ema * (1 - alpha))
|
||||
if buy_signal:
|
||||
logger.info(f"[SIGNAL-CONTRARIAN-BUY] Market DOWN {market_return:.2f}% (Threshold: {CONTRARIAN_BUY_THRESHOLD}%)")
|
||||
|
||||
# StdDev of last 20 prices
|
||||
recent_prices = prices[-BB_PERIOD:]
|
||||
mean = sum(recent_prices) / BB_PERIOD
|
||||
variance = sum((p - mean) ** 2 for p in recent_prices) / BB_PERIOD
|
||||
std_dev = variance ** 0.5
|
||||
return buy_signal
|
||||
|
||||
upper_band = ema + (BB_STD_DEV * std_dev)
|
||||
lower_band = ema - (BB_STD_DEV * std_dev)
|
||||
def is_contrarian_sell_signal(self, symbol):
|
||||
"""Sell when MARKET UP 2%+ (Take profits on rally)"""
|
||||
market_return = self.calculate_market_return()
|
||||
|
||||
return ema, upper_band, lower_band
|
||||
sell_signal = market_return > CONTRARIAN_SELL_THRESHOLD
|
||||
|
||||
def is_hybrid_buy_signal(self, symbol):
|
||||
"""
|
||||
HYBRID Signal: Buy ONLY when BOTH conditions met:
|
||||
1. Price rebounds from lower Bollinger Band (BB Breakout)
|
||||
2. RSI < 35 (Oversolod confirmation)
|
||||
"""
|
||||
if len(self.price_history[symbol]) < max(BB_PERIOD + 1, RSI_PERIOD + 1):
|
||||
return False
|
||||
if sell_signal:
|
||||
logger.info(f"[SIGNAL-CONTRARIAN-SELL] Market UP {market_return:.2f}% (Threshold: {CONTRARIAN_SELL_THRESHOLD}%)")
|
||||
|
||||
prices = self.price_history[symbol]
|
||||
ema, upper, lower = self.calculate_bollinger_bands(prices)
|
||||
|
||||
if not ema or not lower:
|
||||
return False
|
||||
|
||||
current_price = prices[-1]
|
||||
prev_price = prices[-2]
|
||||
|
||||
# Signal 1: Bollinger Breakout
|
||||
bb_breakout = (prev_price < lower and current_price > lower)
|
||||
|
||||
# Signal 2: RSI Oversold
|
||||
rsi = self.calculate_rsi(prices)
|
||||
rsi_oversold = (rsi is not None and rsi < RSI_THRESHOLD)
|
||||
|
||||
# HYBRID: Both must be true
|
||||
hybrid_signal = bb_breakout and rsi_oversold
|
||||
|
||||
if hybrid_signal:
|
||||
logger.info(f"[SIGNAL-HYBRID] {symbol} RSI={rsi:.1f} + BB-Breakout (EMA={ema:.2f}, Lower={lower:.2f})")
|
||||
elif bb_breakout and not rsi_oversold:
|
||||
logger.debug(f"[FILTERED] {symbol} BB-Breakout but RSI={rsi:.1f} (need <{RSI_THRESHOLD})")
|
||||
elif rsi_oversold and not bb_breakout:
|
||||
logger.debug(f"[FILTERED] {symbol} RSI={rsi:.1f} but no BB-Breakout")
|
||||
|
||||
return hybrid_signal
|
||||
return sell_signal
|
||||
|
||||
def get_fresh_balance(self):
|
||||
try:
|
||||
|
|
@ -171,7 +137,7 @@ class TradingBotV051:
|
|||
self.portfolio_value = portfolio_value
|
||||
self.max_trade_usdt = portfolio_value * MAX_POSITION_PCT
|
||||
|
||||
logger.info(f"[v0.5.1] USDT={usdt_available:.2f} | Portfolio={portfolio_value:.2f} | Max={self.max_trade_usdt:.2f}")
|
||||
logger.info(f"[v0.6] USDT={usdt_available:.2f} | Portfolio={portfolio_value:.2f} | Max={self.max_trade_usdt:.2f}")
|
||||
return usdt_available, portfolio_value
|
||||
except:
|
||||
return 0, 0
|
||||
|
|
@ -231,7 +197,27 @@ class TradingBotV051:
|
|||
'entry_time': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"[BUY-v0.5.1] {symbol} {qty} @ {price} (RSI+BB HYBRID)")
|
||||
logger.info(f"[BUY-v0.6] {symbol} {qty} @ {price} (CONTRARIAN: Market DOWN)")
|
||||
return order
|
||||
except:
|
||||
return None
|
||||
|
||||
def place_sell_order(self, symbol):
|
||||
try:
|
||||
if symbol not in self.active_trades:
|
||||
return None
|
||||
|
||||
qty = self.active_trades[symbol]['qty']
|
||||
|
||||
order = self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||
|
||||
price = self.get_current_price(symbol)
|
||||
entry = self.active_trades[symbol]['entry_price']
|
||||
pnl = ((price - entry) / entry) * 100
|
||||
|
||||
logger.info(f"[SELL-v0.6] {symbol} {qty} @ {price} (CONTRARIAN: Market UP, P&L: {pnl:+.2f}%)")
|
||||
|
||||
del self.active_trades[symbol]
|
||||
return order
|
||||
except:
|
||||
return None
|
||||
|
|
@ -247,6 +233,7 @@ class TradingBotV051:
|
|||
qty = trade['qty']
|
||||
pnl_pct = ((current - entry) / entry) * 100
|
||||
|
||||
# TP Hit
|
||||
if pnl_pct >= TAKE_PROFIT_PCT * 100:
|
||||
logger.info(f"[SELL-TP] {symbol} +{pnl_pct:.2f}%")
|
||||
try:
|
||||
|
|
@ -255,6 +242,7 @@ class TradingBotV051:
|
|||
except:
|
||||
pass
|
||||
|
||||
# SL Hit
|
||||
elif pnl_pct <= STOP_LOSS_PCT * 100:
|
||||
logger.info(f"[SELL-SL] {symbol} {pnl_pct:.2f}%")
|
||||
try:
|
||||
|
|
@ -293,26 +281,63 @@ class TradingBotV051:
|
|||
logger.info("="*70)
|
||||
return
|
||||
|
||||
self.check_and_close_positions()
|
||||
|
||||
# Update price history
|
||||
for symbol in SYMBOLS:
|
||||
price = self.get_current_price(symbol)
|
||||
if price:
|
||||
self.price_history[symbol].append(price)
|
||||
if len(self.price_history[symbol]) > 100:
|
||||
if len(self.price_history[symbol]) > 1440: # Keep 24h history
|
||||
self.price_history[symbol].pop(0)
|
||||
|
||||
# Find HYBRID signal (RSI + BB both true)
|
||||
best_signal = None
|
||||
for symbol in SYMBOLS:
|
||||
if symbol not in self.active_trades and self.is_hybrid_buy_signal(symbol):
|
||||
best_signal = symbol
|
||||
break
|
||||
# Check for Contrarian SELL (Market UP 2%+)
|
||||
if self.is_contrarian_sell_signal(None):
|
||||
# Sell holdings that are profitable
|
||||
for symbol in list(self.active_trades.keys()):
|
||||
if symbol not in self.active_trades:
|
||||
continue
|
||||
|
||||
if best_signal and usdt_free >= MIN_TRADE_USDT:
|
||||
current = self.get_current_price(symbol)
|
||||
if not current:
|
||||
continue
|
||||
|
||||
entry = self.active_trades[symbol]['entry_price']
|
||||
pnl_pct = ((current - entry) / entry) * 100
|
||||
|
||||
# Only sell if we have profit (avoid unnecessary SL hits on rally)
|
||||
if pnl_pct > 0.5:
|
||||
self.place_sell_order(symbol)
|
||||
break # One sell per cycle
|
||||
|
||||
# Check TP/SL
|
||||
self.check_and_close_positions()
|
||||
|
||||
# Check for Contrarian BUY (Market DOWN 2%+)
|
||||
buy_signal = self.is_contrarian_buy_signal(None)
|
||||
if buy_signal and usdt_free >= MIN_TRADE_USDT:
|
||||
# Find best coin to buy (the one with biggest loss)
|
||||
worst_coin = None
|
||||
worst_return = 0
|
||||
|
||||
for symbol in SYMBOLS:
|
||||
if symbol in self.active_trades:
|
||||
continue # Skip already held
|
||||
|
||||
if len(self.price_history[symbol]) < 2:
|
||||
continue
|
||||
|
||||
current = self.price_history[symbol][-1]
|
||||
ref_idx = max(0, len(self.price_history[symbol]) - 1440)
|
||||
reference = self.price_history[symbol][ref_idx]
|
||||
|
||||
if reference > 0:
|
||||
ret = ((current - reference) / reference) * 100
|
||||
if ret < worst_return:
|
||||
worst_return = ret
|
||||
worst_coin = symbol
|
||||
|
||||
if worst_coin:
|
||||
trade_amount = min(max(MIN_TRADE_USDT, self.max_trade_usdt), usdt_free * 0.5)
|
||||
self.place_buy_order(best_signal, trade_amount)
|
||||
self.place_buy_order(worst_coin, trade_amount)
|
||||
|
||||
# Save trades
|
||||
try:
|
||||
|
|
@ -324,7 +349,7 @@ class TradingBotV051:
|
|||
'portfolio_value': round(portfolio_val, 2),
|
||||
'max_trade_usdt': round(self.max_trade_usdt, 2),
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'version': 'v0.5.1-rsi-bb-hybrid'
|
||||
'version': 'v0.6-contrarian-mean-reversion'
|
||||
}, f)
|
||||
os.replace(temp, '/home/marc/bot-deploy/active_trades.json')
|
||||
except:
|
||||
|
|
@ -333,18 +358,18 @@ class TradingBotV051:
|
|||
# Save P&L
|
||||
self.save_pnl_to_db(portfolio_val, usdt_free)
|
||||
|
||||
logger.info(f"[CYCLE-END] Trades={len(self.active_trades)} | Portfolio={portfolio_val:.2f} [v0.5.1]")
|
||||
logger.info(f"[CYCLE-END] Trades={len(self.active_trades)} | Portfolio={portfolio_val:.2f} [v0.6]")
|
||||
logger.info("="*70)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
bot = TradingBotV051()
|
||||
bot = TradingBotV06()
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == '--once':
|
||||
bot.run_cycle()
|
||||
else:
|
||||
logger.info("[v0.5.1 START] Trading Bot with RSI + Bollinger Bands HYBRID signals...")
|
||||
logger.info("[v0.6 START] Trading Bot with Contrarian Buy/Sell (Mean Reversion)...")
|
||||
while True:
|
||||
try:
|
||||
bot.run_cycle()
|
||||
|
|
|
|||
|
|
@ -1,336 +1,141 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Trading Bot Dashboard v0.5.1 (RSI + Bollinger Bands HYBRID) - Auto-load 1-Day chart on page load"""
|
||||
import sqlite3
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import HTMLResponse
|
||||
"""Trading Bot Dashboard v0.6 (Contrarian Mean Reversion) - Auto-load 1-Day chart on page load"""
|
||||
import os, json, logging, sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, render_template_string, jsonify
|
||||
from binance.client import Client
|
||||
from datetime import datetime
|
||||
import json, os, time
|
||||
from dotenv import load_dotenv
|
||||
|
||||
app = FastAPI()
|
||||
load_dotenv()
|
||||
API_KEY = os.getenv('BINANCE_API_KEY_LIVE')
|
||||
API_SECRET = os.getenv('BINANCE_API_SECRET_LIVE')
|
||||
client = Client(API_KEY, API_SECRET)
|
||||
|
||||
env = {}
|
||||
with open('/home/marc/bot-deploy/.env') as f:
|
||||
for line in f:
|
||||
k, _, v = line.partition('=')
|
||||
env[k.strip()] = v.strip()
|
||||
app = Flask(__name__)
|
||||
|
||||
binance = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||
DB = '/home/marc/bot-deploy/pnl_charts.db'
|
||||
|
||||
def init_db():
|
||||
c = sqlite3.connect(DB).cursor()
|
||||
c.execute("""CREATE TABLE IF NOT EXISTS history (ts INTEGER PRIMARY KEY, pv REAL, pu REAL, pp REAL, uf REAL, ap INTEGER)""")
|
||||
sqlite3.connect(DB).commit()
|
||||
|
||||
init_db()
|
||||
|
||||
@app.get('/api/state')
|
||||
async def state():
|
||||
try:
|
||||
acc = binance.get_account()
|
||||
bal = {}
|
||||
for a in acc['balances']:
|
||||
ast, free, locked = a['asset'], float(a['free']), float(a['locked'])
|
||||
if free + locked > 1e-5:
|
||||
bal[ast] = {'free': free, 'locked': locked, 'total': free + locked}
|
||||
|
||||
prices = {'USDT': 1.0}
|
||||
for p in ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']:
|
||||
try:
|
||||
t = binance.get_ticker(symbol=p)
|
||||
prices[p.replace('USDT', '')] = float(t['lastPrice'])
|
||||
except: pass
|
||||
|
||||
pv = sum(bal.get(a, {}).get('total', 0) * prices.get(a, 0) for a in ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT'])
|
||||
uf = bal.get('USDT', {}).get('free', 0)
|
||||
# Get latest P&L from database
|
||||
try:
|
||||
conn = sqlite3.connect('/home/marc/bot-deploy/pnl_charts.db')
|
||||
row = conn.execute('SELECT pu, pp FROM history ORDER BY ts DESC LIMIT 1').fetchone()
|
||||
conn.close()
|
||||
if row:
|
||||
pu, pp = row[0], row[1]
|
||||
else:
|
||||
pu, pp = 0.0, 0.0
|
||||
except:
|
||||
pu, pp = 0.0, 0.0
|
||||
|
||||
ap = 0
|
||||
try:
|
||||
with open('/home/marc/bot-deploy/active_trades.json') as f:
|
||||
ap = json.load(f).get('count', 0)
|
||||
except: pass
|
||||
|
||||
conn = sqlite3.connect(DB)
|
||||
conn.execute("INSERT OR REPLACE INTO history VALUES (?, ?, ?, ?, ?, ?)", (int(time.time()), pv, pu, pp, uf, ap))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return {'portfolio_value': round(pv, 2), 'pnl_usdt': round(pu, 2), 'pnl_pct': round(pp, 2), 'usdt_free': round(uf, 2), 'active_positions': ap, 'balance': bal, 'prices': prices}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
@app.get('/api/pnl-history')
|
||||
async def history(hours: int = 24):
|
||||
conn = sqlite3.connect(DB)
|
||||
cutoff = int(time.time()) - hours * 3600
|
||||
rows = conn.execute("SELECT ts, pp, pu FROM history WHERE ts > ? ORDER BY ts", (cutoff,)).fetchall()
|
||||
conn.close()
|
||||
|
||||
ts_list, pcts, usdts = [], [], []
|
||||
seen_ts = set()
|
||||
|
||||
for t, p, u in rows:
|
||||
dt = datetime.fromtimestamp(t)
|
||||
|
||||
if hours <= 24:
|
||||
ts = dt.strftime('%H:00')
|
||||
else:
|
||||
ts = dt.strftime('%d.%m.%y')
|
||||
|
||||
if ts in seen_ts:
|
||||
continue
|
||||
|
||||
seen_ts.add(ts)
|
||||
ts_list.append(ts)
|
||||
pcts.append(round(p, 2))
|
||||
usdts.append(round(u, 2))
|
||||
|
||||
return {'timestamps': ts_list, 'pnl_pcts': pcts, 'pnl_usdts': usdts,
|
||||
'current_pct': pcts[-1] if pcts else 0, 'current_usdt': usdts[-1] if usdts else 0,
|
||||
'min_pct': min(pcts) if pcts else 0, 'min_usdt': min(usdts) if usdts else 0,
|
||||
'max_pct': max(pcts) if pcts else 0, 'max_usdt': max(usdts) if usdts else 0,
|
||||
'avg_pct': sum(pcts)/len(pcts) if pcts else 0, 'avg_usdt': sum(usdts)/len(usdts) if usdts else 0}
|
||||
|
||||
@app.get('/')
|
||||
async def dashboard():
|
||||
html = """<!DOCTYPE html>
|
||||
@app.route('/')
|
||||
def dashboard():
|
||||
html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Trading Bot v0.5.1</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<title>Trading Bot v0.6</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:Segoe UI,Arial;background:#1e1e1e;color:#d0d0d0;min-height:100vh;padding:20px}
|
||||
@media(max-width:768px){body{padding:10px}.container{max-width:100%}}
|
||||
.container{max-width:1400px;margin:0 auto}
|
||||
.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:30px;padding:20px;background:rgba(0,255,136,.05);border:1px solid rgba(0,255,136,.2);border-radius:10px}
|
||||
@media(max-width:768px){.header{flex-direction:column;gap:15px;padding:15px}}
|
||||
.header h1{font-size:28px;color:#00ff88}
|
||||
@media(max-width:768px){.header h1{font-size:20px}}
|
||||
.status{padding:8px 16px;background:rgba(0,255,136,.1);border:2px solid #00ff88;border-radius:20px;font-weight:bold}
|
||||
.tabs{display:flex;gap:10px;margin-bottom:20px}
|
||||
.btn{padding:12px 24px;background:0;border:0;color:#999;cursor:pointer;font-size:16px;border-bottom:3px solid transparent;transition:all .3s}
|
||||
@media(max-width:768px){.btn{padding:10px 16px;font-size:14px}}
|
||||
.btn:hover{color:#00ff88}
|
||||
.btn.active{color:#00ff88;border-bottom-color:#00ff88}
|
||||
.tab{display:none}
|
||||
.tab.active{display:block}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin-bottom:30px}
|
||||
@media(max-width:768px){.grid{grid-template-columns:1fr}}
|
||||
.card{background:rgba(255,255,255,.03);border:1px solid rgba(0,255,136,.2);border-radius:10px;padding:20px;transition:all .3s}
|
||||
@media(max-width:768px){.card{padding:15px}}
|
||||
.card:hover{border-color:rgba(0,255,136,.5)}
|
||||
.lbl{font-size:12px;color:#888;text-transform:uppercase;margin-bottom:8px}
|
||||
.val{font-size:24px;color:#00ff88;font-weight:bold}
|
||||
@media(max-width:768px){.val{font-size:20px}}
|
||||
.sub{font-size:14px;color:#999}
|
||||
.collapse-header{display:flex;justify-content:space-between;align-items:center;padding:15px 20px;background:transparent;border:1px solid rgba(0,255,136,.2);border-radius:10px;cursor:pointer;margin:20px 0 15px 0}
|
||||
@media(max-width:768px){.collapse-header{padding:12px 15px}}
|
||||
.collapse-header h3{color:#00ff88;font-size:16px;margin:0}
|
||||
@media(max-width:768px){.collapse-header h3{font-size:14px}}
|
||||
.collapse-toggle{color:#00ff88;font-size:20px}
|
||||
.holdings{display:none;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin-bottom:20px}
|
||||
@media(max-width:768px){.holdings{grid-template-columns:1fr}}
|
||||
.holdings.open{display:grid}
|
||||
.chart-box{background:rgba(255,255,255,.03);border:1px solid rgba(0,255,136,.2);border-radius:10px;padding:20px}
|
||||
@media(max-width:768px){.chart-box{padding:15px}}
|
||||
.title{font-size:18px;color:#00ff88;margin-bottom:20px;font-weight:bold}
|
||||
@media(max-width:768px){.title{font-size:14px}}
|
||||
.times{display:flex;gap:10px;margin-bottom:20px;flex-wrap:wrap}
|
||||
.time{padding:8px 16px;background:rgba(0,255,136,.1);border:1px solid rgba(0,255,136,.3);color:#00ff88;border-radius:5px;cursor:pointer;font-size:14px}
|
||||
@media(max-width:768px){.time{padding:6px 12px;font-size:12px}}
|
||||
.time:hover{background:rgba(0,255,136,.2)}
|
||||
.time.active{background:rgba(0,255,136,.3)}
|
||||
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-top:20px}
|
||||
@media(max-width:768px){.stats{grid-template-columns:repeat(2,1fr);gap:10px}}
|
||||
.stat{background:rgba(0,255,136,.05);border:1px solid rgba(0,255,136,.15);padding:15px;border-radius:8px;text-align:center}
|
||||
@media(max-width:768px){.stat{padding:12px}}
|
||||
.stat-l{font-size:11px;color:#888;text-transform:uppercase;margin-bottom:5px}
|
||||
@media(max-width:768px){.stat-l{font-size:9px}}
|
||||
.stat-v{font-size:18px;color:#00ff88;font-weight:bold;display:block}
|
||||
@media(max-width:768px){.stat-v{font-size:14px}}
|
||||
.stat-sub{font-size:11px;color:#666;margin-top:3px;display:block}
|
||||
body { font-family: Arial; background: #1e1e1e; color: #d0d0d0; margin: 0; padding: 20px; }
|
||||
.header { margin-bottom: 30px; }
|
||||
h1 { margin: 0; color: #00ff88; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.section { background: #2d2d2d; padding: 15px; margin: 15px 0; border-radius: 5px; }
|
||||
.metric { display: inline-block; width: 23%; margin: 1%; background: #1e1e1e; padding: 12px; border-radius: 3px; border-left: 3px solid #00ff88; }
|
||||
.metric-label { font-size: 11px; color: #888; }
|
||||
.metric-value { font-size: 18px; font-weight: bold; color: #00ff88; }
|
||||
button { background: #00ff88; color: #000; border: none; padding: 8px 15px; border-radius: 3px; cursor: pointer; font-weight: bold; }
|
||||
button:hover { background: #00dd77; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #444; }
|
||||
th { background: #333; color: #00ff88; }
|
||||
.pos { color: #00ff88; }
|
||||
.neg { color: #ff4444; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<div class="header">
|
||||
<div><h1>🤖 Trading Bot v0.5.1</h1><p>P&L Analytics</p></div>
|
||||
<div class="status" id="st">● LOADING</div>
|
||||
<div><h1>🤖 Trading Bot v0.6</h1><p>Contrarian Mean Reversion Strategy</p></div>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="btn active" onclick="switchTab(event, 'portfolio')">📊 Portfolio</button>
|
||||
<button class="btn" onclick="switchTab(event, 'analytics')">📈 Analytics</button>
|
||||
<div class="section" id="portfolio">
|
||||
<h2>Portfolio</h2>
|
||||
<div id="metrics"></div>
|
||||
</div>
|
||||
|
||||
<div id="portfolio" class="tab active">
|
||||
<div class="grid">
|
||||
<div class="card"><div class="lbl">Portfolio</div><div class="val" id="pv">-</div></div>
|
||||
<div class="card"><div class="lbl">P&L</div><div class="val" id="pl">-</div><div class="sub" id="pp">-</div></div>
|
||||
<div class="card"><div class="lbl">USDT</div><div class="val" id="uf">-</div></div>
|
||||
<div class="card"><div class="lbl">Trades</div><div class="val" id="tr">-</div></div>
|
||||
<div class="section" id="analytics">
|
||||
<h2>Analytics</h2>
|
||||
<div id="pnl-chart"></div>
|
||||
</div>
|
||||
|
||||
<div class="collapse-header" onclick="toggleHoldings()">
|
||||
<h3>Holdings</h3>
|
||||
<span class="collapse-toggle" id="toggle-icon">▶</span>
|
||||
<div class="section" id="trades">
|
||||
<h2>Active Trades</h2>
|
||||
<table id="trades-table">
|
||||
<tr><th>Symbol</th><th>Qty</th><th>Entry Price</th><th>Entry Time</th></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="grid holdings" id="holdings"></div>
|
||||
</div>
|
||||
|
||||
<div id="analytics" class="tab">
|
||||
<div class="chart-box">
|
||||
<div class="title">📈 P&L Performance (Live)</div>
|
||||
<div class="times">
|
||||
<button class="time active" onclick="loadChart(24, event)">1 Day</button>
|
||||
<button class="time" onclick="loadChart(168, event)">1 Week</button>
|
||||
<button class="time" onclick="loadChart(720, event)">1 Month</button>
|
||||
</div>
|
||||
<canvas id="chart" height="100"></canvas>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-l">Current</div>
|
||||
<span class="stat-v" id="cur-pct">-</span>
|
||||
<span class="stat-sub" id="cur-usd">-</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-l">Min</div>
|
||||
<span class="stat-v" id="min-pct">-</span>
|
||||
<span class="stat-sub" id="min-usd">-</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-l">Max</div>
|
||||
<span class="stat-v" id="max-pct">-</span>
|
||||
<span class="stat-sub" id="max-usd">-</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-l">Avg</div>
|
||||
<span class="stat-v" id="avg-pct">-</span>
|
||||
<span class="stat-sub" id="avg-usd">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let chartObj = null;
|
||||
async function loadData() {
|
||||
const state = await fetch('/api/state').then(r => r.json());
|
||||
const pnl = await fetch('/api/pnl-history?hours=24').then(r => r.json());
|
||||
|
||||
function switchTab(e, tabName) {
|
||||
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
|
||||
document.querySelectorAll('.btn').forEach(el => el.classList.remove('active'));
|
||||
document.getElementById(tabName).classList.add('active');
|
||||
e.target.classList.add('active');
|
||||
document.getElementById('metrics').innerHTML = `
|
||||
<div class="metric">
|
||||
<div class="metric-label">Portfolio Value</div>
|
||||
<div class="metric-value">$${state.portfolio_value.toFixed(2)}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-label">P&L</div>
|
||||
<div class="metric-value ${pnl.current_pct >= 0 ? 'pos' : 'neg'}">${pnl.current_pct > 0 ? '+' : ''}${pnl.current_pct.toFixed(2)}%</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-label">Active Trades</div>
|
||||
<div class="metric-value">${state.active_positions}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-label">Strategy</div>
|
||||
<div class="metric-value" style="font-size: 12px;">Contrarian</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let tradesHtml = '';
|
||||
for (const [symbol, trade] of Object.entries(state.active_trades || {})) {
|
||||
tradesHtml += `
|
||||
<tr>
|
||||
<td>${symbol}</td>
|
||||
<td>${trade.qty.toFixed(8)}</td>
|
||||
<td>$${trade.entry_price.toFixed(2)}</td>
|
||||
<td>${new Date(trade.entry_time).toLocaleString()}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
document.getElementById('trades-table').innerHTML += tradesHtml;
|
||||
}
|
||||
|
||||
function toggleHoldings() {
|
||||
const h = document.getElementById('holdings');
|
||||
const i = document.getElementById('toggle-icon');
|
||||
h.classList.toggle('open');
|
||||
i.textContent = h.classList.contains('open') ? '▼' : '▶';
|
||||
}
|
||||
|
||||
async function updatePortfolio() {
|
||||
const res = await fetch('/api/state');
|
||||
const data = await res.json();
|
||||
if (data.error) return;
|
||||
|
||||
document.getElementById('pv').textContent = '$' + data.portfolio_value.toFixed(2);
|
||||
document.getElementById('pl').textContent = '$' + data.pnl_usdt.toFixed(2);
|
||||
document.getElementById('pp').textContent = data.pnl_pct.toFixed(2) + '%';
|
||||
document.getElementById('uf').textContent = '$' + data.usdt_free.toFixed(2);
|
||||
document.getElementById('tr').textContent = data.active_positions;
|
||||
document.getElementById('st').textContent = '● LIVE';
|
||||
|
||||
const hh = document.getElementById('holdings');
|
||||
hh.innerHTML = '';
|
||||
for (const [asset, info] of Object.entries(data.balance)) {
|
||||
if (asset !== 'USDT' && info.total > 1e-4) {
|
||||
const price = data.prices[asset] || 0;
|
||||
const usdValue = info.total * price;
|
||||
hh.innerHTML += '<div class="card"><div class="lbl">' + asset + '</div><div class="val">' + info.total.toFixed(4) + '</div><div class="sub">≈ $' + usdValue.toFixed(2) + '</div></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChart(hours, e) {
|
||||
if (e) {
|
||||
document.querySelectorAll('.time').forEach(b => b.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
}
|
||||
|
||||
const res = await fetch('/api/pnl-history?hours=' + hours);
|
||||
const data = await res.json();
|
||||
const ctx = document.getElementById('chart').getContext('2d');
|
||||
|
||||
if (chartObj) chartObj.destroy();
|
||||
|
||||
const col = data.current_pct >= 0 ? '#00ff88' : '#ff4444';
|
||||
const bg = data.current_pct >= 0 ? 'rgba(0,255,136,0.1)' : 'rgba(255,68,68,0.1)';
|
||||
|
||||
chartObj = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.timestamps,
|
||||
datasets: [{
|
||||
label: 'P&L %',
|
||||
data: data.pnl_pcts,
|
||||
borderColor: col,
|
||||
backgroundColor: bg,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 2,
|
||||
pointBackgroundColor: col,
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: { legend: { labels: { color: '#888' } } },
|
||||
scales: {
|
||||
y: { grid: { color: 'rgba(0,255,136,0.1)' }, ticks: { color: '#888' } },
|
||||
x: { grid: { color: 'rgba(0,255,136,0.1)' }, ticks: { color: '#888' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(2);
|
||||
document.getElementById('cur-pct').textContent = data.current_pct.toFixed(2) + '%';
|
||||
document.getElementById('cur-usd').textContent = '$' + fmt(data.current_usdt);
|
||||
document.getElementById('min-pct').textContent = data.min_pct.toFixed(2) + '%';
|
||||
document.getElementById('min-usd').textContent = '$' + fmt(data.min_usdt);
|
||||
document.getElementById('max-pct').textContent = data.max_pct.toFixed(2) + '%';
|
||||
document.getElementById('max-usd').textContent = '$' + fmt(data.max_usdt);
|
||||
document.getElementById('avg-pct').textContent = data.avg_pct.toFixed(2) + '%';
|
||||
document.getElementById('avg-usd').textContent = '$' + fmt(data.avg_usdt);
|
||||
}
|
||||
|
||||
setInterval(updatePortfolio, 10000);
|
||||
updatePortfolio();
|
||||
loadChart(24, null);
|
||||
setInterval(loadData, 5000);
|
||||
loadData();
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
return HTMLResponse(content=html)
|
||||
</html>
|
||||
"""
|
||||
return render_template_string(html)
|
||||
|
||||
@app.route('/api/state')
|
||||
def api_state():
|
||||
try:
|
||||
with open('/home/marc/bot-deploy/active_trades.json') as f:
|
||||
trades_data = json.load(f)
|
||||
|
||||
return jsonify({
|
||||
'active_trades': trades_data.get('active_trades', {}),
|
||||
'active_positions': trades_data.get('count', 0),
|
||||
'portfolio_value': trades_data.get('portfolio_value', 0),
|
||||
'pnl_pct': 0, # Fetched from DB
|
||||
'pnl_usdt': 0
|
||||
})
|
||||
except:
|
||||
return jsonify({'error': 'No data'}), 404
|
||||
|
||||
@app.route('/api/pnl-history')
|
||||
def api_pnl_history():
|
||||
try:
|
||||
conn = sqlite3.connect('/home/marc/bot-deploy/pnl_charts.db')
|
||||
rows = conn.execute('SELECT pp FROM history ORDER BY ts DESC LIMIT 1').fetchall()
|
||||
conn.close()
|
||||
|
||||
if rows:
|
||||
return jsonify({'current_pct': rows[0][0], 'current_usdt': 0, 'entries': []})
|
||||
|
||||
return jsonify({'current_pct': 0, 'current_usdt': 0, 'entries': []})
|
||||
except:
|
||||
return jsonify({'error': 'No data'}), 404
|
||||
|
||||
if __name__ == '__main__':
|
||||
import uvicorn
|
||||
uvicorn.run(app, host='0.0.0.0', port=7000)
|
||||
app.run(host='0.0.0.0', port=7000, debug=False)
|
||||
|
|
|
|||
Loading…
Reference in New Issue