← Back
β˜†
"""
OF-Trader β€” Main Entry Point
============================
Chunk 1: signal reader + dry-run loop.
Polls the screener's signal_log for new Order-Flow Imbalance signals and logs
the trade it WOULD open. No exchange calls yet (added in Chunk 2).
"""

import asyncio
import logging
import os
import sys
from datetime import datetime
from zoneinfo import ZoneInfo

VANCOUVER = ZoneInfo("America/Vancouver")

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger("main")

from src.signal_reader import SignalReader
from src.risk_gate import RiskGate
from src.of_manager import OFManager
from src.tg_bot import TelegramBot
from src.trade_log import TradeLog
from src.config import (
    DRY_RUN, TG_NOTIFY_DRY, POLL_INTERVAL_SEC, SCREENER_DB_PATH, BINANCE_TESTNET,
    OF_TP_PCT, OF_SL_PCT, OF_HOLD_MAX_MIN,
    OF_MIN_CONFIDENCE, OF_MIN_IMBALANCE,
)


class OFTrader:
    def __init__(self):
        self.reader = SignalReader()
        self.risk = RiskGate()
        # Exchange (and its binance deps / API keys) only constructed for live trading.
        self.exchange = None
        if not DRY_RUN:
            from src.exchange import Exchange
            self.exchange = Exchange()
        self.manager = OFManager(self.exchange, self.risk)
        self.telegram = TelegramBot(self)
        self.journal = TradeLog()
        self.running = False
        self._day = datetime.now(VANCOUVER).date()   # for midnight daily-loss reset

    async def start(self):
        logger.info("=" * 56)
        logger.info("  OF-TRADER starting (Chunk 3: telegram + journal)")
        logger.info("=" * 56)
        mode = "DRY-RUN" if DRY_RUN else "LIVE"
        net = "testnet" if BINANCE_TESTNET else "mainnet"
        logger.info(f"{mode} ({net}) | DB={SCREENER_DB_PATH}")

        # Clean slate on startup: OF signals are ephemeral, we never resume old
        # positions. Cancels orphan orders/positions left by a prior process.
        if self.exchange is not None:
            cleaned = self.exchange.nuclear_cleanup()
            logger.info(f"Startup cleanup: cleared {cleaned} symbols")
            orphans = self.journal.close_orphans()
            if orphans:
                logger.info(f"Journal: closed {orphans} orphan rows from prior run")

        self.reader.initialize()
        await self.telegram.start()
        await self.telegram.send_message(
            f"πŸ€– *OF-Trader started* β€” {mode} ({net})\n"
            f"Bracket: TP{OF_TP_PCT}% SL{OF_SL_PCT}% hold{OF_HOLD_MAX_MIN}m\n"
            f"Filters: confβ‰₯{OF_MIN_CONFIDENCE} imbβ‰₯{OF_MIN_IMBALANCE}"
        )
        self.running = True
        await self._loop()

    def _check_daily_reset(self):
        today = datetime.now(VANCOUVER).date()
        if today != self._day:
            self.risk.reset_daily()
            self._day = today
            logger.info("Daily loss counter reset (midnight Vancouver)")

    async def _loop(self):
        while self.running:
            try:
                self._check_daily_reset()
                # open() runs a blocking maker-chase (up to several seconds) β€” run it
                # off the event loop so Telegram stays responsive and tick() isn't delayed.
                for sig in self.reader.poll():
                    await asyncio.to_thread(self.manager.open, sig)
                await asyncio.to_thread(self.manager.tick)
                for ev in self.manager.drain_events():
                    self.journal.record(ev)
                    notify = ev["type"] in ("open", "close") and (not DRY_RUN or TG_NOTIFY_DRY)
                    if notify:
                        await self.telegram.send_event(ev)
                await asyncio.sleep(POLL_INTERVAL_SEC)
            except KeyboardInterrupt:
                self.running = False
                break
            except Exception as e:
                logger.error(f"Loop error: {e}", exc_info=True)
                await asyncio.sleep(5)
        await self.telegram.stop()


def main():
    try:
        asyncio.run(OFTrader().start())
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()

πŸ“œ Git History

03c788cfix(of-trader): close orphan journal rows on startup4 weeks ago
2d5387ffix(of-trader): 6 bug fixes from live review4 weeks ago
a3dbe0ffeat(of-trader): nuclear cleanup on startup (clear orphan positions on restart)4 weeks ago
c0e676dfeat(of-trader): trade journal + notification policy (chunk 3b)4 weeks ago
793f4d9feat(of-trader): telegram notifications + control (chunk 3a)4 weeks ago
c2258e0feat(of-trader): maker chase + bracket + time-stop lifecycle (chunk 2b)4 weeks ago
4480f03feat(of-trader): signal reader + dry-run loop4 weeks ago
120af73chore: archive grid-v3, scaffold of-trader from grid infra4 weeks ago
Show last diff
Loading...