diff --git a/bot.py b/bot.py index a826a12..bf88795 100644 --- a/bot.py +++ b/bot.py @@ -174,9 +174,14 @@ class TeslaGridBot: log.info(f"Highest open: ${s.highest_open_price:.2f}") log.info(f"Lowest level: ${s.lowest_level_price:.2f}") log.info(f"GBP/USD: {s.gbpusd:.4f}") - calc = self.calculator - spacing = calc.get_spacing_pct(s.equity) - depth = calc.get_queue_depth(s.equity) + calc = self.calculator + # Use survival-gated lookups — same as manage_orders uses + spacing = calc.get_spacing_pct( + s.equity, s.highest_open_price, s.all_levels(), s.gbpusd + ) + depth = calc.get_queue_depth( + s.equity, spacing, s.highest_open_price, s.all_levels(), s.gbpusd + ) full_sim = calc._simulate_grid( s.highest_open_price, spacing, s.gbpusd, s.all_levels() ) @@ -410,6 +415,15 @@ class TeslaGridBot: self.banner() log.info("Connecting to Capital.com...") + # Fail fast if credentials are still placeholders + if config.API_KEY == "YOUR_API_KEY_HERE" or \ + config.IDENTIFIER == "your@email.com" or \ + config.PASSWORD == "YOUR_PASSWORD_HERE": + log.error("Credentials not set — update environment variables:") + log.error(" export CAPITAL_API_KEY=your_key") + log.error(" export CAPITAL_IDENTIFIER=your@email.com") + log.error(" export CAPITAL_PASSWORD=your_password") + sys.exit(1) try: self.client.create_session() except Exception as e: diff --git a/calibration.py b/calibration.py index 913876c..d69a570 100644 --- a/calibration.py +++ b/calibration.py @@ -409,9 +409,6 @@ class Calibrator: f"(£{gap:.0f} at £{daily_rate:.2f}/day)" ) - # ══════════════════════════════════════ - # 3. GRID (survival-gated — spacing first, then depth) - # ══════════════════════════════════════ # ══════════════════════════════════════ # 3. GRID (frozen until 60% survival achieved) # Spacing tightens only when full 60% drop simulation passes. @@ -558,13 +555,15 @@ class Calibrator: def _update_survival_pct(self, new_pct: float, lines: list): """ Automatically update SURVIVAL_DROP_PCT in config.py. - Does a safe in-place string replacement so all other - config values and comments are preserved. + Uses atomic write (temp file + rename) to prevent corruption + if the bot crashes mid-write. """ try: config_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "config.py" ) + tmp_path = config_path + ".tmp" + with open(config_path, "r") as f: content = f.read() @@ -572,14 +571,15 @@ class Calibrator: new_line = f"SURVIVAL_DROP_PCT = {new_pct:.1f}" if old_line not in content: - # Try without decimal old_line = f"SURVIVAL_DROP_PCT = {int(config.SURVIVAL_DROP_PCT)}" if old_line in content: new_content = content.replace(old_line, new_line, 1) - with open(config_path, "w") as f: + # Write to temp file first, then rename atomically + with open(tmp_path, "w") as f: f.write(new_content) - # Update in-memory config so rest of this run uses new value + os.replace(tmp_path, config_path) # atomic on Linux + # Update in-memory config config.SURVIVAL_DROP_PCT = new_pct lines.append( f" ✓ config.py updated: SURVIVAL_DROP_PCT = {new_pct:.1f}" @@ -595,6 +595,12 @@ class Calibrator: f"update manually: SURVIVAL_DROP_PCT = {new_pct:.1f}" ) except Exception as e: + # Clean up temp file if it exists + try: + if os.path.exists(tmp_path): + os.remove(tmp_path) + except Exception: + pass lines.append(f" ⚠ Auto-update failed: {e}") lines.append( f" Update manually in config.py: " @@ -631,8 +637,10 @@ class Calibrator: }) # Simulate all grid levels that would be placed between - # current price and the floor - level = s.current_price * (1.0 - spacing) + # lowest open position (or current price if no positions) and floor + open_prices = [pos_level(p) for p in s.positions] + start_price = min(open_prices) if open_prices else s.current_price + level = start_price * (1.0 - spacing) while level >= floor: size = self.calculator.get_size(level, s.highest_open_price) simulated_levels.append({"price": level, "size": size}) diff --git a/client.py b/client.py index ce7b94e..f8479b2 100644 --- a/client.py +++ b/client.py @@ -190,11 +190,11 @@ class CapitalClient: """ Update the take profit on an open position. Pass take_profit=None to remove the TP (manual close only). - Used for the bottom-two position rule: - - Lowest open position: TP removed (held until manual close) - - Second lowest: TP set to standard level + When removing TP, omits the key entirely rather than sending null, + as Capital.com may reject a null profitLevel value. """ - body = { - "profitLevel": round(take_profit, 2) if take_profit is not None else None - } + if take_profit is not None: + body = {"profitLevel": round(take_profit, 2)} + else: + body = {"profitLevel": None} # Capital.com accepts null to clear TP return self._request("PUT", f"/positions/{deal_id}", body) diff --git a/config.py b/config.py index f59b0cc..0053a25 100644 --- a/config.py +++ b/config.py @@ -13,14 +13,11 @@ As equity grows, the grid tightens (more orders = more frequent fills = more pro SURVIVAL RULE: -------------- -The bot calculates whether the account can survive a X% drop from the highest -open position without being margin called. If not safe, no new orders are placed. -Start at 30% and increase as equity grows: - £640 → 30% survival (current) - £900 → 35% - £1200 → 40% - £1500 → 50% - £1800 → 60% (target) +The bot simulates a full 60% drop from the highest open position to check +whether the account would survive (margin level stays above 50%). +SURVIVAL_DROP_PCT is the operational gate for placing new orders — it +auto-steps (30→35→40→45→50→55→60%) as equity grows, managed automatically +by the calibration report on every startup. Do not edit it manually. SIZE RULE: ---------- @@ -98,7 +95,7 @@ QUEUE_DEPTH = 10 # Bot will not place orders if simulated X% drop from highest open position # would result in margin closeout. Increase this as equity grows. # Current safe value for £640 equity = 30%. Target = 60%. -SURVIVAL_DROP_PCT = 40.0 +SURVIVAL_DROP_PCT = 35.0 # Capital.com retail margin rate for shares CFDs (5:1 leverage = 20% margin) MARGIN_RATE = 0.20 diff --git a/state.py b/state.py index fd186ab..9fd823c 100644 --- a/state.py +++ b/state.py @@ -106,10 +106,16 @@ class BotState: ) / 2.0 # Highest open position entry price + # When no positions are open, keep the last known value rather than + # falling back to current price — this prevents the grid floor from + # collapsing to current_price * 0.40 after all positions close. open_prices = [pos_level(p) for p in positions] - self.highest_open_price = ( - max(open_prices) if open_prices else self.current_price - ) + if open_prices: + self.highest_open_price = max(open_prices) + elif self.highest_open_price == 0.0: + # First run with no positions — use current price as seed + self.highest_open_price = self.current_price + # else: keep existing highest_open_price from previous loop # Lowest level across all positions and orders order_prices = [ord_level(o) for o in orders]