| |
| |
| |
| |
| |
| |
|
|
| 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 |
| STUCK_POSITION_DAYS = 7 |
|
|
| 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 |
| |
| 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() |