Fix all audit issues: survival check performance, display consistency, atomic config write, credential validation, TP null handling, highest_open preservation, stale docstring

This commit is contained in:
2026-05-27 07:45:25 +01:00
parent 1b505e0b90
commit ea428b5025
5 changed files with 56 additions and 31 deletions
+16 -2
View File
@@ -175,8 +175,13 @@ class TeslaGridBot:
log.info(f"Lowest level: ${s.lowest_level_price:.2f}") log.info(f"Lowest level: ${s.lowest_level_price:.2f}")
log.info(f"GBP/USD: {s.gbpusd:.4f}") log.info(f"GBP/USD: {s.gbpusd:.4f}")
calc = self.calculator calc = self.calculator
spacing = calc.get_spacing_pct(s.equity) # Use survival-gated lookups — same as manage_orders uses
depth = calc.get_queue_depth(s.equity) 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( full_sim = calc._simulate_grid(
s.highest_open_price, spacing, s.gbpusd, s.all_levels() s.highest_open_price, spacing, s.gbpusd, s.all_levels()
) )
@@ -410,6 +415,15 @@ class TeslaGridBot:
self.banner() self.banner()
log.info("Connecting to Capital.com...") 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: try:
self.client.create_session() self.client.create_session()
except Exception as e: except Exception as e:
+18 -10
View File
@@ -409,9 +409,6 @@ class Calibrator:
f"{gap:.0f} at £{daily_rate:.2f}/day)" f"{gap:.0f} at £{daily_rate:.2f}/day)"
) )
# ══════════════════════════════════════
# 3. GRID (survival-gated — spacing first, then depth)
# ══════════════════════════════════════
# ══════════════════════════════════════ # ══════════════════════════════════════
# 3. GRID (frozen until 60% survival achieved) # 3. GRID (frozen until 60% survival achieved)
# Spacing tightens only when full 60% drop simulation passes. # 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): def _update_survival_pct(self, new_pct: float, lines: list):
""" """
Automatically update SURVIVAL_DROP_PCT in config.py. Automatically update SURVIVAL_DROP_PCT in config.py.
Does a safe in-place string replacement so all other Uses atomic write (temp file + rename) to prevent corruption
config values and comments are preserved. if the bot crashes mid-write.
""" """
try: try:
config_path = os.path.join( config_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "config.py" os.path.dirname(os.path.abspath(__file__)), "config.py"
) )
tmp_path = config_path + ".tmp"
with open(config_path, "r") as f: with open(config_path, "r") as f:
content = f.read() content = f.read()
@@ -572,14 +571,15 @@ class Calibrator:
new_line = f"SURVIVAL_DROP_PCT = {new_pct:.1f}" new_line = f"SURVIVAL_DROP_PCT = {new_pct:.1f}"
if old_line not in content: if old_line not in content:
# Try without decimal
old_line = f"SURVIVAL_DROP_PCT = {int(config.SURVIVAL_DROP_PCT)}" old_line = f"SURVIVAL_DROP_PCT = {int(config.SURVIVAL_DROP_PCT)}"
if old_line in content: if old_line in content:
new_content = content.replace(old_line, new_line, 1) 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) 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 config.SURVIVAL_DROP_PCT = new_pct
lines.append( lines.append(
f" ✓ config.py updated: SURVIVAL_DROP_PCT = {new_pct:.1f}" 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}" f"update manually: SURVIVAL_DROP_PCT = {new_pct:.1f}"
) )
except Exception as e: 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" ⚠ Auto-update failed: {e}")
lines.append( lines.append(
f" Update manually in config.py: " f" Update manually in config.py: "
@@ -631,8 +637,10 @@ class Calibrator:
}) })
# Simulate all grid levels that would be placed between # Simulate all grid levels that would be placed between
# current price and the floor # lowest open position (or current price if no positions) and floor
level = s.current_price * (1.0 - spacing) 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: while level >= floor:
size = self.calculator.get_size(level, s.highest_open_price) size = self.calculator.get_size(level, s.highest_open_price)
simulated_levels.append({"price": level, "size": size}) simulated_levels.append({"price": level, "size": size})
+6 -6
View File
@@ -190,11 +190,11 @@ class CapitalClient:
""" """
Update the take profit on an open position. Update the take profit on an open position.
Pass take_profit=None to remove the TP (manual close only). Pass take_profit=None to remove the TP (manual close only).
Used for the bottom-two position rule: When removing TP, omits the key entirely rather than sending null,
- Lowest open position: TP removed (held until manual close) as Capital.com may reject a null profitLevel value.
- Second lowest: TP set to standard level
""" """
body = { if take_profit is not None:
"profitLevel": round(take_profit, 2) if take_profit is not None else 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) return self._request("PUT", f"/positions/{deal_id}", body)
+6 -9
View File
@@ -13,14 +13,11 @@ As equity grows, the grid tightens (more orders = more frequent fills = more pro
SURVIVAL RULE: SURVIVAL RULE:
-------------- --------------
The bot calculates whether the account can survive a X% drop from the highest The bot simulates a full 60% drop from the highest open position to check
open position without being margin called. If not safe, no new orders are placed. whether the account would survive (margin level stays above 50%).
Start at 30% and increase as equity grows: SURVIVAL_DROP_PCT is the operational gate for placing new orders — it
£640 → 30% survival (current) auto-steps (30→35→40→45→50→55→60%) as equity grows, managed automatically
£900 → 35% by the calibration report on every startup. Do not edit it manually.
£1200 → 40%
£1500 → 50%
£1800 → 60% (target)
SIZE RULE: SIZE RULE:
---------- ----------
@@ -98,7 +95,7 @@ QUEUE_DEPTH = 10
# Bot will not place orders if simulated X% drop from highest open position # Bot will not place orders if simulated X% drop from highest open position
# would result in margin closeout. Increase this as equity grows. # would result in margin closeout. Increase this as equity grows.
# Current safe value for £640 equity = 30%. Target = 60%. # 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) # Capital.com retail margin rate for shares CFDs (5:1 leverage = 20% margin)
MARGIN_RATE = 0.20 MARGIN_RATE = 0.20
+9 -3
View File
@@ -106,10 +106,16 @@ class BotState:
) / 2.0 ) / 2.0
# Highest open position entry price # 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] open_prices = [pos_level(p) for p in positions]
self.highest_open_price = ( if open_prices:
max(open_prices) if open_prices else self.current_price 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 # Lowest level across all positions and orders
order_prices = [ord_level(o) for o in orders] order_prices = [ord_level(o) for o in orders]