Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion bot_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys
import json
from loguru import logger
from database import get_db_session, Position, init_db
from database import get_db_session, Position, TradeHistory, init_db
from sqlalchemy import select
from core.exchange_client import ExchangeClient
from core.executor import TradeExecutor
Expand All @@ -18,6 +18,77 @@
logger.error("config.json not found! Using defaults.")
CONFIG = {"strategy": {}, "system": {}}

class MoonBagBot:
def __init__(self):
self.exchange = ExchangeClient()
strategy = CONFIG.get("strategy", {})
self.target_multiplier = strategy.get("moonbag_target_multiplier", 2.0)
self.initial_sell_percentage = strategy.get("initial_sell_percentage", 0.5)
self.trailing_stop_percentage = strategy.get("trailing_stop_percentage", 0.05)

async def sync_wallet(self, db):
"""
Sync wallet balances into tracked positions.
"""
balances = await self.exchange.fetch_balance()
for symbol, amount in balances.items():
stmt = select(Position).where(Position.symbol == symbol)
result = await db.execute(stmt)
pos = result.scalar_one_or_none()

if not pos:
price = await self.exchange.get_current_price(symbol)
pos = Position(
symbol=symbol,
entry_price=price,
current_amount=amount,
status="active",
highest_price_seen=price,
)
db.add(pos)
logger.info(f"New Position Tracked: {symbol} @ {price}")
else:
if pos.current_amount != amount:
pos.current_amount = amount

await db.commit()

async def check_strategy(self, db):
"""
Apply the MoonBag strategy to active positions.
"""
stmt = select(Position).where(Position.status == "active")
result = await db.execute(stmt)
positions = result.scalars().all()

for pos in positions:
current_price = await self.exchange.get_current_price(pos.symbol)
target_price = pos.entry_price * self.target_multiplier

if current_price >= target_price:
amount_to_sell = pos.current_amount * self.initial_sell_percentage
await self.exchange.execute_sell_order(pos.symbol, amount_to_sell)

trade = TradeHistory(
symbol=pos.symbol,
amount=amount_to_sell,
price=current_price,
type="sell",
)
db.add(trade)

pos.status = "moonbag_secured"
pos.is_initial_investment_recovered = True
pos.current_amount -= amount_to_sell
pos.trailing_stop_price = current_price * (1 - self.trailing_stop_percentage)
if pos.highest_price_seen is None or current_price > pos.highest_price_seen:
pos.highest_price_seen = current_price

logger.success(f"MoonBag Secured for {pos.symbol}. Remaining: {pos.current_amount}")

await db.commit()


class PredatorBot:
def __init__(self):
self.running = False
Expand Down
96 changes: 62 additions & 34 deletions core/exchange_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def __init__(self, exchange_id: str = 'binance'):
self.exchange_id = exchange_id
self.api_key = os.getenv("API_KEY")
self.api_secret = os.getenv("API_SECRET")
self.quote_currency = os.getenv("QUOTE_CURRENCY", "USDT")
self.client = getattr(ccxt, exchange_id)({
'apiKey': self.api_key,
'secret': self.api_secret,
Expand All @@ -22,6 +23,12 @@ def __init__(self, exchange_id: str = 'binance'):
})
self.logger = logging.getLogger("ExchangeClient")

def normalize_symbol(self, symbol: str) -> str:
"""
Normalize symbols to include a quote currency when missing.
"""
return f"{symbol}/{self.quote_currency}" if '/' not in symbol else symbol

async def fetch_tickers(self, symbols: List[str]) -> Dict[str, float]:
"""
Fetch multiple tickers at once if supported, or parallelize.
Expand All @@ -30,39 +37,39 @@ async def fetch_tickers(self, symbols: List[str]) -> Dict[str, float]:
if not symbols:
return {}

try:
# CCXT fetch_tickers support varies.
# If supported, it's 1 call.
# We assume most major exchanges support it.
# Convert 'BTC' to 'BTC/USDT' roughly for crypto
pairs = [f"{s}/USDT" if '/' not in s else s for s in symbols]

# Check capabilities
if self.client.has['fetchTickers']:
# CCXT fetch_tickers support varies. If supported, it's 1 call.
pairs = [self.normalize_symbol(s) for s in symbols]
results = {}

if self.client.has.get('fetchTickers'):
try:
tickers = await self.client.fetch_tickers(pairs)
# Map back to simple symbol if needed or just return last price
results = {}
for pair, data in tickers.items():
if not isinstance(data, dict):
continue
last = data.get('last') or data.get('close')
if last is None:
continue
# extract 'BTC' from 'BTC/USDT'
sym = pair.split('/')[0] if '/' in pair else pair
results[sym] = data['last']
return results
else:
# Fallback to parallel fetch
# Semaphore to avoid rate limits
results = {}
# TODO: Implement parallel fetch with semaphore
# For now sequential fallback logic (simplified)
for s in symbols:
results[s] = await self.get_current_price(s)
return results
results[sym] = last
if results:
return results
except Exception as e:
self.logger.error(f"Batch Tick Error: {e}")

except Exception as e:
self.logger.error(f"Batch Tick Error: {e}")
return {}
# Fallback to sequential fetch if batch is unsupported or empty.
for s in symbols:
try:
results[s] = await self.get_current_price(s)
except Exception as e:
self.logger.error(f"Tick Error {s}: {e}")

return results

async def get_exchange_id(self):
return self.client.idclose()
return self.client.id

async def close(self):
await self.client.close()
Expand All @@ -74,13 +81,34 @@ async def fetch_balance(self) -> Dict[str, float]:
"""
try:
balance = await self.client.fetch_balance()
# Filter for non-zero free balances, exclude USDT/USD usually if not trading against it
# But here we just want non-zero assets
non_zero = {
k: v['free']
for k, v in balance.items()
if v['free'] > 0 and k not in ['USDT', 'USD', 'USDC'] # simplified exclusion
}
# Filter for non-zero free balances, exclude stablecoins.
# CCXT returns both aggregate keys and per-currency dicts.
excluded = {"USDT", "USD", "USDC"}
non_zero: Dict[str, float] = {}

for symbol, data in balance.items():
if symbol in {"info", "free", "used", "total"}:
continue
if not isinstance(data, dict):
continue
free = data.get("free")
try:
free_amount = float(free)
except (TypeError, ValueError):
continue
if free_amount > 0 and symbol not in excluded:
non_zero[symbol] = free_amount

# Fallback to the aggregate free dict if currency entries are missing.
if not non_zero and isinstance(balance.get("free"), dict):
for symbol, free in balance["free"].items():
try:
free_amount = float(free)
except (TypeError, ValueError):
continue
if free_amount > 0 and symbol not in excluded:
non_zero[symbol] = free_amount

return non_zero
except Exception as e:
self.logger.error(f"Error fetching balance: {e}")
Expand All @@ -90,7 +118,7 @@ async def get_current_price(self, symbol: str) -> float:
"""
Get current price for a symbol. Assumes USDT pair if no slash.
"""
pair = f"{symbol}/USDT" if '/' not in symbol else symbol
pair = self.normalize_symbol(symbol)
try:
ticker = await self.client.fetch_ticker(pair)
return ticker['last']
Expand All @@ -103,7 +131,7 @@ async def execute_sell_order(self, symbol: str, amount: float):
"""
Market sell 50% (or specified amount).
"""
pair = f"{symbol}/USDT"
pair = self.normalize_symbol(symbol)
try:
# Fetch market structure to check min notional if possible, but keeping simple for MVP
self.logger.info(f"EXECUTING SELL: {symbol} amount={amount}")
Expand Down
61 changes: 46 additions & 15 deletions core/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,17 @@
class TradeExecutor:
def __init__(self, exchange_client: ExchangeClient, config: dict):
self.exchange = exchange_client
self.slippage_tolerance = config['strategy']['slippage_tolerance_pct']
self.max_chunks = config['strategy']['max_slippage_retry_chunks']
strategy = config.get('strategy', {})
try:
self.slippage_tolerance = float(strategy.get('slippage_tolerance_pct', 0.005))
except (TypeError, ValueError):
self.slippage_tolerance = 0.005

try:
max_chunks = int(strategy.get('max_slippage_retry_chunks', 3))
except (TypeError, ValueError):
max_chunks = 3
self.max_chunks = max(1, max_chunks)

async def calculate_breakeven_entry(self, symbol: str, execution_price: float, side: str = 'buy') -> float:
"""
Expand All @@ -26,11 +35,16 @@ async def calculate_breakeven_entry(self, symbol: str, execution_price: float, s
# most exchanges ~0.1%
fee_rate = 0.001
try:
# Attempt to get actual markets if loaded
if self.exchange.client.markets:
market = self.exchange.client.markets.get(symbol)
if market and 'taker' in market:
fee_rate = market['taker']
# Attempt to get actual markets if loaded
markets = self.exchange.client.markets
if markets:
market = markets.get(symbol)
if not market:
market = markets.get(self.exchange.normalize_symbol(symbol))
if market:
taker = market.get('taker')
if taker is not None:
fee_rate = taker
except Exception:
pass

Expand All @@ -45,8 +59,10 @@ async def get_liquidity_adjusted_price(self, symbol: str, amount: float, side: s
"""
# Fetch top of book - usually enough for small retail.
# For "Predator", we fetch depth.
ob = await self.exchange.client.fetch_order_book(symbol, limit=20)
orders = ob['bids'] if side == 'sell' else ob['asks']
pair = self.exchange.normalize_symbol(symbol)
ob = await self.exchange.client.fetch_order_book(pair, limit=20)
orders = ob.get('bids') if side == 'sell' else ob.get('asks')
orders = orders or []

filled = 0.0
weighted_sum = 0.0
Expand All @@ -59,25 +75,40 @@ async def get_liquidity_adjusted_price(self, symbol: str, amount: float, side: s
if filled >= amount:
break

if filled == 0:
return await self.exchange.get_current_price(symbol)
if filled < amount:
# Not enough liquidity in top 20!
# Fallback to last price or risky.
return orders[-1][0]
# Not enough liquidity in top 20; use average of available depth.
return weighted_sum / filled

return weighted_sum / amount

async def execute_smart_sell(self, symbol: str, total_amount: float) -> List[Dict]:
"""
Liquidity-aware sell. Splits order if slippage is too high.
"""
if total_amount <= 0:
logger.warning(f"Executor: Skipping sell for {symbol} amount={total_amount}")
return []
logger.info(f"Executor: Analyzing sell for {total_amount} {symbol}...")

ticker = await self.exchange.client.fetch_ticker(symbol)
last_price = ticker['last']
pair = self.exchange.normalize_symbol(symbol)
ticker = await self.exchange.client.fetch_ticker(pair)
last_price = ticker.get('last')
if not last_price or last_price <= 0:
last_price = await self.exchange.get_current_price(symbol)
if not last_price or last_price <= 0:
logger.warning(f"Executor: Missing last price for {symbol}, executing without slippage check.")
order = await self.exchange.execute_sell_order(symbol, total_amount)
return [order]

# Check impact
avg_price = await self.get_liquidity_adjusted_price(symbol, total_amount, 'sell')
slippage = (last_price - avg_price) / last_price
if not avg_price or avg_price <= 0:
logger.warning(f"Executor: Missing liquidity price for {symbol}, executing without slippage check.")
order = await self.exchange.execute_sell_order(symbol, total_amount)
return [order]
slippage = max(0.0, (last_price - avg_price) / last_price)

logger.info(f"Executor: Est. Slippage for {symbol}: {slippage:.4%}")

Expand Down
13 changes: 10 additions & 3 deletions core/strategist.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
2. Volatility Guard: Prevent selling into fakeout wicks.
3. Hedged Trailing Stop: Protect the remaining 50% 'free roll'.
"""
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from loguru import logger
from database import Position
from core.executor import TradeExecutor
Expand All @@ -16,7 +16,14 @@
class Strategist:
def __init__(self, executor: TradeExecutor, config: dict):
self.executor = executor
self.config = config['strategy']
defaults = {
"moonbag_target_multiplier": 2.0,
"initial_sell_percentage": 0.5,
"trailing_stop_percentage": 0.05,
"volatility_guard_window_seconds": 5,
}
strategy = config.get('strategy', {})
self.config = {**defaults, **strategy}
self.target_multiplier = self.config['moonbag_target_multiplier']
self.trailing_stop_pct = self.config['trailing_stop_percentage']
self.volatility_window = self.config['volatility_guard_window_seconds']
Expand Down Expand Up @@ -88,7 +95,7 @@ def _check_volatility_pass(self, symbol: str, price: float) -> bool:
"""
Returns True if price has been stable/high for X seconds.
"""
now = datetime.utcnow()
now = datetime.now(timezone.utc)
if symbol not in self.pump_candidates:
self.pump_candidates[symbol] = (now, price)
logger.info(f"Volatility Guard: {symbol} hit target. Waiting {self.volatility_window}s...")
Expand Down
Loading