v0.7: Coin-Level Contrarian - per-coin signals (9 coins: BTC, ETH, BNB, XRP, SOL, ADA, DOGE, DOT, AVAX)

This commit is contained in:
Marc Blatter 2026-07-30 19:20:31 +02:00
parent 2b71ec3204
commit a199e2313d
1 changed files with 59 additions and 89 deletions

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Trading Bot v0.6 - Contrarian Buy/Sell (Mean Reversion) Strategy""" """Trading Bot v0.7 - Coin-Level Contrarian (Mean Reversion per Coin)"""
import os, json, time, logging, sqlite3 import os, json, time, logging, sqlite3
from datetime import datetime, timedelta from datetime import datetime, timedelta
from dotenv import load_dotenv from dotenv import load_dotenv
@ -16,24 +16,24 @@ if not API_KEY or not API_SECRET:
logger.error("Missing API keys") logger.error("Missing API keys")
exit(1) exit(1)
SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XRPUSDT', 'SOLUSDT'] # 9 Coins (Coin-Level Contrarian)
TRACKED_COINS = ['BTC', 'ETH', 'BNB', 'XRP', 'SOL'] SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XRPUSDT', 'SOLUSDT', 'ADAUSDT', 'DOGUSDT', 'DOTUSDT', 'AVAXUSDT']
TRACKED_COINS = ['BTC', 'ETH', 'BNB', 'XRP', 'SOL', 'ADA', 'DOGE', 'DOT', 'AVAX']
MIN_TRADE_USDT = 12.00 MIN_TRADE_USDT = 12.00
MAX_POSITION_PCT = 0.07 MAX_POSITION_PCT = 0.07
TAKE_PROFIT_PCT = 0.015 TAKE_PROFIT_PCT = 0.015
STOP_LOSS_PCT = -0.008 STOP_LOSS_PCT = -0.008
CYCLE_SEC = 60 CYCLE_SEC = 60
# CONTRARIAN THRESHOLDS # COIN-LEVEL CONTRARIAN THRESHOLDS
CONTRARIAN_BUY_THRESHOLD = -2.0 # Buy when market DOWN 2%+ COIN_BUY_THRESHOLD = -2.0 # Buy when coin DOWN 2% (24h)
CONTRARIAN_SELL_THRESHOLD = +2.0 # Sell when market UP 2%+ COIN_SELL_THRESHOLD = +2.0 # Sell when coin UP 2% (24h)
LOOKBACK_HOURS = 24 # Compare last 24h return LOOKBACK_HOURS = 24
class TradingBotV06: class TradingBotV07:
def __init__(self): def __init__(self):
self.client = Client(API_KEY, API_SECRET) self.client = Client(API_KEY, API_SECRET)
self.price_history = {sym: [] for sym in SYMBOLS} self.price_history = {sym: [] for sym in SYMBOLS}
self.daily_opens = {} # Store 24h ago prices
self.active_trades = {} self.active_trades = {}
self.portfolio_value = 0 self.portfolio_value = 0
self.max_trade_usdt = 0 self.max_trade_usdt = 0
@ -61,50 +61,51 @@ class TradingBotV06:
except Exception as e: except Exception as e:
logger.warning(f"Recovery failed: {e}") logger.warning(f"Recovery failed: {e}")
logger.info("[v0.6 INIT] Contrarian Buy/Sell (Mean Reversion) Strategy") logger.info("[v0.7 INIT] Coin-Level Contrarian (9 Coins: BTC, ETH, BNB, XRP, SOL, ADA, DOGE, DOT, AVAX)")
def calculate_market_return(self): def calculate_coin_return_24h(self, symbol):
"""Calculate 24h market-wide return (Average of all symbols)""" """Calculate 24h return for SINGLE coin"""
returns = [] if len(self.price_history[symbol]) < 2:
return None
for symbol in SYMBOLS: prices = self.price_history[symbol]
if len(self.price_history[symbol]) < 2: current = prices[-1]
continue
current = self.price_history[symbol][-1]
# Get price from ~24h ago (or earliest if less than 24h data)
reference_idx = max(0, len(self.price_history[symbol]) - 1440) # 1440 = 24h * 60min
reference = self.price_history[symbol][reference_idx]
if reference > 0:
ret = ((current - reference) / reference) * 100
returns.append(ret)
if returns: # Get price from ~24h ago (or earliest if <24h data)
avg_return = sum(returns) / len(returns) reference_idx = max(0, len(prices) - 1440) # 1440 = 24h * 60min
return avg_return reference = prices[reference_idx]
return 0.0 if reference > 0:
ret = ((current - reference) / reference) * 100
return ret
return None
def is_contrarian_buy_signal(self, symbol): def is_coin_buy_signal(self, symbol):
"""Buy when MARKET DOWN 2%+ (Mean Reversion: expect bounce)""" """Buy when COIN DOWN 2%+ (Coin-Level Contrarian)"""
market_return = self.calculate_market_return() ret = self.calculate_coin_return_24h(symbol)
buy_signal = market_return < CONTRARIAN_BUY_THRESHOLD if ret is None:
return False
buy_signal = ret < COIN_BUY_THRESHOLD
if buy_signal: if buy_signal:
logger.info(f"[SIGNAL-CONTRARIAN-BUY] Market DOWN {market_return:.2f}% (Threshold: {CONTRARIAN_BUY_THRESHOLD}%)") logger.info(f"[SIGNAL-COIN-BUY] {symbol} 24h return {ret:.2f}% (Threshold: {COIN_BUY_THRESHOLD}%)")
return buy_signal return buy_signal
def is_contrarian_sell_signal(self, symbol): def is_coin_sell_signal(self, symbol):
"""Sell when MARKET UP 2%+ (Take profits on rally)""" """Sell when COIN UP 2%+ (Coin-Level Contrarian)"""
market_return = self.calculate_market_return() ret = self.calculate_coin_return_24h(symbol)
sell_signal = market_return > CONTRARIAN_SELL_THRESHOLD if ret is None:
return False
sell_signal = ret > COIN_SELL_THRESHOLD
if sell_signal: if sell_signal:
logger.info(f"[SIGNAL-CONTRARIAN-SELL] Market UP {market_return:.2f}% (Threshold: {CONTRARIAN_SELL_THRESHOLD}%)") logger.info(f"[SIGNAL-COIN-SELL] {symbol} 24h return {ret:.2f}% (Threshold: {COIN_SELL_THRESHOLD}%)")
return sell_signal return sell_signal
@ -137,7 +138,7 @@ class TradingBotV06:
self.portfolio_value = portfolio_value self.portfolio_value = portfolio_value
self.max_trade_usdt = portfolio_value * MAX_POSITION_PCT self.max_trade_usdt = portfolio_value * MAX_POSITION_PCT
logger.info(f"[v0.6] USDT={usdt_available:.2f} | Portfolio={portfolio_value:.2f} | Max={self.max_trade_usdt:.2f}") logger.info(f"[v0.7] USDT={usdt_available:.2f} | Portfolio={portfolio_value:.2f} | Max={self.max_trade_usdt:.2f}")
return usdt_available, portfolio_value return usdt_available, portfolio_value
except: except:
return 0, 0 return 0, 0
@ -197,7 +198,7 @@ class TradingBotV06:
'entry_time': datetime.now().isoformat() 'entry_time': datetime.now().isoformat()
} }
logger.info(f"[BUY-v0.6] {symbol} {qty} @ {price} (CONTRARIAN: Market DOWN)") logger.info(f"[BUY-v0.7] {symbol} {qty} @ {price} (COIN-LEVEL CONTRARIAN: DOWN 2%+)")
return order return order
except: except:
return None return None
@ -215,7 +216,7 @@ class TradingBotV06:
entry = self.active_trades[symbol]['entry_price'] entry = self.active_trades[symbol]['entry_price']
pnl = ((price - entry) / entry) * 100 pnl = ((price - entry) / entry) * 100
logger.info(f"[SELL-v0.6] {symbol} {qty} @ {price} (CONTRARIAN: Market UP, P&L: {pnl:+.2f}%)") logger.info(f"[SELL-v0.7] {symbol} {qty} @ {price} (COIN-LEVEL CONTRARIAN: UP 2%+, P&L: {pnl:+.2f}%)")
del self.active_trades[symbol] del self.active_trades[symbol]
return order return order
@ -289,55 +290,24 @@ class TradingBotV06:
if len(self.price_history[symbol]) > 1440: # Keep 24h history if len(self.price_history[symbol]) > 1440: # Keep 24h history
self.price_history[symbol].pop(0) self.price_history[symbol].pop(0)
# Check for Contrarian SELL (Market UP 2%+) # Check for COIN-LEVEL SELL (each coin UP 2%+)
if self.is_contrarian_sell_signal(None): for symbol in list(self.active_trades.keys()):
# Sell holdings that are profitable if self.is_coin_sell_signal(symbol):
for symbol in list(self.active_trades.keys()): self.place_sell_order(symbol)
if symbol not in self.active_trades: break # One sell per cycle
continue
current = self.get_current_price(symbol)
if not current:
continue
entry = self.active_trades[symbol]['entry_price']
pnl_pct = ((current - entry) / entry) * 100
# Only sell if we have profit (avoid unnecessary SL hits on rally)
if pnl_pct > 0.5:
self.place_sell_order(symbol)
break # One sell per cycle
# Check TP/SL # Check TP/SL
self.check_and_close_positions() self.check_and_close_positions()
# Check for Contrarian BUY (Market DOWN 2%+) # Check for COIN-LEVEL BUY (each coin DOWN 2%+)
buy_signal = self.is_contrarian_buy_signal(None) for symbol in SYMBOLS:
if buy_signal and usdt_free >= MIN_TRADE_USDT: if symbol in self.active_trades:
# Find best coin to buy (the one with biggest loss) continue # Skip already held
worst_coin = None
worst_return = 0
for symbol in SYMBOLS: if self.is_coin_buy_signal(symbol) and usdt_free >= MIN_TRADE_USDT:
if symbol in self.active_trades:
continue # Skip already held
if len(self.price_history[symbol]) < 2:
continue
current = self.price_history[symbol][-1]
ref_idx = max(0, len(self.price_history[symbol]) - 1440)
reference = self.price_history[symbol][ref_idx]
if reference > 0:
ret = ((current - reference) / reference) * 100
if ret < worst_return:
worst_return = ret
worst_coin = symbol
if worst_coin:
trade_amount = min(max(MIN_TRADE_USDT, self.max_trade_usdt), usdt_free * 0.5) trade_amount = min(max(MIN_TRADE_USDT, self.max_trade_usdt), usdt_free * 0.5)
self.place_buy_order(worst_coin, trade_amount) self.place_buy_order(symbol, trade_amount)
break # One buy per cycle
# Save trades # Save trades
try: try:
@ -349,7 +319,7 @@ class TradingBotV06:
'portfolio_value': round(portfolio_val, 2), 'portfolio_value': round(portfolio_val, 2),
'max_trade_usdt': round(self.max_trade_usdt, 2), 'max_trade_usdt': round(self.max_trade_usdt, 2),
'timestamp': datetime.now().isoformat(), 'timestamp': datetime.now().isoformat(),
'version': 'v0.6-contrarian-mean-reversion' 'version': 'v0.7-coin-level-contrarian'
}, f) }, f)
os.replace(temp, '/home/marc/bot-deploy/active_trades.json') os.replace(temp, '/home/marc/bot-deploy/active_trades.json')
except: except:
@ -358,18 +328,18 @@ class TradingBotV06:
# Save P&L # Save P&L
self.save_pnl_to_db(portfolio_val, usdt_free) self.save_pnl_to_db(portfolio_val, usdt_free)
logger.info(f"[CYCLE-END] Trades={len(self.active_trades)} | Portfolio={portfolio_val:.2f} [v0.6]") logger.info(f"[CYCLE-END] Trades={len(self.active_trades)} | Portfolio={portfolio_val:.2f} [v0.7]")
logger.info("="*70) logger.info("="*70)
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
bot = TradingBotV06() bot = TradingBotV07()
if len(sys.argv) > 1 and sys.argv[1] == '--once': if len(sys.argv) > 1 and sys.argv[1] == '--once':
bot.run_cycle() bot.run_cycle()
else: else:
logger.info("[v0.6 START] Trading Bot with Contrarian Buy/Sell (Mean Reversion)...") logger.info("[v0.7 START] Trading Bot with Coin-Level Contrarian (9 Coins)...")
while True: while True:
try: try:
bot.run_cycle() bot.run_cycle()