# dry_run_simulator.py # Virtual trade simulator with full capital management mirroring real trading rules. # - Uses Polymarket official data for settlement (market close or current prices). # - Unknown actions (e.g. buy_legacy) are force‑closed as loss after 7 days. # - Any open position older than 7 days that couldn't be fetched is also force‑closed. # All comments in English. import os, json, time, threading, requests from datetime import datetime, timezone, timedelta from capital_manager import update_after_trade, load_state as load_capital_state, save_state as save_capital_state, TIERS SIMULATE_FLAG_FILE = "/app/dry_run_active.txt" VIRTUAL_POSITIONS_FILE = "/app/virtual_positions.json" LAST_PROCESSED_SIGNAL_FILE = "/app/last_processed_signal.json" VIRTUAL_CAPITAL_STATE_FILE = "/app/virtual_capital_state.json" DATA_API = "https://data-api.polymarket.com" PSEUDO_SETTLE_DAYS = 3 # normal positions are force‑settled after this many days STUCK_POSITION_DAYS = 7 # any position still open after this is force‑closed as loss virtual_positions = [] positions_lock = threading.Lock() def load_positions(): global virtual_positions try: if os.path.exists(VIRTUAL_POSITIONS_FILE): with open(VIRTUAL_POSITIONS_FILE, 'r') as f: virtual_positions = json.load(f) except: virtual_positions = [] def save_positions(): with positions_lock: try: with open(VIRTUAL_POSITIONS_FILE, 'w') as f: json.dump(virtual_positions, f, indent=2) except Exception as e: print(f"[DryRun] Failed to save positions: {e}", flush=True) def is_simulate_active(): return os.path.exists(SIMULATE_FLAG_FILE) def read_last_signal(): try: if os.path.exists(LAST_PROCESSED_SIGNAL_FILE): with open(LAST_PROCESSED_SIGNAL_FILE, 'r') as f: return json.load(f) except: pass return None def get_virtual_capital_state(): if os.path.exists(VIRTUAL_CAPITAL_STATE_FILE): return load_capital_state(filepath=VIRTUAL_CAPITAL_STATE_FILE) state = { "tier_index": 0, "consecutive_wins": 0, "consecutive_losses": 0, "risk_pool": 5.0, "total_capital": 20.0, "max_bet": 1.0, "reinvest_rate": 0.25, "halted": False, "original_deposit": 20.0, "deposit_date": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") } save_capital_state(state, filepath=VIRTUAL_CAPITAL_STATE_FILE) return state def get_virtual_max_bet(): state = get_virtual_capital_state() return state.get("max_bet", 1.0) def get_virtual_open_count(): with positions_lock: return sum(1 for p in virtual_positions if p.get("status") == "open") def get_virtual_risk_pool(): state = get_virtual_capital_state() return state.get("risk_pool", 5.0) def create_position(market, action, confidence, entry_price, signal_summary, strategy="copy-trade"): max_bet = get_virtual_max_bet() open_count = get_virtual_open_count() risk_pool = get_virtual_risk_pool() if max_bet > 0 and (open_count + 1) * max_bet > risk_pool: print(f"[DryRun] Virtual position limit reached (risk pool ${risk_pool:.2f}). Skipping.", flush=True) return None position = { "id": f"vp-{int(time.time())}-{market[:20]}", "market": market, "action": action, "confidence": confidence, "entry_time": datetime.now(timezone.utc).isoformat(), "entry_price": entry_price, "bet_amount": round(max_bet, 2), "risk_pool_at_entry": round(risk_pool, 2), "tier_at_entry": TIERS[get_virtual_capital_state()["tier_index"]][2], "status": "open", "exit_time": None, "exit_price": None, "pnl": None, "result": None, "exit_type": None, "strategy": strategy, "signal_summary": signal_summary } with positions_lock: virtual_positions.append(position) save_positions() print(f"[DryRun] Virtual position opened: {market} | {action} | bet ${max_bet:.2f} | tier {position['tier_at_entry']}", flush=True) return position def fetch_current_market_info(market_slug): try: resp = requests.get(f"{DATA_API}/markets", params={"slug": market_slug}, timeout=10) if resp.status_code != 200: return None, None, None data = resp.json() if not isinstance(data, list) or not data: return None, None, None market_info = data[0] outcome_prices = market_info.get("outcomePrices", []) if len(outcome_prices) < 2: return None, None, None yes_price = float(outcome_prices[0]) no_price = float(outcome_prices[1]) closed = market_info.get("closed", False) return yes_price, no_price, closed except Exception as e: print(f"[DryRun] Failed to fetch market {market_slug}: {e}", flush=True) return None, None, None def settle_position(pos, exit_type, exit_price, result): pos["status"] = "closed" pos["exit_time"] = datetime.now(timezone.utc).isoformat() pos["exit_type"] = exit_type pos["exit_price"] = exit_price pos["result"] = result entry_price = pos["entry_price"] bet = pos["bet_amount"] if result == "win": pnl = bet * ((1.0 - entry_price) / entry_price) if entry_price > 0 else bet elif result == "loss": pnl = -bet else: pnl = 0.0 pos["pnl"] = round(pnl, 2) update_after_trade(won=(result == "win"), profit_amount=pnl, filepath=VIRTUAL_CAPITAL_STATE_FILE) feed_to_wiki(pos) def check_settlements(): updated = False with positions_lock: for pos in virtual_positions: if pos.get("status") != "open": continue market = pos["market"] yes_price, no_price, closed = fetch_current_market_info(market) if closed is None: continue if not closed: continue winner = "Yes" if yes_price == 1.0 else "No" action = pos["action"].lower() if "buy_yes" in action: won = (winner == "Yes") elif "sell_yes" in action: won = (winner != "Yes") elif "buy_no" in action: won = (winner == "No") elif "sell_no" in action: won = (winner != "No") else: won = False settle_position(pos, exit_type="market", exit_price=1.0 if won else 0.0, result="win" if won else "loss") updated = True print(f"[DryRun] Market settled: {market} → {'WIN' if won else 'LOSS'} | P&L={pos['pnl']}", flush=True) if updated: save_positions() def pseudo_settle_timeouts(): now = datetime.now(timezone.utc) cutoff = now - timedelta(days=PSEUDO_SETTLE_DAYS) updated = False with positions_lock: for pos in virtual_positions: if pos.get("status") != "open": continue try: entry_time = datetime.fromisoformat(pos["entry_time"]) if entry_time > cutoff: continue except: continue market = pos["market"] yes_price, no_price, closed = fetch_current_market_info(market) if yes_price is None: continue action = pos["action"].lower() if "buy_yes" in action: won = yes_price > pos["entry_price"] current = yes_price elif "sell_yes" in action: won = yes_price < pos["entry_price"] current = yes_price elif "buy_no" in action: won = no_price > pos["entry_price"] current = no_price elif "sell_no" in action: won = no_price < pos["entry_price"] current = no_price else: won = False current = 0.0 settle_position(pos, exit_type="timeout", exit_price=current, result="win" if won else "loss") updated = True print(f"[DryRun] Timeout settled: {market} after {PSEUDO_SETTLE_DAYS}d → {'WIN' if won else 'LOSS'}", flush=True) if updated: save_positions() def close_stuck_positions(): """Force‑close any open position older than STUCK_POSITION_DAYS. This catches both unknown actions and positions where market fetch failed.""" now = datetime.now(timezone.utc) cutoff = now - timedelta(days=STUCK_POSITION_DAYS) updated = False with positions_lock: for pos in virtual_positions: if pos.get("status") != "open": continue try: entry_time = datetime.fromisoformat(pos["entry_time"]) if entry_time > cutoff: continue except: continue # Force close as loss settle_position(pos, exit_type="stuck", exit_price=0.0, result="loss") updated = True print(f"[DryRun] Stuck position closed: {pos['market']} ({pos.get('action','')}) → LOSS", flush=True) if updated: save_positions() def feed_to_wiki(pos): try: from llm_wiki import save_entry content = ( f"**Virtual Trade Summary**\n\n" f"- Market: {pos['market']}\n" f"- Action: {pos['action']}\n" f"- Confidence: {pos['confidence']}\n" f"- Bet Amount: ${pos['bet_amount']:.2f}\n" f"- Entry Time: {pos['entry_time']}\n" f"- Exit Price: {pos['exit_price']}\n" f"- Result: {pos.get('result', 'unknown')}\n" f"- P&L: {pos.get('pnl', 'N/A')}\n" f"- Exit Type: {pos.get('exit_type', 'unknown')}\n" ) save_entry(topic=f"VirtualTrade-{pos['market']}", content=content, tags=["virtual-trade", pos.get("result", "unknown")]) except Exception as e: print(f"[DryRun] Wiki feed failed: {e}", flush=True) def capture_signals(): signal = read_last_signal() if not signal: return decision = signal.get("decision", "") if "FOLLOW" not in decision.upper(): return market = signal.get("market_slug", "") with positions_lock: for pos in virtual_positions: if pos.get("market") == market and pos.get("status") == "open": return create_position( market=market, action=signal.get("action", ""), confidence=signal.get("confidence", 0), entry_price=signal.get("entry_price", 0.5), signal_summary=decision[:100], strategy="copy-trade" ) def get_statistics(): with positions_lock: total = len(virtual_positions) open_positions = [p for p in virtual_positions if p.get("status") == "open"] closed_positions = [p for p in virtual_positions if p.get("status") == "closed"] wins = [p for p in closed_positions if p.get("result") == "win"] losses = [p for p in closed_positions if p.get("result") == "loss"] total_pnl = sum(p.get("pnl", 0) or 0 for p in closed_positions) win_rate = f"{(len(wins)/len(closed_positions)*100):.1f}%" if closed_positions else "N/A" state = get_virtual_capital_state() return { "active": is_simulate_active(), "total": total, "open": len(open_positions), "closed": len(closed_positions), "wins": len(wins), "losses": len(losses), "win_rate": win_rate, "total_pnl": round(total_pnl, 2), "virtual_risk_pool": round(state.get("risk_pool", 5.0), 2), "virtual_total_capital": round(state.get("total_capital", 20.0), 2), "virtual_tier": TIERS[state.get("tier_index", 0)][2] } def run(): load_positions() print(f"[DryRun] Simulator started (capital-aware, pseudo‑settle after {PSEUDO_SETTLE_DAYS}d, stuck close after {STUCK_POSITION_DAYS}d).", flush=True) last_capture = 0 last_settlement = 0 last_pseudo = 0 last_stuck = 0 while True: active = is_simulate_active() now = time.time() if active and now - last_capture >= 60: capture_signals() last_capture = now if now - last_settlement >= 600: check_settlements() last_settlement = now if now - last_pseudo >= 3600: pseudo_settle_timeouts() last_pseudo = now if now - last_stuck >= 3600: close_stuck_positions() last_stuck = now time.sleep(30) if __name__ == "__main__": run()