Bot auto-update: src/__pycache__/web_dashboard.cpython-310.pyc,src/frigate_report.py,src/main_ml_BACKUP_before_precision_fix.py,src/main_ml_enhanced.py,src/main_ml_fixed.py,src/main_ml_v02_backup_1783615772.py,src/main_ml_v03.py,src/main_ml_v2.py,src/main_ml_v4_backup.py,src/main_ml_v6.py,src/strategies/dca.py,src/web_dashboard_v0.3_backup.py,src/web_dashboard_v03_backup.py
This commit is contained in:
parent
61bf2ac1c6
commit
9d202bf01e
Binary file not shown.
|
|
@ -0,0 +1,86 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Frigate Daily Report Generator
|
||||||
|
Sends to Telegram every evening at 20:30 CET
|
||||||
|
"""
|
||||||
|
import os, json, requests
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
FRIGATE_URL = "http://localhost:5000"
|
||||||
|
|
||||||
|
def get_frigate_events():
|
||||||
|
"""Get events from last 24 hours"""
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{FRIGATE_URL}/api/events", timeout=5)
|
||||||
|
events = resp.json()
|
||||||
|
|
||||||
|
# Filter for last 24h
|
||||||
|
now = datetime.now().timestamp()
|
||||||
|
yesterday = now - (24 * 3600)
|
||||||
|
|
||||||
|
recent = [e for e in events if e.get('start_time', 0) > yesterday]
|
||||||
|
return recent
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching events: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def generate_report():
|
||||||
|
"""Generate Frigate daily summary"""
|
||||||
|
events = get_frigate_events()
|
||||||
|
|
||||||
|
if not events:
|
||||||
|
return "🎥 **Frigate Daily Report** — Keine Events heute\n\nStatus: ✅ Alle Kameras aktiv\nEvents: 0"
|
||||||
|
|
||||||
|
# Group by camera & label
|
||||||
|
by_camera = defaultdict(lambda: defaultdict(int))
|
||||||
|
by_label = defaultdict(int)
|
||||||
|
people = set()
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
camera = event.get('camera', 'Unknown')
|
||||||
|
label = event.get('label', 'Unknown')
|
||||||
|
sub_label = event.get('sub_label', None)
|
||||||
|
|
||||||
|
by_camera[camera][label] += 1
|
||||||
|
by_label[label] += 1
|
||||||
|
|
||||||
|
if label == 'person' and sub_label:
|
||||||
|
people.add(sub_label)
|
||||||
|
|
||||||
|
# Format report
|
||||||
|
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M CET')
|
||||||
|
report = f"""🎥 **Frigate Daily Report** — {timestamp}
|
||||||
|
|
||||||
|
📊 **ZUSAMMENFASSUNG**
|
||||||
|
• Gesamt Events: {len(events)}
|
||||||
|
• Detektierte Personen: {len(people)}
|
||||||
|
• Kameras aktiv: {len(by_camera)}
|
||||||
|
|
||||||
|
👥 **Erkannte Personen**
|
||||||
|
"""
|
||||||
|
|
||||||
|
for person in sorted(people):
|
||||||
|
report += f" • {person}\n"
|
||||||
|
|
||||||
|
report += f"\n📹 **Nach Kamera**\n"
|
||||||
|
|
||||||
|
for camera in sorted(by_camera.keys()):
|
||||||
|
events_count = sum(by_camera[camera].values())
|
||||||
|
labels = ", ".join(by_camera[camera].keys())
|
||||||
|
report += f" 🟢 {camera}: {events_count} Events ({labels})\n"
|
||||||
|
|
||||||
|
report += f"\n🏷️ **Nach Objekttyp**\n"
|
||||||
|
|
||||||
|
for label in sorted(by_label.keys()):
|
||||||
|
count = by_label[label]
|
||||||
|
report += f" • {label.upper()}: {count}\n"
|
||||||
|
|
||||||
|
report += f"\n✅ **Status**: Alle Kameras aktiv\n"
|
||||||
|
report += f"*Report: {datetime.now().strftime('%H:%M:%S UTC')}*"
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
report = generate_report()
|
||||||
|
print(report)
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Trading Bot V5 ENHANCED - Mit kritischen Risk Management Fixes
|
||||||
|
Implementiert: SL, TP Anpassung, Daily Limit, R:R Ratio
|
||||||
|
"""
|
||||||
|
import os, asyncio, logging, random, json, time
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from binance.client import Client
|
||||||
|
from binance.exceptions import BinanceAPIException
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Load config
|
||||||
|
env = {}
|
||||||
|
with open('/home/marc/bot-deploy/.env') as f:
|
||||||
|
for line in f:
|
||||||
|
k, _, v = line.partition('=')
|
||||||
|
env[k.strip()] = v.strip()
|
||||||
|
|
||||||
|
class TradingBotV5Enhanced:
|
||||||
|
def __init__(self):
|
||||||
|
self.binance = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||||
|
self.state_file = '/home/marc/bot-deploy/trades.json'
|
||||||
|
self.load_state()
|
||||||
|
|
||||||
|
# NEW: Risk Management Settings
|
||||||
|
self.STOP_LOSS_PERCENT = 2.5 # 2.5% SL (-2.5%)
|
||||||
|
self.TAKE_PROFIT_PERCENT = 3.0 # 3.0% TP (+3%) - was +1%
|
||||||
|
self.DAILY_LOSS_LIMIT = 5.0 # Max -5% daily
|
||||||
|
self.MIN_RISK_REWARD = 1.5 # Min R:R ratio
|
||||||
|
self.MAX_POSITION_PERCENT = 25 # Max 25% per trade
|
||||||
|
|
||||||
|
logger.info("✅ Bot initialized with Risk Management (SL 2.5%, TP 3%, Daily Limit 5%)")
|
||||||
|
|
||||||
|
def load_state(self):
|
||||||
|
if os.path.exists(self.state_file):
|
||||||
|
with open(self.state_file) as f:
|
||||||
|
self.state = json.load(f)
|
||||||
|
else:
|
||||||
|
self.state = {'current': {}, 'completed': [], 'daily_start_balance': 0}
|
||||||
|
|
||||||
|
def save_state(self):
|
||||||
|
with open(self.state_file, 'w') as f:
|
||||||
|
json.dump(self.state, f, indent=2)
|
||||||
|
|
||||||
|
def check_and_place_sl_orders(self, pair, qty, entry_price):
|
||||||
|
"""
|
||||||
|
NEW: Automatically place Stop Loss orders for existing positions
|
||||||
|
SL = Entry - 2.5%
|
||||||
|
"""
|
||||||
|
sl_price = entry_price * (1 - self.STOP_LOSS_PERCENT / 100)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if already has SL order
|
||||||
|
orders = self.binance.get_open_orders(symbol=pair)
|
||||||
|
has_sl = any(o['side'] == 'SELL' and float(o['price']) < entry_price for o in orders)
|
||||||
|
|
||||||
|
if not has_sl:
|
||||||
|
# Place SL order
|
||||||
|
order = self.binance.order_limit_sell(
|
||||||
|
symbol=pair,
|
||||||
|
quantity=qty,
|
||||||
|
price=round(sl_price, 8)
|
||||||
|
)
|
||||||
|
logger.info(f"🛡️ Stop Loss set: {pair} {qty} @ ${sl_price:.4f}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"SL Error {pair}: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def place_buy(self, pair):
|
||||||
|
"""Place market buy with Risk Management checks"""
|
||||||
|
try:
|
||||||
|
# Get balance
|
||||||
|
balance = self.binance.get_account()
|
||||||
|
usdt_free = float([a['free'] for a in balance['balances'] if a['asset'] == 'USDT'][0])
|
||||||
|
|
||||||
|
# NEW: Daily loss check
|
||||||
|
daily_loss = self.calculate_daily_loss()
|
||||||
|
if daily_loss <= -self.DAILY_LOSS_LIMIT:
|
||||||
|
logger.warning(f"⛔ Daily loss limit hit: {daily_loss:.2f}% (limit: -{self.DAILY_LOSS_LIMIT}%)")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Calculate position size (25% of USDT)
|
||||||
|
qty_usdt = usdt_free * (self.MAX_POSITION_PERCENT / 100)
|
||||||
|
|
||||||
|
if qty_usdt < 10: # Binance minimum
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get current price
|
||||||
|
ticker = self.binance.get_symbol_info(pair)
|
||||||
|
price = float(self.binance.get_ticker(symbol=pair)['lastPrice'])
|
||||||
|
|
||||||
|
# Calculate quantity with LOT_SIZE filter
|
||||||
|
lot_filter = next(f for f in ticker['filters'] if f['filterType'] == 'LOT_SIZE')
|
||||||
|
step_size = float(lot_filter['stepSize'])
|
||||||
|
qty = float(int(qty_usdt / price / step_size) * step_size)
|
||||||
|
|
||||||
|
if qty < float(lot_filter['minQty']):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Place market buy
|
||||||
|
order = self.binance.order_market_buy(symbol=pair, quantity=qty)
|
||||||
|
logger.info(f"🟢 BUY: {pair} x{qty:.6f} @ ${price:.4f}")
|
||||||
|
|
||||||
|
# NEW: Auto-place Stop Loss
|
||||||
|
self.check_and_place_sl_orders(pair, qty, price)
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Buy Error {pair}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_take_profit(self):
|
||||||
|
"""NEW: Check and close at +3% TP with SL protection"""
|
||||||
|
try:
|
||||||
|
balance = self.binance.get_account()
|
||||||
|
|
||||||
|
for pair in ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']:
|
||||||
|
ticker = self.binance.get_ticker(symbol=pair)
|
||||||
|
current_price = float(ticker['lastPrice'])
|
||||||
|
|
||||||
|
# Check if we have open trade
|
||||||
|
if pair in self.state['current']:
|
||||||
|
entry_price = self.state['current'][pair]['buy_price']
|
||||||
|
gain_percent = (current_price - entry_price) / entry_price * 100
|
||||||
|
|
||||||
|
# TP at +3%
|
||||||
|
if gain_percent >= self.TAKE_PROFIT_PERCENT:
|
||||||
|
qty = self.state['current'][pair]['qty']
|
||||||
|
try:
|
||||||
|
order = self.binance.order_market_sell(symbol=pair, quantity=qty)
|
||||||
|
profit_usd = (current_price - entry_price) * qty
|
||||||
|
logger.info(f"💰 TP HIT: {pair} +{gain_percent:.2f}% = ${profit_usd:.2f}")
|
||||||
|
|
||||||
|
# Record completion
|
||||||
|
self.state['completed'].append({
|
||||||
|
'pair': pair,
|
||||||
|
'qty': qty,
|
||||||
|
'buy_price': entry_price,
|
||||||
|
'sell_price': current_price,
|
||||||
|
'profit_percent': gain_percent,
|
||||||
|
'profit_usd': profit_usd
|
||||||
|
})
|
||||||
|
del self.state['current'][pair]
|
||||||
|
self.save_state()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"TP sell error {pair}: {e}")
|
||||||
|
|
||||||
|
# SL at -2.5% (auto-cancelled by limit order but check anyway)
|
||||||
|
elif gain_percent <= -self.STOP_LOSS_PERCENT:
|
||||||
|
qty = self.state['current'][pair]['qty']
|
||||||
|
try:
|
||||||
|
order = self.binance.order_market_sell(symbol=pair, quantity=qty)
|
||||||
|
loss_usd = (current_price - entry_price) * qty
|
||||||
|
logger.warning(f"🛑 SL HIT: {pair} {gain_percent:.2f}% = ${loss_usd:.2f}")
|
||||||
|
|
||||||
|
self.state['completed'].append({
|
||||||
|
'pair': pair,
|
||||||
|
'qty': qty,
|
||||||
|
'buy_price': entry_price,
|
||||||
|
'sell_price': current_price,
|
||||||
|
'profit_percent': gain_percent,
|
||||||
|
'profit_usd': loss_usd
|
||||||
|
})
|
||||||
|
del self.state['current'][pair]
|
||||||
|
self.save_state()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"SL sell error {pair}: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"TP check error: {e}")
|
||||||
|
|
||||||
|
def calculate_daily_loss(self):
|
||||||
|
"""Calculate daily loss percentage"""
|
||||||
|
try:
|
||||||
|
if not self.state['completed']:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
today_trades = [t for t in self.state['completed']
|
||||||
|
if datetime.fromisoformat(t.get('timestamp', datetime.now().isoformat())).date() == datetime.now().date()]
|
||||||
|
|
||||||
|
daily_loss = sum(t.get('profit_usd', 0) for t in today_trades)
|
||||||
|
|
||||||
|
balance = self.binance.get_account()
|
||||||
|
portfolio = sum(float(a['free']) for a in balance['balances'])
|
||||||
|
|
||||||
|
loss_percent = (daily_loss / portfolio * 100) if portfolio > 0 else 0
|
||||||
|
return loss_percent
|
||||||
|
except:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
"""Main trading loop"""
|
||||||
|
logger.info("🚀 Trading Bot V5 ENHANCED started (SL+TP+DailyLimit)")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Check exits first (TP/SL)
|
||||||
|
self.check_take_profit()
|
||||||
|
|
||||||
|
# Generate signal (5% probability)
|
||||||
|
if random.random() < 0.05:
|
||||||
|
pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||||
|
for pair in pairs:
|
||||||
|
if pair not in self.state['current']:
|
||||||
|
self.place_buy(pair)
|
||||||
|
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Loop error: {e}")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
bot = TradingBotV5Enhanced()
|
||||||
|
asyncio.run(bot.run())
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Trading Bot V5 ENHANCED - Mit kritischen Risk Management Fixes
|
||||||
|
Implementiert: SL, TP Anpassung, Daily Limit, R:R Ratio
|
||||||
|
"""
|
||||||
|
import os, asyncio, logging, random, json, time
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from binance.client import Client
|
||||||
|
from binance.exceptions import BinanceAPIException
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Load config
|
||||||
|
env = {}
|
||||||
|
with open('/home/marc/bot-deploy/.env') as f:
|
||||||
|
for line in f:
|
||||||
|
k, _, v = line.partition('=')
|
||||||
|
env[k.strip()] = v.strip()
|
||||||
|
|
||||||
|
class TradingBotV5Enhanced:
|
||||||
|
def __init__(self):
|
||||||
|
self.binance = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||||
|
self.state_file = '/home/marc/bot-deploy/trades.json'
|
||||||
|
self.load_state()
|
||||||
|
|
||||||
|
# NEW: Risk Management Settings
|
||||||
|
self.STOP_LOSS_PERCENT = 2.5 # 2.5% SL (-2.5%)
|
||||||
|
self.TAKE_PROFIT_PERCENT = 3.0 # 3.0% TP (+3%) - was +1%
|
||||||
|
self.DAILY_LOSS_LIMIT = 5.0 # Max -5% daily
|
||||||
|
self.MIN_RISK_REWARD = 1.5 # Min R:R ratio
|
||||||
|
self.MAX_POSITION_PERCENT = 25 # Max 25% per trade
|
||||||
|
|
||||||
|
logger.info("✅ Bot initialized with Risk Management (SL 2.5%, TP 3%, Daily Limit 5%)")
|
||||||
|
|
||||||
|
def load_state(self):
|
||||||
|
if os.path.exists(self.state_file):
|
||||||
|
with open(self.state_file) as f:
|
||||||
|
self.state = json.load(f)
|
||||||
|
else:
|
||||||
|
self.state = {'current': {}, 'completed': [], 'daily_start_balance': 0}
|
||||||
|
|
||||||
|
def save_state(self):
|
||||||
|
with open(self.state_file, 'w') as f:
|
||||||
|
json.dump(self.state, f, indent=2)
|
||||||
|
|
||||||
|
def check_and_place_sl_orders(self, pair, qty, entry_price):
|
||||||
|
"""
|
||||||
|
NEW: Automatically place Stop Loss orders for existing positions
|
||||||
|
SL = Entry - 2.5%
|
||||||
|
"""
|
||||||
|
sl_price = entry_price * (1 - self.STOP_LOSS_PERCENT / 100)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if already has SL order
|
||||||
|
orders = self.binance.get_open_orders(symbol=pair)
|
||||||
|
has_sl = any(o['side'] == 'SELL' and float(o['price']) < entry_price for o in orders)
|
||||||
|
|
||||||
|
if not has_sl:
|
||||||
|
# Place SL order
|
||||||
|
order = self.binance.order_limit_sell(
|
||||||
|
symbol=pair,
|
||||||
|
quantity=qty,
|
||||||
|
price=round(sl_price, 8)
|
||||||
|
)
|
||||||
|
logger.info(f"🛡️ Stop Loss set: {pair} {qty} @ ${sl_price:.4f}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"SL Error {pair}: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def place_buy(self, pair):
|
||||||
|
"""Place market buy with Risk Management checks"""
|
||||||
|
try:
|
||||||
|
# Get balance
|
||||||
|
balance = self.binance.get_account()
|
||||||
|
usdt_free = float([a['free'] for a in balance['balances'] if a['asset'] == 'USDT'][0])
|
||||||
|
|
||||||
|
# NEW: Daily loss check
|
||||||
|
daily_loss = self.calculate_daily_loss()
|
||||||
|
if daily_loss <= -self.DAILY_LOSS_LIMIT:
|
||||||
|
logger.warning(f"⛔ Daily loss limit hit: {daily_loss:.2f}% (limit: -{self.DAILY_LOSS_LIMIT}%)")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Calculate position size (25% of USDT)
|
||||||
|
qty_usdt = usdt_free * (self.MAX_POSITION_PERCENT / 100)
|
||||||
|
|
||||||
|
if qty_usdt < 10: # Binance minimum
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get current price
|
||||||
|
ticker = self.binance.get_symbol_info(pair)
|
||||||
|
price = float(self.binance.get_ticker(symbol=pair)['lastPrice'])
|
||||||
|
|
||||||
|
# Calculate quantity with LOT_SIZE filter
|
||||||
|
lot_filter = next(f for f in ticker['filters'] if f['filterType'] == 'LOT_SIZE')
|
||||||
|
step_size = float(lot_filter['stepSize'])
|
||||||
|
qty = float(int(qty_usdt / price / step_size) * step_size)
|
||||||
|
|
||||||
|
if qty < float(lot_filter['minQty']):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Place market buy
|
||||||
|
order = self.binance.order_market_buy(symbol=pair, quantity=qty)
|
||||||
|
logger.info(f"🟢 BUY: {pair} x{qty:.6f} @ ${price:.4f}")
|
||||||
|
|
||||||
|
# NEW: Auto-place Stop Loss
|
||||||
|
self.check_and_place_sl_orders(pair, qty, price)
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Buy Error {pair}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_take_profit(self):
|
||||||
|
"""NEW: Check and close at +3% TP with SL protection"""
|
||||||
|
try:
|
||||||
|
balance = self.binance.get_account()
|
||||||
|
|
||||||
|
for pair in ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']:
|
||||||
|
ticker = self.binance.get_ticker(symbol=pair)
|
||||||
|
current_price = float(ticker['lastPrice'])
|
||||||
|
|
||||||
|
# Check if we have open trade
|
||||||
|
if pair in self.state['current']:
|
||||||
|
entry_price = self.state['current'][pair]['buy_price']
|
||||||
|
gain_percent = (current_price - entry_price) / entry_price * 100
|
||||||
|
|
||||||
|
# TP at +3%
|
||||||
|
if gain_percent >= self.TAKE_PROFIT_PERCENT:
|
||||||
|
qty = self.state['current'][pair]['qty']
|
||||||
|
try:
|
||||||
|
order = self.binance.order_market_sell(symbol=pair, quantity=qty)
|
||||||
|
profit_usd = (current_price - entry_price) * qty
|
||||||
|
logger.info(f"💰 TP HIT: {pair} +{gain_percent:.2f}% = ${profit_usd:.2f}")
|
||||||
|
|
||||||
|
# Record completion
|
||||||
|
self.state['completed'].append({
|
||||||
|
'pair': pair,
|
||||||
|
'qty': qty,
|
||||||
|
'buy_price': entry_price,
|
||||||
|
'sell_price': current_price,
|
||||||
|
'profit_percent': gain_percent,
|
||||||
|
'profit_usd': profit_usd
|
||||||
|
})
|
||||||
|
del self.state['current'][pair]
|
||||||
|
self.save_state()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"TP sell error {pair}: {e}")
|
||||||
|
|
||||||
|
# SL at -2.5% (auto-cancelled by limit order but check anyway)
|
||||||
|
elif gain_percent <= -self.STOP_LOSS_PERCENT:
|
||||||
|
qty = self.state['current'][pair]['qty']
|
||||||
|
try:
|
||||||
|
order = self.binance.order_market_sell(symbol=pair, quantity=qty)
|
||||||
|
loss_usd = (current_price - entry_price) * qty
|
||||||
|
logger.warning(f"🛑 SL HIT: {pair} {gain_percent:.2f}% = ${loss_usd:.2f}")
|
||||||
|
|
||||||
|
self.state['completed'].append({
|
||||||
|
'pair': pair,
|
||||||
|
'qty': qty,
|
||||||
|
'buy_price': entry_price,
|
||||||
|
'sell_price': current_price,
|
||||||
|
'profit_percent': gain_percent,
|
||||||
|
'profit_usd': loss_usd
|
||||||
|
})
|
||||||
|
del self.state['current'][pair]
|
||||||
|
self.save_state()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"SL sell error {pair}: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"TP check error: {e}")
|
||||||
|
|
||||||
|
def calculate_daily_loss(self):
|
||||||
|
"""Calculate daily loss percentage"""
|
||||||
|
try:
|
||||||
|
if not self.state['completed']:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
today_trades = [t for t in self.state['completed']
|
||||||
|
if datetime.fromisoformat(t.get('timestamp', datetime.now().isoformat())).date() == datetime.now().date()]
|
||||||
|
|
||||||
|
daily_loss = sum(t.get('profit_usd', 0) for t in today_trades)
|
||||||
|
|
||||||
|
balance = self.binance.get_account()
|
||||||
|
portfolio = sum(float(a['free']) for a in balance['balances'])
|
||||||
|
|
||||||
|
loss_percent = (daily_loss / portfolio * 100) if portfolio > 0 else 0
|
||||||
|
return loss_percent
|
||||||
|
except:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
"""Main trading loop"""
|
||||||
|
logger.info("🚀 Trading Bot V5 ENHANCED started (SL+TP+DailyLimit)")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Check exits first (TP/SL)
|
||||||
|
self.check_take_profit()
|
||||||
|
|
||||||
|
# Generate signal (5% probability)
|
||||||
|
if random.random() < 0.05:
|
||||||
|
pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||||
|
for pair in pairs:
|
||||||
|
if pair not in self.state['current']:
|
||||||
|
self.place_buy(pair)
|
||||||
|
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Loop error: {e}")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
bot = TradingBotV5Enhanced()
|
||||||
|
asyncio.run(bot.run())
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Trading Bot V5 ENHANCED - Risk Management FIXED
|
||||||
|
Implementiert: SL (mit korrekter Precision), TP, Daily Limit, R:R Ratio
|
||||||
|
FIXED: PRICE_FILTER für SL Orders durch Tick-Rounding
|
||||||
|
"""
|
||||||
|
import os, asyncio, logging, random, json, time, math
|
||||||
|
from binance.client import Client
|
||||||
|
from binance.exceptions import BinanceAPIException
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Load env
|
||||||
|
env = {}
|
||||||
|
with open('/home/marc/bot-deploy/.env') as f:
|
||||||
|
for line in f:
|
||||||
|
k,_,v = line.partition('=')
|
||||||
|
env[k.strip()] = v.strip()
|
||||||
|
|
||||||
|
class TradingBot:
|
||||||
|
def __init__(self):
|
||||||
|
self.client = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||||
|
|
||||||
|
self.PAIRS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||||
|
self.SIGNAL_THRESHOLD = 5 # 5% random signal
|
||||||
|
self.INVESTMENT_PERCENT = 25 # 25% per trade
|
||||||
|
self.STOP_LOSS_PERCENT = 2.5 # -2.5%
|
||||||
|
self.TAKE_PROFIT_PERCENT = 3.0 # +3%
|
||||||
|
self.DAILY_LOSS_LIMIT = -5 # -5% max
|
||||||
|
|
||||||
|
self.active_trades = {}
|
||||||
|
self.daily_pnl = 0
|
||||||
|
self.paused = False
|
||||||
|
|
||||||
|
# Precision cache
|
||||||
|
self.pair_precision = {}
|
||||||
|
self._load_pair_precision()
|
||||||
|
|
||||||
|
logger.info("✅ Bot initialized with Risk Management (SL 2.5%, TP 3%, Daily Limit 5%)")
|
||||||
|
|
||||||
|
def _load_pair_precision(self):
|
||||||
|
"""Load Binance precision rules for each pair"""
|
||||||
|
for pair in self.PAIRS:
|
||||||
|
try:
|
||||||
|
info = self.client.get_symbol_info(symbol=pair)
|
||||||
|
for f in info['filters']:
|
||||||
|
if f['filterType'] == 'PRICE_FILTER':
|
||||||
|
tick = float(f['tickSize'])
|
||||||
|
self.pair_precision[pair] = {
|
||||||
|
'tick': tick,
|
||||||
|
'decimals': self._get_decimals(tick)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Precision load {pair}: {e}")
|
||||||
|
|
||||||
|
def _get_decimals(self, tick):
|
||||||
|
"""Get decimal places from tick size"""
|
||||||
|
s = str(tick)
|
||||||
|
if 'e' in s:
|
||||||
|
return int(s.split('e-')[1]) if 'e-' in s else 0
|
||||||
|
return len(s.split('.')[1]) if '.' in s else 0
|
||||||
|
|
||||||
|
def _round_to_tick(self, price, pair):
|
||||||
|
"""Round price to Binance tick size"""
|
||||||
|
tick = self.pair_precision.get(pair, {}).get('tick', 0.01)
|
||||||
|
return round(price / tick) * tick
|
||||||
|
|
||||||
|
async def signal_buy(self, pair):
|
||||||
|
"""Generate random 5% buy signal"""
|
||||||
|
rand = random.randint(1, 100)
|
||||||
|
return rand <= self.SIGNAL_THRESHOLD
|
||||||
|
|
||||||
|
async def place_buy_order(self, pair):
|
||||||
|
"""Place market buy order"""
|
||||||
|
try:
|
||||||
|
# Get current price
|
||||||
|
ticker = self.client.get_ticker(symbol=pair)
|
||||||
|
entry_price = float(ticker['lastPrice'])
|
||||||
|
|
||||||
|
# Calculate quantity
|
||||||
|
account = self.client.get_account()
|
||||||
|
usdt_balance = next((b['free'] for b in account['balances'] if b['asset'] == 'USDT'), 0)
|
||||||
|
usdt = float(usdt_balance) * (self.INVESTMENT_PERCENT / 100)
|
||||||
|
|
||||||
|
qty = usdt / entry_price
|
||||||
|
|
||||||
|
# Place market buy
|
||||||
|
order = self.client.order_market_buy(symbol=pair, quantity=qty)
|
||||||
|
logger.info(f"🟢 BUY: {pair} x{qty:.6f} @ ${entry_price:.2f}")
|
||||||
|
|
||||||
|
# Store trade
|
||||||
|
self.active_trades[pair] = {
|
||||||
|
'entry': entry_price,
|
||||||
|
'qty': qty,
|
||||||
|
'time': datetime.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Place SL order (FIXED WITH ROUNDING)
|
||||||
|
await self.place_stop_loss(pair, entry_price, qty)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Buy Error {pair}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def place_stop_loss(self, pair, entry_price, qty):
|
||||||
|
"""Place stop loss order with correct precision"""
|
||||||
|
try:
|
||||||
|
# Calculate SL price with 2.5% loss
|
||||||
|
sl_price = entry_price * (1 - self.STOP_LOSS_PERCENT / 100)
|
||||||
|
|
||||||
|
# ROUND TO TICK SIZE (CRITICAL FIX!)
|
||||||
|
sl_price = self._round_to_tick(sl_price, pair)
|
||||||
|
|
||||||
|
# Place SL order
|
||||||
|
order = self.client.order_take_profit(
|
||||||
|
symbol=pair,
|
||||||
|
side='SELL',
|
||||||
|
type='STOP_LOSS',
|
||||||
|
timeInForce='GTC',
|
||||||
|
quantity=qty,
|
||||||
|
stopPrice=sl_price,
|
||||||
|
price=sl_price # Binance requires price = stopPrice for STOP_LOSS
|
||||||
|
)
|
||||||
|
logger.info(f"🛡️ SL: {pair} @ ${sl_price:.4f} (-{self.STOP_LOSS_PERCENT}%)")
|
||||||
|
|
||||||
|
except BinanceAPIException as e:
|
||||||
|
logger.error(f"SL Error {pair}: {e}")
|
||||||
|
|
||||||
|
async def monitor_positions(self):
|
||||||
|
"""Monitor open positions for TP/SL"""
|
||||||
|
try:
|
||||||
|
account = self.client.get_account()
|
||||||
|
|
||||||
|
for pair in self.active_trades.keys():
|
||||||
|
ticker = self.client.get_ticker(symbol=pair)
|
||||||
|
current = float(ticker['lastPrice'])
|
||||||
|
entry = self.active_trades[pair]['entry']
|
||||||
|
|
||||||
|
gain_percent = ((current - entry) / entry) * 100
|
||||||
|
|
||||||
|
# Check TP
|
||||||
|
if gain_percent >= self.TAKE_PROFIT_PERCENT:
|
||||||
|
await self.close_position(pair, 'TP', current)
|
||||||
|
|
||||||
|
# Check SL (secondary check)
|
||||||
|
elif gain_percent <= -self.STOP_LOSS_PERCENT:
|
||||||
|
await self.close_position(pair, 'SL', current)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Monitor Error: {e}")
|
||||||
|
|
||||||
|
async def close_position(self, pair, reason, current_price):
|
||||||
|
"""Close position"""
|
||||||
|
if pair not in self.active_trades:
|
||||||
|
return
|
||||||
|
|
||||||
|
qty = self.active_trades[pair]['qty']
|
||||||
|
entry = self.active_trades[pair]['entry']
|
||||||
|
pnl = (current_price - entry) * qty
|
||||||
|
|
||||||
|
logger.info(f"📊 {reason}: {pair} closed @ ${current_price:.2f}, PnL: ${pnl:.2f}")
|
||||||
|
|
||||||
|
del self.active_trades[pair]
|
||||||
|
self.daily_pnl += pnl
|
||||||
|
|
||||||
|
# Check daily loss limit
|
||||||
|
if self.daily_pnl <= self.DAILY_LOSS_LIMIT:
|
||||||
|
logger.warning(f"⚠️ DAILY LOSS LIMIT REACHED: ${self.daily_pnl:.2f}")
|
||||||
|
self.paused = True
|
||||||
|
|
||||||
|
async def run_cycle(self):
|
||||||
|
"""Main trading cycle"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Check daily loss limit pause
|
||||||
|
if self.paused:
|
||||||
|
logger.info("⏸️ Bot PAUSED (daily loss limit reached)")
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Signal generation
|
||||||
|
for pair in self.PAIRS:
|
||||||
|
if pair not in self.active_trades and await self.signal_buy(pair):
|
||||||
|
await self.place_buy_order(pair)
|
||||||
|
|
||||||
|
# Monitor positions
|
||||||
|
await self.monitor_positions()
|
||||||
|
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Cycle Error: {e}")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
bot = TradingBot()
|
||||||
|
await bot.run_cycle()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
|
|
@ -0,0 +1,682 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Trading Bot V0.2 — Adaptive Strategy Learning
|
||||||
|
Implementiert: SL, TP, Daily Limit, R:R Ratio
|
||||||
|
FIXED: Binance API method (order_take_profit → create_order)
|
||||||
|
FIXED: PRICE_FILTER für SL Orders durch Tick-Rounding
|
||||||
|
FIXED: Quantity rounding mit Decimal (no floating point errors)
|
||||||
|
FIXED: Quantity string formatting für Binance
|
||||||
|
NEW: Startup Message + 3h Performance Reports via Telegram
|
||||||
|
"""
|
||||||
|
import os, asyncio, logging, random, json, time, math, requests
|
||||||
|
from decimal import Decimal, ROUND_DOWN
|
||||||
|
from binance.client import Client
|
||||||
|
from binance.exceptions import BinanceAPIException
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Load env
|
||||||
|
env = {}
|
||||||
|
with open('/home/marc/bot-deploy/.env') as f:
|
||||||
|
for line in f:
|
||||||
|
k,_,v = line.partition('=')
|
||||||
|
env[k.strip()] = v.strip()
|
||||||
|
|
||||||
|
class TradingBot:
|
||||||
|
def __init__(self):
|
||||||
|
self.client = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||||
|
|
||||||
|
self.PAIRS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||||
|
self.SIGNAL_THRESHOLD = 7.5 # 7-8% range (midpoint 7.5%) # 5% random signal
|
||||||
|
self.INVESTMENT_PERCENT = 50 # 50% (single position for liquidity) (single position)
|
||||||
|
self.INVESTMENT_PERCENT_HIGH = 55 # 55% when confidence > 85% > 85%
|
||||||
|
self.CONFIDENCE_THRESHOLD = 85 # Min confidence for high investment # 35% per trade (5 parallel = 90% max, 10% buffer)
|
||||||
|
self.NOTIONAL_MIN = 5.0 # Override Binance minimum to $3
|
||||||
|
self.STOP_LOSS_PERCENT = 1.8 # -2.5%
|
||||||
|
self.TAKE_PROFIT_PERCENT = 2.8 # +3%
|
||||||
|
self.DAILY_LOSS_LIMIT = -5
|
||||||
|
|
||||||
|
# Trailing Stop
|
||||||
|
self.TRAILING_STOP_ENTRY = 1.5 # Activate trailing stop at +1.5%
|
||||||
|
self.TRAILING_STOP_DISTANCE = 0.6 # 0.6% distance
|
||||||
|
|
||||||
|
# Position & Trade Limits
|
||||||
|
self.MAX_OPEN_POSITIONS = 1 # Single position for max liquidity # Max concurrent trades
|
||||||
|
self.MAX_CONSECUTIVE_LOSSES = 3 # Stop after 3 losses
|
||||||
|
self.CONSECUTIVE_LOSS_COOLDOWN = 30 * 60 # 30 minutes in seconds
|
||||||
|
self.MAX_TRADES_PER_DAY = 15
|
||||||
|
self.MIN_WIN_PROBABILITY = 75 # Min expected win %
|
||||||
|
|
||||||
|
# Tracking
|
||||||
|
self.consecutive_losses = 0
|
||||||
|
self.last_loss_time = None
|
||||||
|
self.trades_today = 0
|
||||||
|
self.last_trade_reset = None # -5% max
|
||||||
|
|
||||||
|
# Profit tracking
|
||||||
|
self.entry_price_history = {} # symbol -> entry price
|
||||||
|
self.closed_trades = [] # list of {symbol, entry, exit, profit_pct, profit_usdt}
|
||||||
|
self.session_start_balance = None
|
||||||
|
|
||||||
|
self.active_trades = {}
|
||||||
|
self.daily_pnl = 0
|
||||||
|
self.paused = False
|
||||||
|
|
||||||
|
# ADAPTIVE TRACKING (Option 2: Win Rate based Strategy)
|
||||||
|
self.total_trades = 0
|
||||||
|
self.total_wins = 0
|
||||||
|
self.total_losses = 0
|
||||||
|
self.last_win_rate = 50.0 # Start neutral
|
||||||
|
self.strategy_version = 1
|
||||||
|
self.start_time = datetime.now()
|
||||||
|
self.trades_today = 0
|
||||||
|
self.wins_today = 0
|
||||||
|
self.losses_today = 0
|
||||||
|
|
||||||
|
# Precision cache
|
||||||
|
self.pair_precision = {}
|
||||||
|
self._load_pair_precision()
|
||||||
|
|
||||||
|
# Telegram
|
||||||
|
self.telegram_token = env.get('TELEGRAM_BOT_TOKEN')
|
||||||
|
self.telegram_chat_id = env.get('TELEGRAM_CHAT_ID')
|
||||||
|
|
||||||
|
logger.info(f"✅ Bot initialized with Risk Management (SL {self.STOP_LOSS_PERCENT}%, TP {self.TAKE_PROFIT_PERCENT}%, Daily Limit {-self.DAILY_LOSS_LIMIT}%, Max Pos: {self.MAX_OPEN_POSITIONS})")
|
||||||
|
|
||||||
|
# Send startup message
|
||||||
|
self._send_startup_message()
|
||||||
|
|
||||||
|
def _send_telegram(self, message):
|
||||||
|
"""Send message to Telegram"""
|
||||||
|
try:
|
||||||
|
if not self.telegram_token or not self.telegram_chat_id:
|
||||||
|
logger.warning("Telegram not configured")
|
||||||
|
return False
|
||||||
|
|
||||||
|
url = f"https://api.telegram.org/bot{self.telegram_token}/sendMessage"
|
||||||
|
data = {
|
||||||
|
'chat_id': self.telegram_chat_id,
|
||||||
|
'text': message,
|
||||||
|
'parse_mode': 'Markdown'
|
||||||
|
}
|
||||||
|
response = requests.post(url, data=data, timeout=5)
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Telegram Error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _send_startup_message(self):
|
||||||
|
"""Send startup message with current strategy"""
|
||||||
|
message = """🤖 **TRADING BOT V0.2 — STARTED!**
|
||||||
|
|
||||||
|
⚙️ **AKTUELLE STRATEGIE:**
|
||||||
|
|
||||||
|
**Entry:**
|
||||||
|
• Signal: 5% Random (5 sec cycle)
|
||||||
|
• Investment: 18% USDT per trade ← FIXED!
|
||||||
|
• Pairs: BTC, ETH, SOL, BNB, XRP
|
||||||
|
• Max Parallel: 5 trades (5×18% = 90% max)
|
||||||
|
|
||||||
|
**Exit:**
|
||||||
|
• Take Profit: +3.0% ✅
|
||||||
|
• Stop Loss: -2.5% ✅
|
||||||
|
• Risk/Reward: 1:1.2
|
||||||
|
|
||||||
|
**Risk Management:**
|
||||||
|
• Daily Loss Limit: -5%
|
||||||
|
• Position Size Cap: 18%
|
||||||
|
• Buffer Reserve: 10% USDT
|
||||||
|
• SL Auto-Place: Ja (korrekt gerundet)
|
||||||
|
|
||||||
|
**Status:** 🟢 LIVE
|
||||||
|
• Time: """ + datetime.now().strftime('%Y-%m-%d %H:%M UTC') + """
|
||||||
|
• Capital Ready: 100% USDT
|
||||||
|
|
||||||
|
---
|
||||||
|
Reports: Alle 3h via Telegram 📊"""
|
||||||
|
|
||||||
|
self._send_telegram(message)
|
||||||
|
logger.info("📱 Startup message sent to Telegram")
|
||||||
|
|
||||||
|
def _load_pair_precision(self):
|
||||||
|
"""Load Binance precision rules for each pair"""
|
||||||
|
for pair in self.PAIRS:
|
||||||
|
try:
|
||||||
|
info = self.client.get_symbol_info(symbol=pair)
|
||||||
|
for f in info['filters']:
|
||||||
|
if f['filterType'] == 'PRICE_FILTER':
|
||||||
|
tick = float(f['tickSize'])
|
||||||
|
self.pair_precision[pair] = {
|
||||||
|
'tick': tick,
|
||||||
|
'decimals': self._get_decimals(tick)
|
||||||
|
}
|
||||||
|
if f['filterType'] == 'LOT_SIZE':
|
||||||
|
step = float(f['stepSize'])
|
||||||
|
if pair not in self.pair_precision:
|
||||||
|
self.pair_precision[pair] = {}
|
||||||
|
self.pair_precision[pair]['step'] = step
|
||||||
|
self.pair_precision[pair]['step_decimals'] = self._get_decimals(step)
|
||||||
|
if f['filterType'] == 'NOTIONAL':
|
||||||
|
min_notional = float(f['minNotional'])
|
||||||
|
if pair not in self.pair_precision:
|
||||||
|
self.pair_precision[pair] = {}
|
||||||
|
self.pair_precision[pair]['min_notional'] = min_notional
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Precision load {pair}: {e}")
|
||||||
|
|
||||||
|
def _get_decimals(self, tick):
|
||||||
|
"""Get decimal places from tick size"""
|
||||||
|
s = str(tick)
|
||||||
|
if 'e' in s:
|
||||||
|
return int(s.split('e-')[1]) if 'e-' in s else 0
|
||||||
|
return len(s.split('.')[1]) if '.' in s else 0
|
||||||
|
|
||||||
|
def _round_to_tick(self, price, pair):
|
||||||
|
"""Round price to Binance tick size using Decimal"""
|
||||||
|
tick = self.pair_precision.get(pair, {}).get('tick', 0.01)
|
||||||
|
price_decimal = Decimal(str(price))
|
||||||
|
tick_decimal = Decimal(str(tick))
|
||||||
|
|
||||||
|
rounded = (price_decimal / tick_decimal).quantize(Decimal('1'), rounding=ROUND_DOWN) * tick_decimal
|
||||||
|
return float(rounded)
|
||||||
|
|
||||||
|
def _round_quantity(self, qty, pair):
|
||||||
|
"""Round quantity to Binance step size using Decimal - NO PRECISION LOSS"""
|
||||||
|
step = self.pair_precision.get(pair, {}).get('step', 0.00001)
|
||||||
|
step_decimals = self.pair_precision.get(pair, {}).get('step_decimals', 5)
|
||||||
|
|
||||||
|
qty_decimal = Decimal(str(qty))
|
||||||
|
step_decimal = Decimal(str(step))
|
||||||
|
|
||||||
|
# Round down (safe side)
|
||||||
|
rounded = (qty_decimal / step_decimal).quantize(Decimal('1'), rounding=ROUND_DOWN) * step_decimal
|
||||||
|
|
||||||
|
# Format as string with exactly the right decimals
|
||||||
|
format_str = f"0.{'':<{step_decimals}}"
|
||||||
|
if step_decimals == 0:
|
||||||
|
return int(rounded)
|
||||||
|
|
||||||
|
return float(rounded)
|
||||||
|
|
||||||
|
async def signal_buy(self, pair):
|
||||||
|
"""Generate random 5% buy signal"""
|
||||||
|
rand = random.randint(1, 100)
|
||||||
|
return rand <= self.SIGNAL_THRESHOLD
|
||||||
|
|
||||||
|
async def place_buy_order(self, pair):
|
||||||
|
"""Place market buy order"""
|
||||||
|
try:
|
||||||
|
# Get current price
|
||||||
|
ticker = self.client.get_ticker(symbol=pair)
|
||||||
|
entry_price = float(ticker['lastPrice'])
|
||||||
|
|
||||||
|
# Calculate quantity
|
||||||
|
account = self.client.get_account()
|
||||||
|
usdt_balance = next((b['free'] for b in account['balances'] if b['asset'] == 'USDT'), 0)
|
||||||
|
usdt = float(usdt_balance) * (self.INVESTMENT_PERCENT / 100)
|
||||||
|
|
||||||
|
qty = usdt / entry_price
|
||||||
|
|
||||||
|
# ROUND QUANTITY TO STEP SIZE (CRITICAL FIX WITH DECIMAL!)
|
||||||
|
qty = self._round_quantity(qty, pair)
|
||||||
|
|
||||||
|
# Check if qty is valid (not zero after rounding)
|
||||||
|
if qty <= 0:
|
||||||
|
logger.warning(f"Quantity too small for {pair}: {qty}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# VALIDATE NOTIONAL (order_value must be >= 3.0 MINIMUM)
|
||||||
|
order_value = qty * entry_price
|
||||||
|
NOTIONAL_MIN = 5.0 # Minimum $3
|
||||||
|
|
||||||
|
if order_value < NOTIONAL_MIN:
|
||||||
|
logger.warning(f"Order value too small {pair}: ${order_value:.2f} < ${NOTIONAL_MIN:.2f} (qty={qty}, price={entry_price})")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info(f"✅ NOTIONAL Check Passed: {pair} ${order_value:.2f} >= ${NOTIONAL_MIN:.2f}")
|
||||||
|
|
||||||
|
# Place market buy
|
||||||
|
order = self.client.order_market_buy(symbol=pair, quantity=qty)
|
||||||
|
logger.info(f"🟢 BUY: {pair} x{qty} @ ${entry_price:.2f} (value: ${order_value:.2f})")
|
||||||
|
|
||||||
|
# Store trade
|
||||||
|
self.active_trades[pair] = {
|
||||||
|
'entry': entry_price,
|
||||||
|
'qty': qty,
|
||||||
|
'time': datetime.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Place SL order (FIXED WITH CORRECT API METHOD)
|
||||||
|
await self.place_stop_loss(pair, entry_price, qty)
|
||||||
|
|
||||||
|
self.trades_today += 1
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Buy Error {pair}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def place_stop_loss(self, pair, entry_price, qty):
|
||||||
|
"""Place stop loss order with correct precision & API method"""
|
||||||
|
try:
|
||||||
|
# Calculate SL price with {self.STOP_LOSS_PERCENT}% loss
|
||||||
|
sl_price = entry_price * (1 - self.STOP_LOSS_PERCENT / 100)
|
||||||
|
|
||||||
|
# ROUND TO TICK SIZE (CRITICAL FIX!)
|
||||||
|
sl_price = self._round_to_tick(sl_price, pair)
|
||||||
|
|
||||||
|
# ROUND QUANTITY TO STEP SIZE (WITH DECIMAL!)
|
||||||
|
qty_rounded = self._round_quantity(qty, pair)
|
||||||
|
|
||||||
|
# Place SL order using create_order (correct Binance API method)
|
||||||
|
order = self.client.create_order(
|
||||||
|
symbol=pair,
|
||||||
|
side='SELL',
|
||||||
|
type='STOP_LOSS_LIMIT',
|
||||||
|
timeInForce='GTC',
|
||||||
|
quantity=qty_rounded,
|
||||||
|
stopPrice=sl_price,
|
||||||
|
price=sl_price # For STOP_LOSS_LIMIT, need price = stopPrice
|
||||||
|
)
|
||||||
|
logger.info(f"🛡️ SL: {pair} x{qty_rounded} @ ${sl_price:.4f} (-{self.STOP_LOSS_PERCENT}%)")
|
||||||
|
|
||||||
|
except BinanceAPIException as e:
|
||||||
|
logger.error(f"SL Error {pair}: {e}")
|
||||||
|
|
||||||
|
async def monitor_positions(self):
|
||||||
|
"""Monitor open positions for TP/SL"""
|
||||||
|
try:
|
||||||
|
account = self.client.get_account()
|
||||||
|
|
||||||
|
for pair in list(self.active_trades.keys()):
|
||||||
|
ticker = self.client.get_ticker(symbol=pair)
|
||||||
|
current = float(ticker['lastPrice'])
|
||||||
|
entry = self.active_trades[pair]['entry']
|
||||||
|
|
||||||
|
gain_percent = ((current - entry) / entry) * 100
|
||||||
|
|
||||||
|
# Check TP
|
||||||
|
if gain_percent >= self.TAKE_PROFIT_PERCENT:
|
||||||
|
await self.close_position(pair, 'TP', current)
|
||||||
|
|
||||||
|
# Check SL (secondary check)
|
||||||
|
elif gain_percent <= -self.STOP_LOSS_PERCENT:
|
||||||
|
await self.close_position(pair, 'SL', current)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Monitor Error: {e}")
|
||||||
|
|
||||||
|
async def close_position(self, pair, reason, current_price):
|
||||||
|
"""Close position"""
|
||||||
|
if pair not in self.active_trades:
|
||||||
|
return
|
||||||
|
|
||||||
|
qty = self.active_trades[pair]['qty']
|
||||||
|
entry = self.active_trades[pair]['entry']
|
||||||
|
pnl = (current_price - entry) * qty
|
||||||
|
|
||||||
|
logger.info(f"📊 {reason}: {pair} closed @ ${current_price:.2f}, PnL: ${pnl:.2f}")
|
||||||
|
|
||||||
|
del self.active_trades[pair]
|
||||||
|
self.daily_pnl += pnl
|
||||||
|
|
||||||
|
if pnl > 0:
|
||||||
|
self.wins_today += 1
|
||||||
|
else:
|
||||||
|
self.losses_today += 1
|
||||||
|
|
||||||
|
# Check daily loss limit
|
||||||
|
if self.daily_pnl <= self.DAILY_LOSS_LIMIT:
|
||||||
|
logger.warning(f"⚠️ DAILY LOSS LIMIT REACHED: ${self.daily_pnl:.2f}")
|
||||||
|
self.paused = True
|
||||||
|
|
||||||
|
def get_performance_report(self):
|
||||||
|
"""Get current performance metrics"""
|
||||||
|
try:
|
||||||
|
account = self.client.get_account()
|
||||||
|
balance = {}
|
||||||
|
|
||||||
|
for asset_data in account['balances']:
|
||||||
|
asset = asset_data['asset']
|
||||||
|
free = float(asset_data['free'])
|
||||||
|
locked = float(asset_data['locked'])
|
||||||
|
total = free + locked
|
||||||
|
|
||||||
|
if total > 0.00001:
|
||||||
|
balance[asset] = {
|
||||||
|
'free': free,
|
||||||
|
'locked': locked,
|
||||||
|
'total': total
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get prices
|
||||||
|
prices = {}
|
||||||
|
for pair in self.PAIRS:
|
||||||
|
try:
|
||||||
|
ticker = self.client.get_ticker(symbol=pair)
|
||||||
|
asset = pair.replace('USDT', '')
|
||||||
|
prices[asset] = float(ticker['lastPrice'])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
prices['USDT'] = 1.0
|
||||||
|
|
||||||
|
# Calculate portfolio
|
||||||
|
portfolio = 0
|
||||||
|
tracked = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT']
|
||||||
|
for asset in tracked:
|
||||||
|
if asset in balance:
|
||||||
|
portfolio += balance[asset]['total'] * prices.get(asset, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'portfolio': round(portfolio, 2),
|
||||||
|
'usdt_free': balance.get('USDT', {}).get('free', 0),
|
||||||
|
'daily_pnl': self.daily_pnl,
|
||||||
|
'trades_today': self.trades_today,
|
||||||
|
'wins': self.wins_today,
|
||||||
|
'losses': self.losses_today,
|
||||||
|
'active_trades': len(self.active_trades),
|
||||||
|
'paused': self.paused
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Performance Report Error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def swap_coins_to_usdt(self):
|
||||||
|
"""
|
||||||
|
AUTO-SWAP: Konvertiere alle freien (unlocked) Coins → USDT
|
||||||
|
Ignoriert locked Coins (von aktiven Trades)
|
||||||
|
Skip-list: LDBTTC (shitcoin), LDDOGE (shitcoin), USDC (dust)
|
||||||
|
"""
|
||||||
|
skip_coins = ['USDT', 'LDBTTC', 'LDDOGE', 'USDC'] # Never swap these
|
||||||
|
|
||||||
|
try:
|
||||||
|
balance = self.client.get_account()
|
||||||
|
|
||||||
|
swapped_total_usdt = 0
|
||||||
|
swap_log = []
|
||||||
|
|
||||||
|
for asset in balance['balances']:
|
||||||
|
coin = asset['asset']
|
||||||
|
free_qty = float(asset['free'])
|
||||||
|
|
||||||
|
# Skip: small amounts, USDT, locked coins, skip-list
|
||||||
|
if free_qty < 0.00001 or coin in skip_coins:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
symbol = f"{coin}USDT"
|
||||||
|
|
||||||
|
# Get current price to estimate value
|
||||||
|
ticker = self.client.get_symbol_info(symbol)
|
||||||
|
if not ticker:
|
||||||
|
logger.warning(f"No ticker for {symbol}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Round quantity to step size
|
||||||
|
qty_to_sell = self._round_quantity(free_qty, symbol)
|
||||||
|
if qty_to_sell < 0.00001:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# MARKET SELL (immediate)
|
||||||
|
order = self.client.order_market_sell(symbol=symbol, quantity=qty_to_sell)
|
||||||
|
|
||||||
|
# Calculate USDT received
|
||||||
|
fills = order.get('fills', [])
|
||||||
|
usdt_received = sum(float(f['qty']) * float(f['price']) for f in fills)
|
||||||
|
|
||||||
|
swapped_total_usdt += usdt_received
|
||||||
|
swap_log.append(f"✅ {coin}: {qty_to_sell:.6f} → ${usdt_received:.2f}")
|
||||||
|
logger.info(f"Sweep: Sold {qty_to_sell} {coin} for ${usdt_received:.2f}")
|
||||||
|
|
||||||
|
except BinanceAPIException as e:
|
||||||
|
logger.warning(f"Sweep {coin}: Binance Error {e.status_code} - {e.message}")
|
||||||
|
swap_log.append(f"❌ {coin}: {e.message}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Sweep {coin}: {e}")
|
||||||
|
swap_log.append(f"❌ {coin}: {str(e)}")
|
||||||
|
|
||||||
|
# RESULT
|
||||||
|
result = {
|
||||||
|
'success': True,
|
||||||
|
'total_usdt_acquired': swapped_total_usdt,
|
||||||
|
'swaps_attempted': len(swap_log),
|
||||||
|
'log': swap_log
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send Telegram notification
|
||||||
|
msg = f"""🔄 **COINS TO USDT SWAP COMPLETE**
|
||||||
|
|
||||||
|
**Total Converted:** ${swapped_total_usdt:.2f} → USDT
|
||||||
|
|
||||||
|
{chr(10).join(swap_log)}
|
||||||
|
|
||||||
|
**New USDT Balance:** ${self.get_usdt_balance():.2f}
|
||||||
|
"""
|
||||||
|
self._send_telegram(msg)
|
||||||
|
|
||||||
|
logger.info(f"Swap complete: ${swapped_total_usdt:.2f} converted")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Swap error: {e}")
|
||||||
|
self._send_telegram(f"❌ **SWAP FAILED**: {e}")
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def get_usdt_balance(self):
|
||||||
|
"""Get current USDT balance"""
|
||||||
|
try:
|
||||||
|
balance = self.client.get_account()
|
||||||
|
for asset in balance['balances']:
|
||||||
|
if asset['asset'] == 'USDT':
|
||||||
|
return float(asset['free'])
|
||||||
|
return 0.0
|
||||||
|
except:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def send_performance_report(self):
|
||||||
|
"""Send 3h performance report via Telegram"""
|
||||||
|
report = self.get_performance_report()
|
||||||
|
if not report:
|
||||||
|
return
|
||||||
|
|
||||||
|
win_rate = 0
|
||||||
|
if report['trades_today'] > 0:
|
||||||
|
win_rate = (report['wins'] / report['trades_today']) * 100
|
||||||
|
|
||||||
|
status = "🟢 RUNNING" if not report['paused'] else "⏸️ PAUSED"
|
||||||
|
|
||||||
|
message = f"""📊 **3H PERFORMANCE REPORT**
|
||||||
|
|
||||||
|
**Portfolio Status:**
|
||||||
|
• Total: ${report['portfolio']:.2f}
|
||||||
|
• USDT Free: ${report['usdt_free']:.2f}
|
||||||
|
• Status: {status}
|
||||||
|
|
||||||
|
**Today's Trading:**
|
||||||
|
• Trades Executed: {report['trades_today']}
|
||||||
|
• Wins: {report['wins']} ✅
|
||||||
|
• Losses: {report['losses']} ❌
|
||||||
|
• Win Rate: {win_rate:.1f}%
|
||||||
|
|
||||||
|
**P&L:**
|
||||||
|
• Daily P&L: ${report['daily_pnl']:.2f}
|
||||||
|
• Open Positions: {report['active_trades']}
|
||||||
|
|
||||||
|
**Risk Status:**
|
||||||
|
• Daily Loss Limit: -5%
|
||||||
|
• Current Daily Loss: ${report['daily_pnl']:.2f}
|
||||||
|
• Pause Active: {'Yes ⏸️' if report['paused'] else 'No ✅'}
|
||||||
|
|
||||||
|
---
|
||||||
|
Time: {datetime.now().strftime('%Y-%m-%d %H:%M UTC')}
|
||||||
|
Bot: V0.2 Adaptive"""
|
||||||
|
|
||||||
|
self._send_telegram(message)
|
||||||
|
logger.info("📱 Performance report sent to Telegram")
|
||||||
|
|
||||||
|
async def run_cycle(self):
|
||||||
|
"""Main trading cycle"""
|
||||||
|
last_report_hour = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Check if it's time for 3h report
|
||||||
|
current_hour = datetime.now().hour
|
||||||
|
if current_hour % 3 == 0 and last_report_hour != current_hour:
|
||||||
|
self.send_performance_report()
|
||||||
|
last_report_hour = current_hour
|
||||||
|
|
||||||
|
# Check daily loss limit pause
|
||||||
|
if self.paused:
|
||||||
|
logger.info("⏸️ Bot PAUSED (daily loss limit reached)")
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Signal generation
|
||||||
|
for pair in self.PAIRS:
|
||||||
|
if pair not in self.active_trades and await self.signal_buy(pair):
|
||||||
|
await self.place_buy_order(pair)
|
||||||
|
|
||||||
|
# Monitor positions
|
||||||
|
await self.monitor_positions()
|
||||||
|
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Cycle Error: {e}")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
bot = TradingBot()
|
||||||
|
await bot.run_cycle()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
|
def get_signal_confidence(self):
|
||||||
|
"""Calculate confidence level for current signal (0-100%)"""
|
||||||
|
# This can be enhanced with actual ML model
|
||||||
|
# For now: random 30-95%
|
||||||
|
import random
|
||||||
|
return random.uniform(30, 95)
|
||||||
|
|
||||||
|
def get_investment_percent(self, confidence):
|
||||||
|
"""Select investment % based on confidence"""
|
||||||
|
return self.INVESTMENT_PERCENT_HIGH if confidence > self.CONFIDENCE_THRESHOLD else self.INVESTMENT_PERCENT
|
||||||
|
|
||||||
|
def check_consecutive_loss_cooldown(self):
|
||||||
|
"""Check if bot is in cooldown after 3 consecutive losses"""
|
||||||
|
if self.consecutive_losses >= self.MAX_CONSECUTIVE_LOSSES:
|
||||||
|
if self.last_loss_time is None:
|
||||||
|
return False # First loss, no cooldown
|
||||||
|
|
||||||
|
time_elapsed = time.time() - self.last_loss_time
|
||||||
|
if time_elapsed < self.CONSECUTIVE_LOSS_COOLDOWN:
|
||||||
|
logger.warning(f"🚫 Cooldown active: {int(self.CONSECUTIVE_LOSS_COOLDOWN - time_elapsed)}s remaining")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# Cooldown expired, reset counter
|
||||||
|
self.consecutive_losses = 0
|
||||||
|
logger.info("✅ Cooldown expired, consecutive loss counter reset")
|
||||||
|
return True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_volatility(self, pair):
|
||||||
|
"""Check market volatility (simplified)"""
|
||||||
|
try:
|
||||||
|
ticker = self.client.get_symbol_ticker(symbol=pair)
|
||||||
|
current_price = float(ticker['price'])
|
||||||
|
|
||||||
|
# Get 1h candle for volatility estimate
|
||||||
|
candles = self.client.get_klines(symbol=pair, interval='1h', limit=5)
|
||||||
|
|
||||||
|
high_prices = [float(c[2]) for c in candles]
|
||||||
|
low_prices = [float(c[3]) for c in candles]
|
||||||
|
|
||||||
|
volatility = (max(high_prices) - min(low_prices)) / min(low_prices) * 100
|
||||||
|
|
||||||
|
# Flag as extreme if > 5% 1h volatility
|
||||||
|
if volatility > 5:
|
||||||
|
logger.warning(f"⚠️ High volatility {pair}: {volatility:.2f}% (skipping trade)")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return True # If check fails, allow trade
|
||||||
|
|
||||||
|
def check_daily_trade_limit(self):
|
||||||
|
"""Check if daily trade limit reached"""
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
|
if self.last_trade_reset is None or self.last_trade_reset < today_start:
|
||||||
|
self.trades_today = 0
|
||||||
|
self.last_trade_reset = now
|
||||||
|
|
||||||
|
if self.trades_today >= self.MAX_TRADES_PER_DAY:
|
||||||
|
logger.warning(f"⚠️ Daily limit reached: {self.trades_today}/{self.MAX_TRADES_PER_DAY} trades")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update_trailing_stop(self, pair, current_price, entry_price):
|
||||||
|
"""Update trailing stop for an open position"""
|
||||||
|
if pair not in self.active_trades:
|
||||||
|
return False
|
||||||
|
|
||||||
|
trade_data = self.active_trades[pair]
|
||||||
|
profit_pct = ((current_price - entry_price) / entry_price) * 100
|
||||||
|
|
||||||
|
# Activate trailing stop when profit >= 1.5%
|
||||||
|
if profit_pct >= self.TRAILING_STOP_ENTRY:
|
||||||
|
trailing_stop_price = current_price * (1 - self.TRAILING_STOP_DISTANCE / 100)
|
||||||
|
trade_data['trailing_stop'] = trailing_stop_price
|
||||||
|
|
||||||
|
# If price falls below trailing stop, close position
|
||||||
|
if current_price < trailing_stop_price:
|
||||||
|
logger.info(f"🛑 Trailing stop triggered {pair}: Sell @ ${current_price:.2f}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def record_entry(self, pair, price, quantity):
|
||||||
|
"""Record entry price for profit calculation"""
|
||||||
|
self.entry_price_history[pair] = {
|
||||||
|
'price': price,
|
||||||
|
'qty': quantity,
|
||||||
|
'value': price * quantity,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
def calculate_unrealized_pnl(self):
|
||||||
|
"""Calculate unrealized P&L for open positions"""
|
||||||
|
try:
|
||||||
|
prices = get_live_prices()
|
||||||
|
total_unrealized = 0
|
||||||
|
|
||||||
|
for pair, entry_data in self.entry_price_history.items():
|
||||||
|
asset = pair.replace('USDT', '')
|
||||||
|
current_price = prices.get(asset, 0)
|
||||||
|
if current_price > 0:
|
||||||
|
current_value = entry_data['qty'] * current_price
|
||||||
|
unrealized = current_value - entry_data['value']
|
||||||
|
total_unrealized += unrealized
|
||||||
|
|
||||||
|
return total_unrealized
|
||||||
|
except:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def calculate_realized_pnl(self):
|
||||||
|
"""Sum all closed trades realized P&L"""
|
||||||
|
return sum(t.get('profit_usdt', 0) for t in self.closed_trades)
|
||||||
|
|
||||||
|
def get_total_pnl(self):
|
||||||
|
"""Total P&L = realized + unrealized"""
|
||||||
|
return self.calculate_realized_pnl() + self.calculate_unrealized_pnl()
|
||||||
|
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Trading Bot V0.3 - Strategy Rewrite
|
||||||
|
Deployed: 2026-07-09 18:30 UTC
|
||||||
|
Changes: Fresh balance cache, local min signal, hard TP/SL
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from binance.client import Client
|
||||||
|
from binance.exceptions import BinanceAPIException
|
||||||
|
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s')
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
try:
|
||||||
|
API_KEY = os.getenv('BINANCE_API_KEY_LIVE')
|
||||||
|
API_SECRET = os.getenv('BINANCE_API_SECRET_LIVE')
|
||||||
|
except:
|
||||||
|
logger.error("Missing API keys")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XRPUSDT', 'SOLUSDT']
|
||||||
|
MIN_USDT = 5.00
|
||||||
|
MAX_TRADE_USDT = 20.00
|
||||||
|
TAKE_PROFIT_PCT = 0.015 # +1.5%
|
||||||
|
STOP_LOSS_PCT = -0.008 # -0.8%
|
||||||
|
CYCLE_SEC = 60
|
||||||
|
|
||||||
|
|
||||||
|
class TradingBotV03:
|
||||||
|
"""Trading Bot with Fresh Cache + Local Min Signals + Hard Risk Management"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.client = Client(API_KEY, API_SECRET)
|
||||||
|
self.price_history = {sym: [] for sym in SYMBOLS}
|
||||||
|
self.active_trades = {} # {symbol: {'entry_price': float, 'qty': float}}
|
||||||
|
logger.info("Bot V0.3 initialized | Fresh Cache + Local Min + Hard TP/SL")
|
||||||
|
|
||||||
|
def get_fresh_balance(self):
|
||||||
|
"""KEY FIX: Always fetch FRESH balance from API (no stale cache!)"""
|
||||||
|
try:
|
||||||
|
account = self.client.get_account()
|
||||||
|
balances = {}
|
||||||
|
for b in account['balances']:
|
||||||
|
balances[b['asset']] = float(b['free'])
|
||||||
|
usdt_available = balances.get('USDT', 0)
|
||||||
|
logger.info(f"Fresh balance: USDT=${usdt_available:.2f}")
|
||||||
|
return balances, usdt_available
|
||||||
|
except BinanceAPIException as e:
|
||||||
|
logger.error(f"Balance fetch failed: {e}")
|
||||||
|
return {}, 0
|
||||||
|
|
||||||
|
def get_current_price(self, symbol):
|
||||||
|
"""Get current market price"""
|
||||||
|
try:
|
||||||
|
trades = self.client.get_recent_trades(symbol=symbol, limit=1)
|
||||||
|
if trades:
|
||||||
|
return float(trades[0]['price'])
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def calculate_valid_quantity(self, symbol, usdt_amount):
|
||||||
|
"""Calculate valid order quantity respecting LOT_SIZE"""
|
||||||
|
try:
|
||||||
|
price = self.get_current_price(symbol)
|
||||||
|
if not price:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
info = self.client.get_symbol_info(symbol)
|
||||||
|
if not info:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
step_size = 0.00001 # default
|
||||||
|
for filt in info.get('filters', []):
|
||||||
|
if filt['filterType'] == 'LOT_SIZE':
|
||||||
|
step_size = float(filt['stepSize'])
|
||||||
|
break
|
||||||
|
|
||||||
|
qty = (usdt_amount / price)
|
||||||
|
qty = int(qty / step_size) * step_size # Round to step_size
|
||||||
|
notional = qty * price
|
||||||
|
|
||||||
|
if notional < MIN_USDT:
|
||||||
|
logger.debug(f"Order too small: {symbol} ${notional:.2f}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return qty
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Qty calc failed: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def is_local_minimum(self, symbol):
|
||||||
|
"""Signal Logic: Buy when price is at local minimum (not random %)"""
|
||||||
|
if len(self.price_history[symbol]) < 5:
|
||||||
|
return False
|
||||||
|
|
||||||
|
recent_prices = self.price_history[symbol][-5:]
|
||||||
|
current_price = recent_prices[-1]
|
||||||
|
|
||||||
|
# Local min condition: current is lower than all recent prices
|
||||||
|
is_min = all(current_price < p for p in recent_prices[:-1])
|
||||||
|
|
||||||
|
if is_min:
|
||||||
|
logger.info(f"Local min detected: {symbol} @ ${current_price:.2f}")
|
||||||
|
|
||||||
|
return is_min
|
||||||
|
|
||||||
|
def place_buy_order(self, symbol, usdt_amount):
|
||||||
|
"""Place market buy order with entry price tracking"""
|
||||||
|
try:
|
||||||
|
qty = self.calculate_valid_quantity(symbol, usdt_amount)
|
||||||
|
if qty == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
entry_price = self.get_current_price(symbol)
|
||||||
|
if not entry_price:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Place market buy
|
||||||
|
order = self.client.order_market_buy(symbol=symbol, quantity=qty)
|
||||||
|
|
||||||
|
# Track entry
|
||||||
|
self.active_trades[symbol] = {
|
||||||
|
'entry_price': entry_price,
|
||||||
|
'qty': qty,
|
||||||
|
'order_id': order.get('orderId'),
|
||||||
|
'entry_time': datetime.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"BUY: {qty} {symbol} @ ${entry_price:.2f} (${qty*entry_price:.2f})")
|
||||||
|
logger.info(f" TP target: +${qty*entry_price*TAKE_PROFIT_PCT:.2f} ({TAKE_PROFIT_PCT*100:.1f}%)")
|
||||||
|
logger.info(f" SL target: -${qty*entry_price*abs(STOP_LOSS_PCT):.2f} ({STOP_LOSS_PCT*100:.1f}%)")
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
except BinanceAPIException as e:
|
||||||
|
logger.error(f"Buy order failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_and_close_positions(self):
|
||||||
|
"""HARD RISK MANAGEMENT: Close positions that hit TP or SL"""
|
||||||
|
for symbol in list(self.active_trades.keys()):
|
||||||
|
trade = self.active_trades[symbol]
|
||||||
|
current_price = self.get_current_price(symbol)
|
||||||
|
|
||||||
|
if not current_price:
|
||||||
|
continue
|
||||||
|
|
||||||
|
entry_price = trade['entry_price']
|
||||||
|
qty = trade['qty']
|
||||||
|
pnl_pct = (current_price - entry_price) / entry_price
|
||||||
|
pnl_usdt = qty * (current_price - entry_price)
|
||||||
|
|
||||||
|
# Check Take Profit (close winners immediately!)
|
||||||
|
if pnl_pct >= TAKE_PROFIT_PCT:
|
||||||
|
logger.info(f"TAKE PROFIT: {symbol} +{pnl_pct*100:.2f}% (${pnl_usdt:.2f})")
|
||||||
|
try:
|
||||||
|
self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||||
|
del self.active_trades[symbol]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Sell failed: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check Stop Loss (cut losers fast!)
|
||||||
|
if pnl_pct <= STOP_LOSS_PCT:
|
||||||
|
logger.warning(f"STOP LOSS: {symbol} {pnl_pct*100:.2f}% (${pnl_usdt:.2f})")
|
||||||
|
try:
|
||||||
|
self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||||
|
del self.active_trades[symbol]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Sell failed: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
def cycle(self):
|
||||||
|
"""Main trading cycle (runs every 60 seconds)"""
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info(f"CYCLE START @ {datetime.now().strftime('%H:%M:%S CET')}")
|
||||||
|
|
||||||
|
# STEP 1: Fresh balance (KEY FIX for cache bug!)
|
||||||
|
balances, usdt_free = self.get_fresh_balance()
|
||||||
|
|
||||||
|
if usdt_free < MIN_USDT:
|
||||||
|
logger.warning(f"Insufficient capital: ${usdt_free:.2f} < ${MIN_USDT}")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
return
|
||||||
|
|
||||||
|
# STEP 2: Check existing positions (TP/SL logic)
|
||||||
|
self.check_and_close_positions()
|
||||||
|
|
||||||
|
# STEP 3: Update price history for all symbols
|
||||||
|
for symbol in SYMBOLS:
|
||||||
|
price = self.get_current_price(symbol)
|
||||||
|
if price:
|
||||||
|
self.price_history[symbol].append(price)
|
||||||
|
# Keep only last 20 prices
|
||||||
|
if len(self.price_history[symbol]) > 20:
|
||||||
|
self.price_history[symbol].pop(0)
|
||||||
|
|
||||||
|
# STEP 4: Look for local minimum signal
|
||||||
|
best_signal = None
|
||||||
|
for symbol in SYMBOLS:
|
||||||
|
if symbol not in self.active_trades and self.is_local_minimum(symbol):
|
||||||
|
best_signal = symbol
|
||||||
|
break
|
||||||
|
|
||||||
|
# STEP 5: Place trade if signal exists and we have capital
|
||||||
|
if best_signal and usdt_free >= MIN_USDT:
|
||||||
|
# Use max 50% of available capital, but capped at MAX_TRADE_USDT
|
||||||
|
trade_amount = min(MAX_TRADE_USDT, usdt_free * 0.5)
|
||||||
|
self.place_buy_order(best_signal, trade_amount)
|
||||||
|
|
||||||
|
logger.info(f"CYCLE END | Active trades: {len(self.active_trades)} | Free USDT: ${usdt_free:.2f}")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Infinite trading loop"""
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info("TRADING BOT V0.3 STARTED")
|
||||||
|
logger.info(f"Symbols: {SYMBOLS}")
|
||||||
|
logger.info(f"Strategy: Local Min Signals | Risk: TP=+{TAKE_PROFIT_PCT*100:.1f}% / SL={STOP_LOSS_PCT*100:.1f}%")
|
||||||
|
logger.info(f"Position size: Max ${MAX_TRADE_USDT}/trade (${usdt_free*0.5} = 50% avail)")
|
||||||
|
logger.info(f"KEY FIX: Fresh balance fetched EVERY cycle (no stale cache!)")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
self.cycle()
|
||||||
|
time.sleep(CYCLE_SEC)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Bot stopped by user")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"CRITICAL ERROR: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
bot = TradingBotV03()
|
||||||
|
bot.run()
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
import asyncio, logging, joblib, time
|
||||||
|
from datetime import datetime
|
||||||
|
from src.config import get_config
|
||||||
|
from src.bot.binance_client import BinanceClientWrapper
|
||||||
|
from src.integrations.telegram_notifier import TelegramNotifier
|
||||||
|
from src.integrations.obsidian_logger import ObsidianLogger
|
||||||
|
from src.strategies.ml_strategy import MLStrategy
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class MLTradingBot:
|
||||||
|
def __init__(self, config, binance, telegram, obsidian, model, scaler):
|
||||||
|
self.config = config
|
||||||
|
self.binance = binance
|
||||||
|
self.telegram = telegram
|
||||||
|
self.obsidian = obsidian
|
||||||
|
self.model = model
|
||||||
|
self.scaler = scaler
|
||||||
|
self.strategy = MLStrategy(trading_pair=config.trading_pair)
|
||||||
|
|
||||||
|
self.last_report_time = time.time()
|
||||||
|
self.report_interval = 10800
|
||||||
|
self.trades_today = 0
|
||||||
|
self.wins_today = 0
|
||||||
|
self.losses_today = 0
|
||||||
|
self.daily_pnl = 0.0
|
||||||
|
self.report_count = 0
|
||||||
|
|
||||||
|
async def auto_swap_to_usdt(self):
|
||||||
|
"""Auto-swap holdings to USDT if needed"""
|
||||||
|
try:
|
||||||
|
balance = await self.binance.get_balance()
|
||||||
|
usdt_free = float(balance.get('USDT', {}).get('free', 0)) if balance else 0
|
||||||
|
|
||||||
|
# If low on USDT, sell any BTC/ETH/SOL holdings
|
||||||
|
for crypto in ['BTC', 'ETH', 'SOL']:
|
||||||
|
crypto_balance = float(balance.get(crypto, {}).get('free', 0)) if balance else 0
|
||||||
|
if usdt_free < 20 and crypto_balance > 0.0001:
|
||||||
|
pair = crypto + 'USDT'
|
||||||
|
logger.info(f'SWAP: Selling {crypto_balance:.6f} {crypto} for USDT')
|
||||||
|
try:
|
||||||
|
await self.binance.place_order(pair, 'SELL', 'MARKET', crypto_balance * 0.95)
|
||||||
|
await self.telegram.send_alert(f'SWAP: Sold {crypto_balance:.6f} {crypto}')
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Swap failed: {e}')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Auto-swap error: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def find_best_trade(self):
|
||||||
|
"""Scan multiple pairs for best signal"""
|
||||||
|
pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||||
|
|
||||||
|
for pair in pairs:
|
||||||
|
try:
|
||||||
|
price = await self.binance.get_ticker_price(pair)
|
||||||
|
signal = self.strategy.predict(price) if hasattr(self.strategy, 'predict') else 'HOLD'
|
||||||
|
|
||||||
|
if signal == 'BUY':
|
||||||
|
logger.info(f'BUY signal: {pair} at {price:.2f}')
|
||||||
|
return {'pair': pair, 'price': price, 'signal': signal}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f'{pair}: {e}')
|
||||||
|
|
||||||
|
return {'pair': None, 'signal': 'HOLD'}
|
||||||
|
|
||||||
|
async def monitor_trades(self):
|
||||||
|
"""Monitor & execute trades"""
|
||||||
|
try:
|
||||||
|
balance = await self.binance.get_balance()
|
||||||
|
usdt = float(balance.get('USDT', {}).get('free', 0)) if balance else 0
|
||||||
|
|
||||||
|
# Auto-swap if needed
|
||||||
|
if usdt < 15:
|
||||||
|
await self.auto_swap_to_usdt()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find best trade
|
||||||
|
trade = await self.find_best_trade()
|
||||||
|
|
||||||
|
if trade['signal'] == 'BUY' and usdt > 15:
|
||||||
|
pair = trade['pair']
|
||||||
|
price = trade['price']
|
||||||
|
qty = (usdt * 0.7) / price
|
||||||
|
|
||||||
|
logger.info(f'EXECUTE BUY: {qty:.6f} {pair} @ {price:.2f}')
|
||||||
|
try:
|
||||||
|
await self.binance.place_order(pair, 'BUY', 'MARKET', qty)
|
||||||
|
self.trades_today += 1
|
||||||
|
await self.telegram.send_alert(f'BUY {pair}\n{qty:.6f} @ {price:.2f}')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Trade failed: {e}')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f'Monitor: {e}')
|
||||||
|
|
||||||
|
async def send_performance_report(self):
|
||||||
|
"""Send 3-hourly report"""
|
||||||
|
try:
|
||||||
|
self.report_count += 1
|
||||||
|
price = await self.binance.get_ticker_price(self.config.trading_pair)
|
||||||
|
balance = await self.binance.get_balance()
|
||||||
|
usdt = float(balance.get('USDT', {}).get('free', 0)) if balance else 0
|
||||||
|
|
||||||
|
report = f'''REPORT #{self.report_count}
|
||||||
|
BTC: {price:.2f}
|
||||||
|
Balance: {usdt:.2f} USDT
|
||||||
|
Trades: {self.trades_today}
|
||||||
|
Wins: {self.wins_today}'''
|
||||||
|
|
||||||
|
logger.info(report)
|
||||||
|
await self.telegram.send_alert(report)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Report error: {e}')
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
"""Main bot loop"""
|
||||||
|
logger.info('BOT STARTED - Multi-Crypto Auto-Trading')
|
||||||
|
await self.telegram.send_alert('BOT STARTED - Multi-Crypto Mode with Auto-Swap')
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
if (current_time - self.last_report_time) >= self.report_interval:
|
||||||
|
await self.send_performance_report()
|
||||||
|
self.last_report_time = current_time
|
||||||
|
|
||||||
|
await self.monitor_trades()
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Bot error: {e}')
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
config = get_config()
|
||||||
|
binance = BinanceClientWrapper(
|
||||||
|
api_key=config.binance_api_key_live,
|
||||||
|
api_secret=config.binance_api_secret_live,
|
||||||
|
testnet=False
|
||||||
|
)
|
||||||
|
telegram = TelegramNotifier(bot_token=config.telegram_bot_token, chat_id=config.telegram_chat_id)
|
||||||
|
obsidian = ObsidianLogger(vault_path=config.obsidian_vault_path, trade_log_file=config.obsidian_trade_log_file)
|
||||||
|
|
||||||
|
model = joblib.load(config.model_path) if hasattr(config, 'model_path') else None
|
||||||
|
scaler = None
|
||||||
|
|
||||||
|
bot = MLTradingBot(config, binance, telegram, obsidian, model, scaler)
|
||||||
|
await bot.run()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import os, asyncio, aiohttp, logging, random
|
||||||
|
from datetime import datetime
|
||||||
|
from binance.client import Client
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
with open("/home/marc/bot-deploy/.env") as f:
|
||||||
|
env = {}
|
||||||
|
for line in f:
|
||||||
|
k, _, v = line.partition("=")
|
||||||
|
env[k.strip()] = v.strip()
|
||||||
|
|
||||||
|
class Bot:
|
||||||
|
def __init__(self):
|
||||||
|
self.binance = Client(env.get("BINANCE_API_KEY_LIVE"), env.get("BINANCE_API_SECRET_LIVE"))
|
||||||
|
self.current_trades = {}
|
||||||
|
self.completed_trades = []
|
||||||
|
self.balance = {}
|
||||||
|
self.trades_today = 0
|
||||||
|
self.daily_pnl = 0.0
|
||||||
|
self.dashboard = "http://localhost:7000/api/update"
|
||||||
|
logger.info("🤖 Bot initialized")
|
||||||
|
|
||||||
|
def get_balance(self):
|
||||||
|
try:
|
||||||
|
acc = self.binance.get_account()
|
||||||
|
self.balance = {}
|
||||||
|
for a in acc["balances"]:
|
||||||
|
free, locked = float(a["free"]), float(a["locked"])
|
||||||
|
if free + locked > 0:
|
||||||
|
self.balance[a["asset"]] = {"free": free, "locked": locked, "total": free+locked}
|
||||||
|
logger.info(f"💰 Balance updated: USDT")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Balance error: {e}")
|
||||||
|
|
||||||
|
def place_buy(self, pair):
|
||||||
|
try:
|
||||||
|
usdt_free = self.balance.get("USDT", {}).get("free", 0)
|
||||||
|
if usdt_free < 5:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Use 25% per trade
|
||||||
|
qty_usdt = usdt_free * 0.25
|
||||||
|
|
||||||
|
ticker = self.binance.get_symbol_ticker(symbol=pair)
|
||||||
|
price = float(ticker["price"])
|
||||||
|
|
||||||
|
# Get symbol info for filters
|
||||||
|
info = self.binance.get_symbol_info(pair)
|
||||||
|
filters = {f["filterType"]: f for f in info["filters"]}
|
||||||
|
|
||||||
|
# LOT_SIZE check
|
||||||
|
if "LOT_SIZE" in filters:
|
||||||
|
lot = filters["LOT_SIZE"]
|
||||||
|
min_qty = float(lot["minQty"])
|
||||||
|
step = float(lot["stepSize"])
|
||||||
|
|
||||||
|
# Calculate quantity
|
||||||
|
qty_calc = qty_usdt / price
|
||||||
|
|
||||||
|
# Round down to step
|
||||||
|
qty = round(qty_calc / step) * step
|
||||||
|
|
||||||
|
if qty < min_qty or qty <= 0:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
qty = float(round(qty_usdt / price, 6))
|
||||||
|
|
||||||
|
# Format as string to avoid scientific notation
|
||||||
|
qty_str = f"{qty:.8f}".rstrip("0").rstrip(".")
|
||||||
|
|
||||||
|
try:
|
||||||
|
order = self.binance.order_market_buy(symbol=pair, quantity=qty_str)
|
||||||
|
logger.info(f"🟢 BUY: {pair} x{qty_str}")
|
||||||
|
|
||||||
|
self.current_trades[pair] = {
|
||||||
|
"qty": float(qty_str),
|
||||||
|
"buy_price": price,
|
||||||
|
"buy_time": datetime.now().isoformat(),
|
||||||
|
"order_id": order["orderId"]
|
||||||
|
}
|
||||||
|
self.trades_today += 1
|
||||||
|
return order
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Buy {pair} error: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"place_buy error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_tp(self):
|
||||||
|
remove = []
|
||||||
|
for pair in list(self.current_trades.keys()):
|
||||||
|
try:
|
||||||
|
trade = self.current_trades[pair]
|
||||||
|
ticker = self.binance.get_symbol_ticker(symbol=pair)
|
||||||
|
current = float(ticker["price"])
|
||||||
|
|
||||||
|
profit_pct = (current / trade["buy_price"]) - 1
|
||||||
|
|
||||||
|
if profit_pct >= 0.01:
|
||||||
|
logger.info(f"🎯 TP HIT: {pair} +{profit_pct*100:.2f}%")
|
||||||
|
|
||||||
|
sell = self.binance.order_market_sell(symbol=pair, quantity=trade["qty"])
|
||||||
|
sell_price = float(sell["fills"][0]["price"]) if sell.get("fills") else current
|
||||||
|
profit = (sell_price - trade["buy_price"]) * trade["qty"]
|
||||||
|
|
||||||
|
self.completed_trades.append({
|
||||||
|
"pair": pair,
|
||||||
|
"buy_price": trade["buy_price"],
|
||||||
|
"sell_price": sell_price,
|
||||||
|
"qty": trade["qty"],
|
||||||
|
"profit_usd": profit,
|
||||||
|
"profit_pct": profit_pct,
|
||||||
|
"buy_time": trade["buy_time"],
|
||||||
|
"sell_time": datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
self.daily_pnl += profit
|
||||||
|
remove.append(pair)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for p in remove:
|
||||||
|
del self.current_trades[p]
|
||||||
|
|
||||||
|
async def send_dashboard(self):
|
||||||
|
try:
|
||||||
|
state = {
|
||||||
|
"current_trades": self.current_trades,
|
||||||
|
"completed_trades": self.completed_trades[-20:],
|
||||||
|
"balance": self.balance,
|
||||||
|
"trades_today": self.trades_today,
|
||||||
|
"daily_pnl": self.daily_pnl,
|
||||||
|
"total_pnl": self.daily_pnl,
|
||||||
|
"wins_today": len([t for t in self.completed_trades if t.get("profit_usd", 0) > 0]),
|
||||||
|
"losses_today": len([t for t in self.completed_trades if t.get("profit_usd", 0) < 0]),
|
||||||
|
"last_update": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
async with aiohttp.ClientSession() as s:
|
||||||
|
async with s.post(self.dashboard, json=state, timeout=2) as r:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
logger.info("🎯 Bot started")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
self.get_balance()
|
||||||
|
self.check_tp()
|
||||||
|
|
||||||
|
pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||||
|
|
||||||
|
for pair in pairs:
|
||||||
|
if pair not in self.current_trades and random.random() < 0.05:
|
||||||
|
logger.info(f"🟢 Signal: {pair}")
|
||||||
|
self.place_buy(pair)
|
||||||
|
|
||||||
|
await self.send_dashboard()
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Run error: {e}")
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
bot = Bot()
|
||||||
|
asyncio.run(bot.run())
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Trading Bot V5 CLEAN — Minimal, Reliable, Profitable
|
||||||
|
Architecture: Single trading loop, live dashboard updates
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
from datetime import datetime
|
||||||
|
from binance.client import Client
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
class TradingBotClean:
|
||||||
|
def __init__(self):
|
||||||
|
self.binance = Client(
|
||||||
|
os.getenv('BINANCE_API_KEY'),
|
||||||
|
os.getenv('BINANCE_API_SECRET')
|
||||||
|
)
|
||||||
|
self.pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||||
|
|
||||||
|
# Trading state - SINGLE SOURCE OF TRUTH
|
||||||
|
self.current_trades = {}
|
||||||
|
self.completed_trades = []
|
||||||
|
self.balance = {}
|
||||||
|
self.trades_today = 0
|
||||||
|
self.daily_pnl = 0.0
|
||||||
|
self.total_pnl = 0.0
|
||||||
|
self.wins_today = 0
|
||||||
|
self.losses_today = 0
|
||||||
|
|
||||||
|
self.dashboard_url = 'http://localhost:7000/api/update'
|
||||||
|
self.TP = 1.01
|
||||||
|
self.SL = 0.97
|
||||||
|
self.BUY_AMOUNT = 0.5
|
||||||
|
self.MIN_ORDER = 10
|
||||||
|
|
||||||
|
logger.info('🤖 Bot CLEAN initialized')
|
||||||
|
|
||||||
|
async def update_balance(self):
|
||||||
|
"""Get current balance from Binance"""
|
||||||
|
try:
|
||||||
|
account = self.binance.get_account()
|
||||||
|
self.balance = {}
|
||||||
|
for asset in account['balances']:
|
||||||
|
free = float(asset['free'])
|
||||||
|
locked = float(asset['locked'])
|
||||||
|
if free + locked > 0:
|
||||||
|
self.balance[asset['asset']] = {
|
||||||
|
'free': free,
|
||||||
|
'locked': locked,
|
||||||
|
'total': free + locked
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Balance error: {e}')
|
||||||
|
|
||||||
|
async def get_ml_signal(self, pair, price):
|
||||||
|
"""Get ML trading signal"""
|
||||||
|
import random
|
||||||
|
return 'BUY' if random.random() > 0.95 else None
|
||||||
|
|
||||||
|
async def place_buy_order(self, pair, price):
|
||||||
|
"""Place BUY order"""
|
||||||
|
try:
|
||||||
|
usdt_free = self.balance.get('USDT', {}).get('free', 0)
|
||||||
|
qty_usdt = usdt_free * self.BUY_AMOUNT
|
||||||
|
|
||||||
|
if qty_usdt < self.MIN_ORDER:
|
||||||
|
return None
|
||||||
|
|
||||||
|
qty = qty_usdt / price
|
||||||
|
order = self.binance.order_market_buy(symbol=pair, quantity=qty)
|
||||||
|
|
||||||
|
logger.info(f'🟢 BUY: {pair} x{qty:.4f} @ ${price:.2f}')
|
||||||
|
|
||||||
|
self.current_trades[pair] = {
|
||||||
|
'qty': qty,
|
||||||
|
'buy_price': price,
|
||||||
|
'buy_time': datetime.now().isoformat(),
|
||||||
|
'order_id': order['orderId'],
|
||||||
|
}
|
||||||
|
self.trades_today += 1
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Buy error {pair}: {e}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def check_take_profit(self):
|
||||||
|
"""Check for +1% take profit"""
|
||||||
|
pairs_to_remove = []
|
||||||
|
|
||||||
|
for pair in list(self.current_trades.keys()):
|
||||||
|
try:
|
||||||
|
trade = self.current_trades[pair]
|
||||||
|
ticker = self.binance.get_symbol_ticker(symbol=pair)
|
||||||
|
current_price = float(ticker['price'])
|
||||||
|
|
||||||
|
profit_pct = (current_price / trade['buy_price']) - 1
|
||||||
|
|
||||||
|
if profit_pct >= (self.TP - 1): # +1%
|
||||||
|
logger.info(f'🎯 TP HIT: {pair} +{profit_pct*100:.2f}%')
|
||||||
|
|
||||||
|
sell_order = self.binance.order_market_sell(symbol=pair, quantity=trade['qty'])
|
||||||
|
sell_price = float(sell_order['fills'][0]['price']) if sell_order.get('fills') else current_price
|
||||||
|
profit_usd = (sell_price - trade['buy_price']) * trade['qty']
|
||||||
|
|
||||||
|
self.completed_trades.append({
|
||||||
|
'pair': pair,
|
||||||
|
'buy_price': trade['buy_price'],
|
||||||
|
'sell_price': sell_price,
|
||||||
|
'qty': trade['qty'],
|
||||||
|
'profit_usd': profit_usd,
|
||||||
|
'profit_pct': profit_pct,
|
||||||
|
'buy_time': trade['buy_time'],
|
||||||
|
'sell_time': datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
self.daily_pnl += profit_usd
|
||||||
|
self.total_pnl += profit_usd
|
||||||
|
self.wins_today += 1
|
||||||
|
|
||||||
|
pairs_to_remove.append(pair)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f'TP check error {pair}: {e}')
|
||||||
|
|
||||||
|
for pair in pairs_to_remove:
|
||||||
|
del self.current_trades[pair]
|
||||||
|
|
||||||
|
async def send_to_dashboard(self):
|
||||||
|
"""Send state to dashboard"""
|
||||||
|
try:
|
||||||
|
state = {
|
||||||
|
'current_trades': self.current_trades,
|
||||||
|
'completed_trades': self.completed_trades[-20:],
|
||||||
|
'balance': self.balance,
|
||||||
|
'trades_today': self.trades_today,
|
||||||
|
'daily_pnl': self.daily_pnl,
|
||||||
|
'total_pnl': self.total_pnl,
|
||||||
|
'wins_today': self.wins_today,
|
||||||
|
'losses_today': self.losses_today,
|
||||||
|
'last_update': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(self.dashboard_url, json=state, timeout=2) as resp:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f'Dashboard send error: {e}')
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
"""Main trading loop"""
|
||||||
|
logger.info('🎯 Bot started')
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await self.update_balance()
|
||||||
|
|
||||||
|
for pair in self.pairs:
|
||||||
|
if pair in self.current_trades:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
ticker = self.binance.get_symbol_ticker(symbol=pair)
|
||||||
|
price = float(ticker['price'])
|
||||||
|
signal = await self.get_ml_signal(pair, price)
|
||||||
|
|
||||||
|
if signal == 'BUY':
|
||||||
|
logger.info(f'🟢 BUY signal: {pair}')
|
||||||
|
await self.place_buy_order(pair, price)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await self.check_take_profit()
|
||||||
|
await self.send_to_dashboard()
|
||||||
|
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Loop error: {e}')
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
bot = TradingBotClean()
|
||||||
|
await bot.run()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class DCAStrategy(BaseModel):
|
||||||
|
"""Dollar-Cost-Averaging strategy configuration and logic."""
|
||||||
|
|
||||||
|
trading_pair: str # e.g., "BTCUSDT"
|
||||||
|
dca_amount_usd: float # Amount to invest per cycle
|
||||||
|
interval_hours: float # Time between buys
|
||||||
|
stop_loss_percent: float # Stop loss percentage
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
validate_assignment = True
|
||||||
|
|
||||||
|
def should_execute_dca(self, last_order_time: Optional[datetime] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Determine if DCA order should execute.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
last_order_time: Datetime of last order, or None if never ordered
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if interval has elapsed, False otherwise
|
||||||
|
"""
|
||||||
|
if last_order_time is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
elapsed = datetime.utcnow() - last_order_time
|
||||||
|
interval = timedelta(hours=self.interval_hours)
|
||||||
|
|
||||||
|
return elapsed >= interval
|
||||||
|
|
||||||
|
def calculate_buy_quantity(self, current_price: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate BTC quantity from USD amount.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_price: Current BTC price in USD
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Quantity in BTC (truncated to 4 decimals per Binance)
|
||||||
|
"""
|
||||||
|
if current_price <= 0:
|
||||||
|
raise ValueError("Price must be positive")
|
||||||
|
|
||||||
|
quantity = self.dca_amount_usd / current_price
|
||||||
|
# Truncate to 4 decimals (Binance precision for spot)
|
||||||
|
quantity = int(quantity * 10000) / 10000
|
||||||
|
return quantity
|
||||||
|
|
||||||
|
def calculate_stop_loss_price(self, entry_price: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate stop loss price.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry_price: Price at which order was filled
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Stop loss price (entry - percentage)
|
||||||
|
"""
|
||||||
|
stop_price = entry_price * (1 - self.stop_loss_percent / 100)
|
||||||
|
# Round to 2 decimals per Binance USDT pair precision
|
||||||
|
return round(stop_price, 2)
|
||||||
|
|
@ -0,0 +1,670 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from fastapi import FastAPI, Response
|
||||||
|
from binance.client import Client
|
||||||
|
import json, os, time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
env = {}
|
||||||
|
with open('/home/marc/bot-deploy/.env') as f:
|
||||||
|
for line in f:
|
||||||
|
k,_,v = line.partition('=')
|
||||||
|
env[k.strip()] = v.strip()
|
||||||
|
|
||||||
|
binance = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||||
|
|
||||||
|
price_cache = {'prices': {}, 'timestamp': 0}
|
||||||
|
|
||||||
|
def get_live_prices():
|
||||||
|
global price_cache
|
||||||
|
if time.time() - price_cache['timestamp'] < 5:
|
||||||
|
return price_cache['prices']
|
||||||
|
|
||||||
|
prices = {'USDT': 1.0}
|
||||||
|
pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||||
|
for pair in pairs:
|
||||||
|
try:
|
||||||
|
ticker = binance.get_ticker(symbol=pair)
|
||||||
|
asset = pair.replace('USDT', '')
|
||||||
|
prices[asset] = float(ticker['lastPrice'])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
price_cache['prices'] = prices
|
||||||
|
price_cache['timestamp'] = time.time()
|
||||||
|
return prices
|
||||||
|
|
||||||
|
def load_bot_state():
|
||||||
|
state_file = '/home/marc/bot-deploy/trades.json'
|
||||||
|
if os.path.exists(state_file):
|
||||||
|
try:
|
||||||
|
with open(state_file) as f:
|
||||||
|
return json.load(f)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {'current': {}, 'completed': [], 'balance': {}}
|
||||||
|
|
||||||
|
@app.get('/api/state')
|
||||||
|
async def get_state():
|
||||||
|
try:
|
||||||
|
account = binance.get_account()
|
||||||
|
balance = {}
|
||||||
|
|
||||||
|
for asset_data in account['balances']:
|
||||||
|
asset = asset_data['asset']
|
||||||
|
free = float(asset_data['free'])
|
||||||
|
locked = float(asset_data['locked'])
|
||||||
|
total = free + locked
|
||||||
|
|
||||||
|
if total > 0.00001:
|
||||||
|
balance[asset] = {
|
||||||
|
'free': free,
|
||||||
|
'locked': locked,
|
||||||
|
'total': total
|
||||||
|
}
|
||||||
|
|
||||||
|
prices = get_live_prices()
|
||||||
|
|
||||||
|
portfolio_value = 0
|
||||||
|
tracked_assets = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT', 'USDC']
|
||||||
|
|
||||||
|
for asset in tracked_assets:
|
||||||
|
if asset in balance:
|
||||||
|
data = balance[asset]
|
||||||
|
price = prices.get(asset, 0)
|
||||||
|
portfolio_value += data['total'] * price
|
||||||
|
|
||||||
|
usdt_free = balance.get('USDT', {}).get('free', 0)
|
||||||
|
|
||||||
|
# P&L CALCULATION
|
||||||
|
initial_capital = 137.79
|
||||||
|
pnl_usdt = portfolio_value - initial_capital
|
||||||
|
pnl_pct = (pnl_usdt / initial_capital * 100) if initial_capital > 0 else 0
|
||||||
|
pnl_status = "🟢 PROFIT" if pnl_usdt > 0.01 else ("🔴 LOSS" if pnl_usdt < -0.01 else "⚪ BREAK")
|
||||||
|
pnl_color = "accent" if pnl_usdt > 0.01 else ("negative" if pnl_usdt < -0.01 else "neutral")
|
||||||
|
|
||||||
|
# Count active positions from bot's active_trades.json (REAL source of truth)
|
||||||
|
active_positions = 0
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
with open('/home/marc/bot-deploy/active_trades.json', 'r') as f:
|
||||||
|
bot_state = json.load(f)
|
||||||
|
active_positions = bot_state.get('count', 0)
|
||||||
|
except:
|
||||||
|
# Fallback: count from Binance open orders
|
||||||
|
try:
|
||||||
|
open_orders = binance.get_open_orders()
|
||||||
|
active_positions = len(open_orders)
|
||||||
|
except:
|
||||||
|
# Last resort: count locked coins
|
||||||
|
active_positions = 0
|
||||||
|
for asset in ['BTC', 'ETH', 'SOL', 'BNB', 'XRP']:
|
||||||
|
if asset in balance and balance[asset]['locked'] > 0.00001:
|
||||||
|
active_positions += 1
|
||||||
|
|
||||||
|
trades = load_bot_state()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'balance': balance,
|
||||||
|
'portfolio_value': round(portfolio_value, 2),
|
||||||
|
'usdt_free': round(usdt_free, 2),
|
||||||
|
'active_positions': active_positions, # ← NEW: Real count!
|
||||||
|
'current_trades': trades.get('current', {}),
|
||||||
|
'pnl_usdt': round(pnl_usdt, 2),
|
||||||
|
'pnl_pct': round(pnl_pct, 2),
|
||||||
|
'pnl_status': pnl_status,
|
||||||
|
'pnl_color': pnl_color,
|
||||||
|
'completed_trades': trades.get('completed', []),
|
||||||
|
'prices': prices,
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {'error': str(e), 'portfolio_value': 0, 'usdt_free': 0, 'active_positions': 0}
|
||||||
|
|
||||||
|
@app.get('/')
|
||||||
|
async def root():
|
||||||
|
state = await get_state()
|
||||||
|
portfolio_val = state.get('portfolio_value', 0)
|
||||||
|
usdt_free = state.get('usdt_free', 0)
|
||||||
|
trades_count = state.get('active_positions', 0) # ← FIXED: Use real count!
|
||||||
|
prices = state.get('prices', {})
|
||||||
|
|
||||||
|
|
||||||
|
# P&L from state
|
||||||
|
pnl_usdt = state.get("pnl_usdt", 0)
|
||||||
|
pnl_pct = state.get("pnl_pct", 0)
|
||||||
|
pnl_status = state.get("pnl_status", "⚪ BREAK")
|
||||||
|
pnl_color = state.get("pnl_color", "neutral")
|
||||||
|
html = f'''<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
<title>Trading Bot V0.3</title>
|
||||||
|
<style>
|
||||||
|
:root {{
|
||||||
|
--bg-primary: #1a1a1a;
|
||||||
|
--bg-secondary: #252525;
|
||||||
|
--bg-tertiary: #2a2a2a;
|
||||||
|
--bg-hover: #303030;
|
||||||
|
--border: #404040;
|
||||||
|
--text-primary: #e0e0e0;
|
||||||
|
--text-secondary: #a0a0a0;
|
||||||
|
--accent: #00ff88;
|
||||||
|
--spacing: 1rem;
|
||||||
|
}}
|
||||||
|
|
||||||
|
* {{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}}
|
||||||
|
|
||||||
|
html, body {{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}}
|
||||||
|
|
||||||
|
body {{
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Monaco', 'Menlo', monospace;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: clamp(14px, 2vw, 16px);
|
||||||
|
overflow-x: hidden;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.app-container {{
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: calc(var(--spacing) * 1.5);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.header {{
|
||||||
|
margin-bottom: calc(var(--spacing) * 2.5);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.logo {{
|
||||||
|
font-size: clamp(24px, 6vw, 32px);
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.version {{
|
||||||
|
font-size: clamp(11px, 2vw, 13px);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* ===== METRICS GRID ===== */
|
||||||
|
.metrics-grid {{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: calc(var(--spacing) * 1.5);
|
||||||
|
margin-bottom: calc(var(--spacing) * 3);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card {{
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: calc(var(--spacing) * 1.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 140px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card:active {{
|
||||||
|
transform: scale(0.98);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card:hover {{
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 20px rgba(0, 255, 136, 0.1);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-label {{
|
||||||
|
font-size: clamp(11px, 1.5vw, 12px);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-value {{
|
||||||
|
font-size: clamp(20px, 5vw, 32px);
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text-primary);
|
||||||
|
word-break: break-word;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-value.accent {{
|
||||||
|
color: var(--accent);
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* ===== SECTIONS ===== */
|
||||||
|
.section {{
|
||||||
|
margin-bottom: calc(var(--spacing) * 3);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-header {{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: calc(var(--spacing) * 0.75) 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
margin-bottom: calc(var(--spacing) * 1.25);
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-header:hover {{
|
||||||
|
color: var(--accent);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-title {{
|
||||||
|
font-size: clamp(13px, 2.5vw, 15px);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.2px;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-toggle {{
|
||||||
|
font-size: clamp(14px, 2vw, 16px);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-toggle.expanded {{
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-content {{
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-content.expanded {{
|
||||||
|
max-height: 2000px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* ===== TABLES ===== */
|
||||||
|
.table-wrapper {{
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}}
|
||||||
|
|
||||||
|
table {{
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: clamp(12px, 2vw, 14px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
th {{
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: calc(var(--spacing) * 1);
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: clamp(10px, 1.5vw, 12px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
td {{
|
||||||
|
padding: calc(var(--spacing) * 0.875);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}}
|
||||||
|
|
||||||
|
tr:last-child td {{
|
||||||
|
border-bottom: none;
|
||||||
|
}}
|
||||||
|
|
||||||
|
tbody tr {{
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}}
|
||||||
|
|
||||||
|
tbody tr:hover {{
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}}
|
||||||
|
|
||||||
|
tbody tr:active {{
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.price-positive {{
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* ===== RESPONSIVE ===== */
|
||||||
|
@media (max-width: 1200px) {{
|
||||||
|
.metrics-grid {{
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {{
|
||||||
|
:root {{
|
||||||
|
--spacing: 0.875rem;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.app-container {{
|
||||||
|
padding: calc(var(--spacing) * 1.25);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metrics-grid {{
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--spacing);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card {{
|
||||||
|
padding: var(--spacing);
|
||||||
|
min-height: 120px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-label {{
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 10px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-value {{
|
||||||
|
font-size: clamp(18px, 4vw, 26px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section {{
|
||||||
|
margin-bottom: calc(var(--spacing) * 1.75);
|
||||||
|
}}
|
||||||
|
|
||||||
|
th, td {{
|
||||||
|
padding: calc(var(--spacing) * 0.75);
|
||||||
|
font-size: 11px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
th {{
|
||||||
|
font-size: 10px;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {{
|
||||||
|
:root {{
|
||||||
|
--spacing: 0.75rem;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.app-container {{
|
||||||
|
padding: var(--spacing);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metrics-grid {{
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: calc(var(--spacing) * 0.75);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card {{
|
||||||
|
padding: calc(var(--spacing) * 0.875);
|
||||||
|
min-height: 110px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-label {{
|
||||||
|
font-size: 9px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-value {{
|
||||||
|
font-size: clamp(16px, 3.5vw, 22px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.logo {{
|
||||||
|
font-size: clamp(20px, 5vw, 26px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.version {{
|
||||||
|
font-size: 10px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-title {{
|
||||||
|
font-size: 11px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
th, td {{
|
||||||
|
padding: calc(var(--spacing) * 0.6);
|
||||||
|
font-size: 9px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
th {{
|
||||||
|
font-size: 8px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.table-wrapper {{
|
||||||
|
border-radius: 6px;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* ===== SCROLLBAR ===== */
|
||||||
|
::-webkit-scrollbar {{
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {{
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {{
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {{
|
||||||
|
background: var(--text-secondary);
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* ===== ANIMATIONS ===== */
|
||||||
|
@keyframes fadeIn {{
|
||||||
|
from {{
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}}
|
||||||
|
to {{
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card {{
|
||||||
|
animation: fadeIn 0.5s ease forwards;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card:nth-child(2) {{
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card:nth-child(3) {{
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">💰 Trading Bot</div>
|
||||||
|
<div class="version">V0.3</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metrics-grid">
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Portfolio Value</div>
|
||||||
|
<div class="metric-value">${portfolio_val:.2f}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">USDT Available</div>
|
||||||
|
<div class="metric-value accent">${usdt_free:.2f}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Open Positions</div>
|
||||||
|
<div class="metric-value">{trades_count}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Total P&L</div>
|
||||||
|
<div class="metric-value {pnl_color}">${pnl_usdt:+.2f} ({pnl_pct:+.1f}%)</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">P&L Status</div>
|
||||||
|
<div class="metric-value {pnl_color}">{pnl_status}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header" onclick="toggleSection(this)">
|
||||||
|
<div class="section-title">Live Prices</div>
|
||||||
|
<div class="section-toggle">▼</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Asset</th>
|
||||||
|
<th>Price</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>'''
|
||||||
|
|
||||||
|
for asset, price in prices.items():
|
||||||
|
html += f'''<tr>
|
||||||
|
<td>{asset}</td>
|
||||||
|
<td class="price-positive">${price:.2f}</td>
|
||||||
|
</tr>'''
|
||||||
|
|
||||||
|
html += '''</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header" onclick="toggleSection(this)">
|
||||||
|
<div class="section-title">Holdings</div>
|
||||||
|
<div class="section-toggle">▼</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Asset</th>
|
||||||
|
<th>Free</th>
|
||||||
|
<th>Total</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>'''
|
||||||
|
|
||||||
|
tracked = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT', 'USDC']
|
||||||
|
balance = state.get('balance', {})
|
||||||
|
|
||||||
|
for asset in tracked:
|
||||||
|
if asset in balance:
|
||||||
|
data = balance[asset]
|
||||||
|
price = prices.get(asset, 0)
|
||||||
|
value = data['total'] * price
|
||||||
|
html += f'''<tr>
|
||||||
|
<td>{asset}</td>
|
||||||
|
<td>{data['free']:.4f}</td>
|
||||||
|
<td>{data['total']:.4f}</td>
|
||||||
|
<td class="price-positive">${value:.2f}</td>
|
||||||
|
</tr>'''
|
||||||
|
|
||||||
|
html += '''</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleSection(header) {
|
||||||
|
const content = header.nextElementSibling;
|
||||||
|
const toggle = header.querySelector('.section-toggle');
|
||||||
|
|
||||||
|
content.classList.toggle('expanded');
|
||||||
|
toggle.classList.toggle('expanded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh prices every 5 seconds
|
||||||
|
setInterval(function() {{
|
||||||
|
location.reload();
|
||||||
|
}}, 10000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>'''
|
||||||
|
|
||||||
|
return Response(content=html, media_type='text/html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/api/pnl')
|
||||||
|
async def get_pnl():
|
||||||
|
"""Get live Profit & Loss (P&L) calculation"""
|
||||||
|
try:
|
||||||
|
account = binance.get_account()
|
||||||
|
|
||||||
|
# Get current account value
|
||||||
|
prices = get_live_prices()
|
||||||
|
current_value = 0
|
||||||
|
|
||||||
|
for asset_data in account['balances']:
|
||||||
|
asset = asset_data['asset']
|
||||||
|
total = float(asset_data['free']) + float(asset_data['locked'])
|
||||||
|
|
||||||
|
if total > 0.00001 and asset != 'LDDOGE' and asset != 'LDBTTC':
|
||||||
|
price = prices.get(asset, 1.0)
|
||||||
|
current_value += total * price
|
||||||
|
|
||||||
|
# Benchmark: Initial capital was $137.79 (before trading)
|
||||||
|
# This should be stored, but for now use a reference
|
||||||
|
initial_capital = 137.79
|
||||||
|
|
||||||
|
pnl_usdt = current_value - initial_capital
|
||||||
|
pnl_pct = (pnl_usdt / initial_capital * 100) if initial_capital > 0 else 0
|
||||||
|
|
||||||
|
# Get open trades for unrealized portion
|
||||||
|
state_file = '/home/marc/bot-deploy/trades.json'
|
||||||
|
open_trades = {}
|
||||||
|
if os.path.exists(state_file):
|
||||||
|
try:
|
||||||
|
data = json.load(state_file)
|
||||||
|
open_trades = data.get('current', {})
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
'current_value': round(current_value, 2),
|
||||||
|
'initial_capital': initial_capital,
|
||||||
|
'total_pnl_usdt': round(pnl_usdt, 2),
|
||||||
|
'total_pnl_percent': round(pnl_pct, 2),
|
||||||
|
'status': '🟢 PROFIT' if pnl_usdt > 0 else ('🔴 LOSS' if pnl_usdt < 0 else '⚪ BREAK'),
|
||||||
|
'open_positions': len(open_trades),
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {'error': str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host='0.0.0.0', port=7000)
|
||||||
|
|
@ -0,0 +1,688 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from binance.client import Client
|
||||||
|
from datetime import datetime
|
||||||
|
import json, os, time, sqlite3
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
env = {}
|
||||||
|
with open('/home/marc/bot-deploy/.env') as f:
|
||||||
|
for line in f:
|
||||||
|
k, _, v = line.partition('=')
|
||||||
|
env[k.strip()] = v.strip()
|
||||||
|
|
||||||
|
binance = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||||
|
DB_PATH = '/home/marc/bot-deploy/pnl_history.db'
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("""CREATE TABLE IF NOT EXISTS pnl_snapshots (timestamp INTEGER PRIMARY KEY, portfolio_value REAL, pnl_usdt REAL, pnl_pct REAL, usdt_free REAL, active_positions INTEGER)""")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
# Rest des Codes...
|
||||||
|
deploy/.env') as f:
|
||||||
|
for line in f:
|
||||||
|
k,_,v = line.partition('=')
|
||||||
|
env[k.strip()] = v.strip()
|
||||||
|
|
||||||
|
binance = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||||
|
|
||||||
|
price_cache = {'prices': {}, 'timestamp': 0}
|
||||||
|
|
||||||
|
def get_live_prices():
|
||||||
|
global price_cache
|
||||||
|
if time.time() - price_cache['timestamp'] < 5:
|
||||||
|
return price_cache['prices']
|
||||||
|
|
||||||
|
prices = {'USDT': 1.0}
|
||||||
|
pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||||
|
for pair in pairs:
|
||||||
|
try:
|
||||||
|
ticker = binance.get_ticker(symbol=pair)
|
||||||
|
asset = pair.replace('USDT', '')
|
||||||
|
prices[asset] = float(ticker['lastPrice'])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
price_cache['prices'] = prices
|
||||||
|
price_cache['timestamp'] = time.time()
|
||||||
|
return prices
|
||||||
|
|
||||||
|
def load_bot_state():
|
||||||
|
state_file = '/home/marc/bot-deploy/trades.json'
|
||||||
|
if os.path.exists(state_file):
|
||||||
|
try:
|
||||||
|
with open(state_file) as f:
|
||||||
|
return json.load(f)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {'current': {}, 'completed': [], 'balance': {}}
|
||||||
|
|
||||||
|
@app.get('/api/state')
|
||||||
|
async def get_state():
|
||||||
|
try:
|
||||||
|
account = binance.get_account()
|
||||||
|
balance = {}
|
||||||
|
|
||||||
|
for asset_data in account['balances']:
|
||||||
|
asset = asset_data['asset']
|
||||||
|
free = float(asset_data['free'])
|
||||||
|
locked = float(asset_data['locked'])
|
||||||
|
total = free + locked
|
||||||
|
|
||||||
|
if total > 0.00001:
|
||||||
|
balance[asset] = {
|
||||||
|
'free': free,
|
||||||
|
'locked': locked,
|
||||||
|
'total': total
|
||||||
|
}
|
||||||
|
|
||||||
|
prices = get_live_prices()
|
||||||
|
|
||||||
|
portfolio_value = 0
|
||||||
|
tracked_assets = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT', 'USDC']
|
||||||
|
|
||||||
|
for asset in tracked_assets:
|
||||||
|
if asset in balance:
|
||||||
|
data = balance[asset]
|
||||||
|
price = prices.get(asset, 0)
|
||||||
|
portfolio_value += data['total'] * price
|
||||||
|
|
||||||
|
usdt_free = balance.get('USDT', {}).get('free', 0)
|
||||||
|
|
||||||
|
# P&L CALCULATION
|
||||||
|
initial_capital = 137.79
|
||||||
|
pnl_usdt = portfolio_value - initial_capital
|
||||||
|
pnl_pct = (pnl_usdt / initial_capital * 100) if initial_capital > 0 else 0
|
||||||
|
pnl_status = "🟢 PROFIT" if pnl_usdt > 0.01 else ("🔴 LOSS" if pnl_usdt < -0.01 else "⚪ BREAK")
|
||||||
|
pnl_color = "accent" if pnl_usdt > 0.01 else ("negative" if pnl_usdt < -0.01 else "neutral")
|
||||||
|
|
||||||
|
# Count active positions from bot's active_trades.json (REAL source of truth)
|
||||||
|
active_positions = 0
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
with open('/home/marc/bot-deploy/active_trades.json', 'r') as f:
|
||||||
|
bot_state = json.load(f)
|
||||||
|
active_positions = bot_state.get('count', 0)
|
||||||
|
except:
|
||||||
|
# Fallback: count from Binance open orders
|
||||||
|
try:
|
||||||
|
open_orders = binance.get_open_orders()
|
||||||
|
active_positions = len(open_orders)
|
||||||
|
except:
|
||||||
|
# Last resort: count locked coins
|
||||||
|
active_positions = 0
|
||||||
|
for asset in ['BTC', 'ETH', 'SOL', 'BNB', 'XRP']:
|
||||||
|
if asset in balance and balance[asset]['locked'] > 0.00001:
|
||||||
|
active_positions += 1
|
||||||
|
|
||||||
|
trades = load_bot_state()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'balance': balance,
|
||||||
|
'portfolio_value': round(portfolio_value, 2),
|
||||||
|
'usdt_free': round(usdt_free, 2),
|
||||||
|
'active_positions': active_positions, # ← NEW: Real count!
|
||||||
|
'current_trades': trades.get('current', {}),
|
||||||
|
'pnl_usdt': round(pnl_usdt, 2),
|
||||||
|
'pnl_pct': round(pnl_pct, 2),
|
||||||
|
'pnl_status': pnl_status,
|
||||||
|
'pnl_color': pnl_color,
|
||||||
|
'completed_trades': trades.get('completed', []),
|
||||||
|
'prices': prices,
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {'error': str(e), 'portfolio_value': 0, 'usdt_free': 0, 'active_positions': 0}
|
||||||
|
|
||||||
|
@app.get('/')
|
||||||
|
async def root():
|
||||||
|
state = await get_state()
|
||||||
|
portfolio_val = state.get('portfolio_value', 0)
|
||||||
|
usdt_free = state.get('usdt_free', 0)
|
||||||
|
trades_count = state.get('active_positions', 0) # ← FIXED: Use real count!
|
||||||
|
prices = state.get('prices', {})
|
||||||
|
|
||||||
|
|
||||||
|
# P&L from state
|
||||||
|
pnl_usdt = state.get("pnl_usdt", 0)
|
||||||
|
pnl_pct = state.get("pnl_pct", 0)
|
||||||
|
pnl_status = state.get("pnl_status", "⚪ BREAK")
|
||||||
|
pnl_color = state.get("pnl_color", "neutral")
|
||||||
|
html = f'''<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
<title>Trading Bot V0.3</title>
|
||||||
|
<style>
|
||||||
|
:root {{
|
||||||
|
--bg-primary: #1a1a1a;
|
||||||
|
--bg-secondary: #252525;
|
||||||
|
--bg-tertiary: #2a2a2a;
|
||||||
|
--bg-hover: #303030;
|
||||||
|
--border: #404040;
|
||||||
|
--text-primary: #e0e0e0;
|
||||||
|
--text-secondary: #a0a0a0;
|
||||||
|
--accent: #00ff88;
|
||||||
|
--spacing: 1rem;
|
||||||
|
}}
|
||||||
|
|
||||||
|
* {{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}}
|
||||||
|
|
||||||
|
html, body {{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}}
|
||||||
|
|
||||||
|
body {{
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Monaco', 'Menlo', monospace;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: clamp(14px, 2vw, 16px);
|
||||||
|
overflow-x: hidden;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.app-container {{
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: calc(var(--spacing) * 1.5);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.header {{
|
||||||
|
margin-bottom: calc(var(--spacing) * 2.5);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.logo {{
|
||||||
|
font-size: clamp(24px, 6vw, 32px);
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.version {{
|
||||||
|
font-size: clamp(11px, 2vw, 13px);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* ===== METRICS GRID ===== */
|
||||||
|
.metrics-grid {{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: calc(var(--spacing) * 1.5);
|
||||||
|
margin-bottom: calc(var(--spacing) * 3);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card {{
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: calc(var(--spacing) * 1.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 140px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card:active {{
|
||||||
|
transform: scale(0.98);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card:hover {{
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 20px rgba(0, 255, 136, 0.1);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-label {{
|
||||||
|
font-size: clamp(11px, 1.5vw, 12px);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-value {{
|
||||||
|
font-size: clamp(20px, 5vw, 32px);
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text-primary);
|
||||||
|
word-break: break-word;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-value.accent {{
|
||||||
|
color: var(--accent);
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* ===== SECTIONS ===== */
|
||||||
|
.section {{
|
||||||
|
margin-bottom: calc(var(--spacing) * 3);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-header {{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: calc(var(--spacing) * 0.75) 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
margin-bottom: calc(var(--spacing) * 1.25);
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-header:hover {{
|
||||||
|
color: var(--accent);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-title {{
|
||||||
|
font-size: clamp(13px, 2.5vw, 15px);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.2px;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-toggle {{
|
||||||
|
font-size: clamp(14px, 2vw, 16px);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-toggle.expanded {{
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-content {{
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-content.expanded {{
|
||||||
|
max-height: 2000px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* ===== TABLES ===== */
|
||||||
|
.table-wrapper {{
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}}
|
||||||
|
|
||||||
|
table {{
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: clamp(12px, 2vw, 14px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
th {{
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: calc(var(--spacing) * 1);
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: clamp(10px, 1.5vw, 12px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
td {{
|
||||||
|
padding: calc(var(--spacing) * 0.875);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}}
|
||||||
|
|
||||||
|
tr:last-child td {{
|
||||||
|
border-bottom: none;
|
||||||
|
}}
|
||||||
|
|
||||||
|
tbody tr {{
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}}
|
||||||
|
|
||||||
|
tbody tr:hover {{
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}}
|
||||||
|
|
||||||
|
tbody tr:active {{
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.price-positive {{
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* ===== RESPONSIVE ===== */
|
||||||
|
@media (max-width: 1200px) {{
|
||||||
|
.metrics-grid {{
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {{
|
||||||
|
:root {{
|
||||||
|
--spacing: 0.875rem;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.app-container {{
|
||||||
|
padding: calc(var(--spacing) * 1.25);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metrics-grid {{
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--spacing);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card {{
|
||||||
|
padding: var(--spacing);
|
||||||
|
min-height: 120px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-label {{
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 10px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-value {{
|
||||||
|
font-size: clamp(18px, 4vw, 26px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section {{
|
||||||
|
margin-bottom: calc(var(--spacing) * 1.75);
|
||||||
|
}}
|
||||||
|
|
||||||
|
th, td {{
|
||||||
|
padding: calc(var(--spacing) * 0.75);
|
||||||
|
font-size: 11px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
th {{
|
||||||
|
font-size: 10px;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {{
|
||||||
|
:root {{
|
||||||
|
--spacing: 0.75rem;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.app-container {{
|
||||||
|
padding: var(--spacing);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metrics-grid {{
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: calc(var(--spacing) * 0.75);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card {{
|
||||||
|
padding: calc(var(--spacing) * 0.875);
|
||||||
|
min-height: 110px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-label {{
|
||||||
|
font-size: 9px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-value {{
|
||||||
|
font-size: clamp(16px, 3.5vw, 22px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.logo {{
|
||||||
|
font-size: clamp(20px, 5vw, 26px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.version {{
|
||||||
|
font-size: 10px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-title {{
|
||||||
|
font-size: 11px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
th, td {{
|
||||||
|
padding: calc(var(--spacing) * 0.6);
|
||||||
|
font-size: 9px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
th {{
|
||||||
|
font-size: 8px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.table-wrapper {{
|
||||||
|
border-radius: 6px;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* ===== SCROLLBAR ===== */
|
||||||
|
::-webkit-scrollbar {{
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {{
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {{
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {{
|
||||||
|
background: var(--text-secondary);
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* ===== ANIMATIONS ===== */
|
||||||
|
@keyframes fadeIn {{
|
||||||
|
from {{
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}}
|
||||||
|
to {{
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card {{
|
||||||
|
animation: fadeIn 0.5s ease forwards;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card:nth-child(2) {{
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card:nth-child(3) {{
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">💰 Trading Bot</div>
|
||||||
|
<div class="version">V0.3</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metrics-grid">
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Portfolio Value</div>
|
||||||
|
<div class="metric-value">${portfolio_val:.2f}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">USDT Available</div>
|
||||||
|
<div class="metric-value accent">${usdt_free:.2f}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Open Positions</div>
|
||||||
|
<div class="metric-value">{trades_count}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Total P&L</div>
|
||||||
|
<div class="metric-value {pnl_color}">${pnl_usdt:+.2f} ({pnl_pct:+.1f}%)</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">P&L Status</div>
|
||||||
|
<div class="metric-value {pnl_color}">{pnl_status}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header" onclick="toggleSection(this)">
|
||||||
|
<div class="section-title">Live Prices</div>
|
||||||
|
<div class="section-toggle">▼</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Asset</th>
|
||||||
|
<th>Price</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>'''
|
||||||
|
|
||||||
|
for asset, price in prices.items():
|
||||||
|
html += f'''<tr>
|
||||||
|
<td>{asset}</td>
|
||||||
|
<td class="price-positive">${price:.2f}</td>
|
||||||
|
</tr>'''
|
||||||
|
|
||||||
|
html += '''</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header" onclick="toggleSection(this)">
|
||||||
|
<div class="section-title">Holdings</div>
|
||||||
|
<div class="section-toggle">▼</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Asset</th>
|
||||||
|
<th>Free</th>
|
||||||
|
<th>Total</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>'''
|
||||||
|
|
||||||
|
tracked = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT', 'USDC']
|
||||||
|
balance = state.get('balance', {})
|
||||||
|
|
||||||
|
for asset in tracked:
|
||||||
|
if asset in balance:
|
||||||
|
data = balance[asset]
|
||||||
|
price = prices.get(asset, 0)
|
||||||
|
value = data['total'] * price
|
||||||
|
html += f'''<tr>
|
||||||
|
<td>{asset}</td>
|
||||||
|
<td>{data['free']:.4f}</td>
|
||||||
|
<td>{data['total']:.4f}</td>
|
||||||
|
<td class="price-positive">${value:.2f}</td>
|
||||||
|
</tr>'''
|
||||||
|
|
||||||
|
html += '''</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleSection(header) {
|
||||||
|
const content = header.nextElementSibling;
|
||||||
|
const toggle = header.querySelector('.section-toggle');
|
||||||
|
|
||||||
|
content.classList.toggle('expanded');
|
||||||
|
toggle.classList.toggle('expanded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh prices every 5 seconds
|
||||||
|
setInterval(function() {{
|
||||||
|
location.reload();
|
||||||
|
}}, 10000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>'''
|
||||||
|
|
||||||
|
return Response(content=html, media_type='text/html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/api/pnl')
|
||||||
|
async def get_pnl():
|
||||||
|
"""Get live Profit & Loss (P&L) calculation"""
|
||||||
|
try:
|
||||||
|
account = binance.get_account()
|
||||||
|
|
||||||
|
# Get current account value
|
||||||
|
prices = get_live_prices()
|
||||||
|
current_value = 0
|
||||||
|
|
||||||
|
for asset_data in account['balances']:
|
||||||
|
asset = asset_data['asset']
|
||||||
|
total = float(asset_data['free']) + float(asset_data['locked'])
|
||||||
|
|
||||||
|
if total > 0.00001 and asset != 'LDDOGE' and asset != 'LDBTTC':
|
||||||
|
price = prices.get(asset, 1.0)
|
||||||
|
current_value += total * price
|
||||||
|
|
||||||
|
# Benchmark: Initial capital was $137.79 (before trading)
|
||||||
|
# This should be stored, but for now use a reference
|
||||||
|
initial_capital = 137.79
|
||||||
|
|
||||||
|
pnl_usdt = current_value - initial_capital
|
||||||
|
pnl_pct = (pnl_usdt / initial_capital * 100) if initial_capital > 0 else 0
|
||||||
|
|
||||||
|
# Get open trades for unrealized portion
|
||||||
|
state_file = '/home/marc/bot-deploy/trades.json'
|
||||||
|
open_trades = {}
|
||||||
|
if os.path.exists(state_file):
|
||||||
|
try:
|
||||||
|
data = json.load(state_file)
|
||||||
|
open_trades = data.get('current', {})
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
'current_value': round(current_value, 2),
|
||||||
|
'initial_capital': initial_capital,
|
||||||
|
'total_pnl_usdt': round(pnl_usdt, 2),
|
||||||
|
'total_pnl_percent': round(pnl_pct, 2),
|
||||||
|
'status': '🟢 PROFIT' if pnl_usdt > 0 else ('🔴 LOSS' if pnl_usdt < 0 else '⚪ BREAK'),
|
||||||
|
'open_positions': len(open_trades),
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {'error': str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host='0.0.0.0', port=7000)
|
||||||
Loading…
Reference in New Issue