Logging and trade journaling

Two related but distinct things: system logs (for debugging what your code did) and a trade journal (for understanding what your strategy did and why, over time). Both are unglamorous and both are non-negotiable for anyone serious about this.

Structured system logging

import logging, json

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
    handlers=[
        logging.FileHandler("logs/bot.log"),
        logging.StreamHandler(),
    ],
)

def log_event(event_type: str, **fields):
    logging.info(json.dumps({"event": event_type, **fields}))
log_event("order_placed", order_id=order_id, symbol="INFY", side="BUY", qty=10, tag="orb-nifty")
log_event("order_rejected", order_id=order_id, reason=status_message)
log_event("daily_loss_limit_breached", loss_pct=3.2, threshold=3.0)

Structured (JSON-per-line) logs are trivially greppable/parseable later — when debugging why a specific trade happened at 2 PM three weeks ago, you want to filter by event and symbol, not read prose log lines.

The trade journal — a different artifact, for a different purpose

class TradeJournal:
    def __init__(self, db_path="data/journal.db"):
        self.conn = sqlite3.connect(db_path)
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS trades (
                trade_id TEXT PRIMARY KEY, strategy TEXT, symbol TEXT,
                entry_time TEXT, entry_price REAL, exit_time TEXT, exit_price REAL,
                qty INTEGER, pnl REAL, signal_reason TEXT, notes TEXT
            )
        """)

    def record_entry(self, trade_id, strategy, symbol, entry_price, qty, signal_reason):
        self.conn.execute(
            "INSERT INTO trades (trade_id, strategy, symbol, entry_time, entry_price, qty, signal_reason) VALUES (?,?,?,?,?,?,?)",
            (trade_id, strategy, symbol, datetime.now().isoformat(), entry_price, qty, signal_reason),
        )
        self.conn.commit()

    def record_exit(self, trade_id, exit_price, pnl, notes=""):
        self.conn.execute(
            "UPDATE trades SET exit_time=?, exit_price=?, pnl=?, notes=? WHERE trade_id=?",
            (datetime.now().isoformat(), exit_price, pnl, notes, trade_id),
        )
        self.conn.commit()

Why signal_reason matters more than most fields here

Recording *why* the strategy entered (which specific condition fired, what the indicator values were at the time) turns your journal into a dataset you can later query: "show me every trade where the ATR was above X" or "how did this strategy perform specifically during the choppy period in March." Without this, three months from now you have a list of P&L numbers with no way to diagnose *why* performance changed.

The discipline this enables: periodic honest review

def monthly_review(journal: TradeJournal, strategy: str) -> dict:
    df = pd.read_sql(f"SELECT * FROM trades WHERE strategy='{strategy}'", journal.conn)
    return {
        "total_trades": len(df),
        "win_rate": (df["pnl"] > 0).mean(),
        "avg_win": df[df["pnl"] > 0]["pnl"].mean(),
        "avg_loss": df[df["pnl"] < 0]["pnl"].mean(),
        "max_drawdown": compute_max_drawdown(df["pnl"].cumsum()),
    }

This is the raw material for chapter 98's psychology discipline — you can't make a level-headed decision about whether a strategy is underperforming-but-fine versus actually broken without a journal to look back at.

Next: 090 — Building a kill switch