diff --git a/.env.example b/.env.example index 1b9cb69..6c7f095 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,6 @@ KRAKEN_API_KEY=your_api_key_here KRAKEN_API_SECRET=your_api_secret_here # Starting balance for paper trading simulation +# Needs to be at least (max_positions * min_order_usd) to fill all slots. +# Default config: 5 positions * $15 min = $75 minimum, $500+ recommended. PAPER_BALANCE_USD=1000 diff --git a/bot.py b/bot.py index d54d062..f0557db 100644 --- a/bot.py +++ b/bot.py @@ -45,9 +45,11 @@ def setup_logging(log_file: str) -> None: ) -def available_usd(client: KrakenClient, config: Config) -> float: +def available_usd(client: KrakenClient, config: Config, portfolio: Portfolio) -> float: if config.paper_trading: - return float(os.getenv("PAPER_BALANCE_USD", "1000")) + total = float(os.getenv("PAPER_BALANCE_USD", "1000")) + invested = sum(pos.cost_usd for pos in portfolio.all()) + return max(0.0, total - invested) try: return client.get_usd_balance() except KrakenError as exc: @@ -129,7 +131,7 @@ def run_cycle(config: Config) -> bool: log.info("Portfolio full — no new positions to open.") return bool(closed) - balance = available_usd(client, config) + balance = available_usd(client, config, portfolio) log.info("Available balance: $%.2f | Open slots: %d", balance, open_slots) if balance < config.min_order_usd: @@ -146,22 +148,26 @@ def run_cycle(config: Config) -> bool: log.info("No opportunities matching criteria this cycle.") return bool(closed) - # Cap to available slots and allocate equally - to_buy = opportunities[:open_slots] - alloc_per_asset = balance / len(to_buy) + # Sort by 24h change descending — strongest movers get priority when funds are limited + opportunities.sort(key=lambda o: o.change_pct, reverse=True) + + # How many can we actually afford at the minimum order size? + affordable = int(balance // config.min_order_usd) + num_to_buy = min(open_slots, affordable, len(opportunities)) + + if num_to_buy == 0: + log.info("Balance $%.2f too low for even one minimum order ($%.2f).", balance, config.min_order_usd) + return bool(closed) + + to_buy = opportunities[:num_to_buy] + # Split available balance equally across the chosen assets (always >= min_order_usd) + alloc_per_asset = balance / num_to_buy log.info( "Buying %d asset(s) @ $%.2f each (total $%.2f)", - len(to_buy), alloc_per_asset, alloc_per_asset * len(to_buy), + num_to_buy, alloc_per_asset, alloc_per_asset * num_to_buy, ) for opp in to_buy: - if alloc_per_asset < config.min_order_usd: - log.warning( - "Allocation $%.2f < minimum $%.2f — skipping %s", - alloc_per_asset, config.min_order_usd, opp.pair, - ) - continue - quantity = round(alloc_per_asset / opp.last_price, opp.lot_decimals) if opp.order_min > 0 and quantity < opp.order_min: