← back to Demo Longshot Bot

executor.py

88 lines

"""
executor.py — the money boundary.

Two executors, one interface (open_position / close_position):

  PaperExecutor  — default. Writes simulated fills to the ledger. No key, $0,
                   no real order ever leaves the machine.

  LiveExecutor   — GATED. Refuses to place a real order unless ALL of:
                     1. env LIVE=1
                     2. env SAFE_MODE=0            (safe_mode honored — see below)
                     3. a confirmation token file exists (data/LIVE_CONFIRMED)
                     4. a real broker key is present
                   AND — even then — this template does not ship a wired broker
                   call. Going live is a Steve-only step: draft a go-live memo to
                   ~/.claude/yolo-queue/pending-approval/, implement the broker
                   call, then remove this guard. This is deliberate: a scaffold
                   should never be one typo away from spending real money.

Why safe_mode matters: an earlier autoTrader in the Ken/Kalshi dashboard once
placed orders while safe_mode was on. Here safe_mode is a hard precondition, not
a suggestion — LiveExecutor treats SAFE_MODE!=0 as "do not trade".
"""

import os

import ledger


class PaperExecutor:
    mode = "paper"

    def open_position(self, market, side, size_usd, reason):
        return ledger.record_fill(
            market["id"], market["title"], side, "open",
            market["price"], size_usd, reason, self.mode,
        )

    def close_position(self, position, mark_price, reason):
        return ledger.record_fill(
            position["market_id"], position["title"], position["side"], "close",
            mark_price, position["size_usd"], reason, self.mode,
        )


class LiveExecutor:
    mode = "live"

    def __init__(self, venue):
        self.venue = venue
        self._assert_gates()

    @staticmethod
    def _assert_gates():
        problems = []
        if os.environ.get("LIVE") != "1":
            problems.append("LIVE!=1")
        if os.environ.get("SAFE_MODE", "1") != "0":
            problems.append("SAFE_MODE!=0 (safe_mode is ON — refusing to trade)")
        token = os.path.join(os.path.dirname(__file__), "data", "LIVE_CONFIRMED")
        if not os.path.exists(token):
            problems.append("missing data/LIVE_CONFIRMED confirmation token")
        if not (os.environ.get("POLYMARKET_API_KEY") or os.environ.get("KALSHI_API_KEY")):
            problems.append("no broker API key in env")
        if problems:
            raise PermissionError(
                "LIVE trading blocked — unmet safety gates: " + "; ".join(problems)
                + ".\nGoing live is Steve-gated: draft a go-live memo to "
                "~/.claude/yolo-queue/pending-approval/, wire the broker call, "
                "and clear these gates deliberately. See references/safety-model.md."
            )

    def open_position(self, market, side, size_usd, reason):
        raise NotImplementedError(
            "Live broker order not wired in the template — implement the venue "
            "order call here, then get Steve's approval. Gates passed, but the "
            "money path is intentionally left unimplemented."
        )

    def close_position(self, position, mark_price, reason):
        raise NotImplementedError("Live broker close not wired — see open_position.")


def get_executor(mode, venue):
    if mode == "live":
        return LiveExecutor(venue)
    return PaperExecutor()