Skill: witty-quotes25/openclaw-questrade (version 1.0.0)

--- name: open-claw-questrade description: Execute and monitor stock trades for openclaw.ai workflows using Questrade's browser platform with Yahoo Finance cross-checks. Use when Codex is asked to prepare or review Questrade Web order tickets, monitor symbols from Questrade and Yahoo Finance, produce pre-trade or post-trade checklists, reconcile fills, or enforce privacy-safe trading operations where credentials and sensitive account data stay on the user's side only. --- # Open Claw ## Overview Use this skill to run a repeatable browser-trading workflow for Questrade with independent quote checks from Yahoo Finance. Keep execution manual in the broker UI, but automate preparation, validation, and monitoring artifacts. ## Workflow Decision Tree 1. Need a quote/monitoring snapshot across symbols: run `scripts/market_snapshot.py`. 2. Need a trade-ready plan with risk controls: run `scripts/build_trade_checklist.py` with Special Safety Check flags. 3. Need to place a trade in Questrade Web: follow `references/questrade-browser-playbook.md`. 4. Need field-level input/output rules: load `references/data-contracts.md`. 5. Need policy gating details: load `references/openclaw-policy-compliance.md`. ## Standard Execution Flow 1. Confirm trading intent: symbol, side, size, account, session constraints. 2. Pull live Yahoo quotes and optional Questrade export using `scripts/market_snapshot.py`. 3. Reject stale data if snapshot age exceeds user threshold. 4. Run Special Safety Check (mandatory) through `scripts/build_trade_checklist.py`: - `--policy-ack OPENCLAW_POLICY_ACK` - `--confirm-user-authorized` - `--confirm-manual-execution` - `--confirm-no-secrets-shared` - Live mode also enforces risk cap, data freshness, and drift thresholds. 5. Draft or refine ticket with `scripts/build_trade_checklist.py`. 6. Enforce hard checks before submitting in browser: - Confirm side and quantity. - Confirm order type and time-in-force. - Confirm max risk and stop/exit rule. - Confirm buying power and open-order conflicts. 7. Submit manually in Questrade Web and record confirmation ID. 8. Capture post-trade state (fill price, remaining quantity, stop/target status). ## Guardrails - Never claim an order is submitted unless a human confirms broker submission. - Treat Yahoo as secondary market data, not authoritative execution data. - Escalate if broker and Yahoo prices diverge beyond the configured tolerance. - Refuse ambiguous instructions (missing side, quantity, or symbol). - Require explicit user acknowledgement before high-impact actions. - Block checklist generation when safety gate requirements fail. ## Privacy Rules (OpenClaw.ai) - Keep all credentials, MFA tokens, session cookies, API keys, and passwords user-side only. - Never ask the user to paste secrets into chat, files, or logs. - Use masked identifiers in generated artifacts by default (account IDs, order IDs, personal details). - Only include raw sensitive identifiers when the user explicitly asks and confirms local-only usage. - Store outputs locally and avoid sharing raw broker exports unless redacted. ## Outputs Generate one or more of these artifacts per task: - `snapshot.json` or `snapshot.csv` from `scripts/market_snapshot.py`. - `trade_checklist.md` from `scripts/build_trade_checklist.py`. - A concise execution log containing timestamp, symbol, side, quantity, order type, and broker confirmation reference. ## Resource Loading Guide - Load `references/data-contracts.md` when parsing or validating CSV and JSON payloads. - Load `references/questrade-browser-playbook.md` when the user asks for browser execution steps, troubleshooting, or reconciliation. - Load `references/openclaw-policy-compliance.md` when enforcing or explaining policy checks. - Prefer scripts for deterministic output instead of recreating tables/checklists manually.

path: scripts/build_trade_checklist.py
content: #!/usr/bin/env python3
"""
Generate a structured checklist for manually submitting a stock trade
in Questrade Web.
"""

from __future__ import annotations

import argparse
import datetime as dt
import json
import sys
from pathlib import Path
from typing import Optional


VALID_SIDES = {"buy", "sell"}
VALID_ORDER_TYPES = {"market", "limit", "stop", "stop_limit"}
VALID_TIF = {"day", "gtc", "gted"}
VALID_ENVIRONMENTS = {"paper", "live"}
POLICY_ACK_PHRASE = "OPENCLAW_POLICY_ACK"


def validate_positive_int(value: str) -> int:
    number = int(value)
    if number <= 0:
        raise argparse.ArgumentTypeError("Quantity must be greater than zero.")
    return number


def validate_side(value: str) -> str:
    side = value.lower()
    if side not in VALID_SIDES:
        raise argparse.ArgumentTypeError(f"side must be one of: {sorted(VALID_SIDES)}")
    return side


def validate_order_type(value: str) -> str:
    order_type = value.lower()
    if order_type not in VALID_ORDER_TYPES:
        raise argparse.ArgumentTypeError(
            f"order-type must be one of: {sorted(VALID_ORDER_TYPES)}"
        )
    return order_type


def validate_tif(value: str) -> str:
    tif = value.lower()
    if tif not in VALID_TIF:
        raise argparse.ArgumentTypeError(f"tif must be one of: {sorted(VALID_TIF)}")
    return tif


def validate_price(value: str) -> float:
    number = float(value)
    if number <= 0:
        raise argparse.ArgumentTypeError("Price must be greater than zero.")
    return number


def validate_non_negative_float(value: str) -> float:
    number = float(value)
    if number < 0:
        raise argparse.ArgumentTypeError("Value must be zero or greater.")
    return number


def validate_non_negative_int(value: str) -> int:
    number = int(value)
    if number < 0:
        raise argparse.ArgumentTypeError("Value must be zero or greater.")
    return number


def validate_environment(value: str) -> str:
    environment = value.lower()
    if environment not in VALID_ENVIRONMENTS:
        raise argparse.ArgumentTypeError(
            f"environment must be one of: {sorted(VALID_ENVIRONMENTS)}"
        )
    return environment


def mask_identifier(raw_value: str) -> str:
    text = raw_value.strip()
    if len(text) <= 4:
        return "*" * len(text)
    return f"{'*' * (len(text) - 4)}{text[-4:]}"


def enforce_special_safety_checks(
    args: argparse.Namespace, parser: argparse.ArgumentParser
) -> None:
    if args.policy_ack.strip() != POLICY_ACK_PHRASE:
        parser.error(
            "--policy-ack must match OPENCLAW_POLICY_ACK for policy-compliant execution"
        )
    if not args.confirm_user_authorized:
        parser.error("--confirm-user-authorized is required")
    if not args.confirm_manual_execution:
        parser.error("--confirm-manual-execution is required")
    if not args.confirm_no_secrets_shared:
        parser.error("--confirm-no-secrets-shared is required")

    if args.include_sensitive and not args.local_sensitive_storage_confirm:
        parser.error(
            "--local-sensitive-storage-confirm is required with --include-sensitive"
        )

    if args.environment == "live":
        if args.risk_cap_usd is None:
            parser.error("--risk-cap-usd is required for live trading mode")
        if args.data_age_seconds is None:
            parser.error("--data-age-seconds is required for live trading mode")
        if args.data_age_seconds > args.max_data_age_seconds:
            parser.error(
                f"data is stale ({args.data_age_seconds}s > {args.max_data_age_seconds}s)"
            )
        if args.order_type == "market" and not args.allow_market_order_live:
            parser.error(
                "live market orders are blocked by default; pass --allow-market-order-live "
                "to override explicitly"
            )
        if (
            args.observed_price_drift_pct is not None
            and args.observed_price_drift_pct > args.max_price_drift_pct
        ):
            parser.error(
                "observed price drift exceeds configured maximum "
                f"({args.observed_price_drift_pct}% > {args.max_price_drift_pct}%)"
            )


def build_markdown(args: argparse.Namespace) -> str:
    created_at = dt.datetime.now(dt.timezone.utc).isoformat()

    reference_price: Optional[float] = args.limit_price or args.stop_price
    estimated_notional = None
    if reference_price is not None:
        estimated_notional = reference_price * args.quantity

    account_value = args.account_id if args.include_sensitive else mask_identifier(args.account_id)

    payload = {
        "created_at_utc": created_at,
        "account_id": account_value,
        "account_id_masked": mask_identifier(args.account_id),
        "environment": args.environment,
        "special_safety_check": {
            "status": "pass",
            "policy_acknowledged": True,
            "user_authorized": args.confirm_user_authorized,
            "manual_execution_confirmed": args.confirm_manual_execution,
            "no_secrets_shared_confirmed": args.confirm_no_secrets_shared,
            "data_age_seconds": args.data_age_seconds,
            "max_data_age_seconds": args.max_data_age_seconds,
            "observed_price_drift_pct": args.observed_price_drift_pct,
            "max_price_drift_pct": args.max_price_drift_pct,
            "risk_cap_usd": args.risk_cap_usd,
        },
        "symbol": args.symbol.upper(),
        "side": args.side,
        "quantity": args.quantity,
        "order_type": args.order_type,
        "time_in_force": args.tif,
        "limit_price": args.limit_price,
        "stop_price": args.stop_price,
        "estimated_notional": estimated_notional,
        "risk_cap_usd": args.risk_cap_usd,
        "notes": args.notes,
    }

    lines = [
        "# Trade Checklist",
        "",
        "## Ticket",
        f"- Created (UTC): {created_at}",
        f"- Environment: {args.environment.upper()}",
        f"- Account: {account_value}",
        f"- Symbol: {args.symbol.upper()}",
        f"- Side: {args.side.upper()}",
        f"- Quantity: {args.quantity}",
        f"- Order type: {args.order_type}",
        f"- Time in force: {args.tif.upper()}",
        f"- Limit price: {args.limit_price if args.limit_price is not None else 'N/A'}",
        f"- Stop price: {args.stop_price if args.stop_price is not None else 'N/A'}",
        f"- Estimated notional: {round(estimated_notional, 4) if estimated_notional is not None else 'N/A'}",
        f"- Risk cap (USD): {args.risk_cap_usd if args.risk_cap_usd is not None else 'N/A'}",
        "",
        "## Special Safety Check (Policy-Enforced)",
        "- [x] openclaw.ai policy acknowledgement confirmed.",
        "- [x] User authorization to trade this account confirmed.",
        "- [x] Manual broker-side execution confirmed (no autonomous order placement).",
        "- [x] Sensitive credentials were not shared in artifacts.",
        f"- [x] Data age check passed: {args.data_age_seconds if args.data_age_seconds is not None else 'N/A'}s <= {args.max_data_age_seconds}s.",
        f"- [x] Price drift check passed: {args.observed_price_drift_pct if args.observed_price_drift_pct is not None else 'N/A'}% <= {args.max_price_drift_pct}%.",
        "",
        "## Pre-Trade Hard Checks",
        "- [ ] Symbol, side, and quantity match user intent exactly.",
        "- [ ] Buying power and margin impact are acceptable.",
        "- [ ] Open orders/positions were checked for conflicts.",
        "- [ ] Order type and price fields are valid for current market state.",
        "- [ ] Stop or exit plan is explicitly defined.",
        "- [ ] Broker quote was cross-checked with Yahoo snapshot.",
        "",
        "## Questrade Web Submission Steps",
        "- [ ] Open order entry ticket in Questrade Web.",
        "- [ ] Select account and verify symbol.",
        "- [ ] Enter side, quantity, order type, and required prices.",
        "- [ ] Confirm time in force and routing options.",
        "- [ ] Review estimated cost/proceeds and commissions.",
        "- [ ] Submit order and capture confirmation ID.",
        "",
        "## Post-Submission Log",
        "- [ ] Confirmation ID:",
        "- [ ] Submitted timestamp (UTC):",
        "- [ ] Fill status (open/partial/filled/cancelled):",
        "- [ ] Average fill price:",
        "- [ ] Remaining quantity:",
        "",
        "## Machine-Readable Ticket",
        "```json",
        json.dumps(payload, indent=2),
        "```",
    ]

    if args.notes:
        lines.extend(["", "## Notes", args.notes])

    return "\n".join(lines) + "\n"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Generate a markdown checklist for manual Questrade trade execution."
    )
    parser.add_argument("--account-id", required=True, help="Broker account identifier.")
    parser.add_argument("--symbol", required=True, help="Ticker symbol.")
    parser.add_argument("--side", required=True, type=validate_side, help="buy or sell.")
    parser.add_argument(
        "--quantity", required=True, type=validate_positive_int, help="Share quantity."
    )
    parser.add_argument(
        "--order-type",
        required=True,
        type=validate_order_type,
        help="market, limit, stop, or stop_limit.",
    )
    parser.add_argument(
        "--tif",
        default="day",
        type=validate_tif,
        help="Time in force: day, gtc, or gted.",
    )
    parser.add_argument(
        "--limit-price",
        type=validate_price,
        help="Required for limit and stop_limit orders.",
    )
    parser.add_argument(
        "--stop-price",
        type=validate_price,
        help="Required for stop and stop_limit orders.",
    )
    parser.add_argument(
        "--risk-cap-usd",
        type=validate_price,
        help="Optional max accepted risk in USD.",
    )
    parser.add_argument(
        "--environment",
        default="live",
        type=validate_environment,
        help="Execution mode: paper or live. Default: live.",
    )
    parser.add_argument(
        "--policy-ack",
        required=True,
        help="Set to OPENCLAW_POLICY_ACK to confirm policy compliance intent.",
    )
    parser.add_argument(
        "--confirm-user-authorized",
        action="store_true",
        help="Confirm user is authorized to trade the target account.",
    )
    parser.add_argument(
        "--confirm-manual-execution",
        action="store_true",
        help="Confirm trade submission remains manual in Questrade browser.",
    )
    parser.add_argument(
        "--confirm-no-secrets-shared",
        action="store_true",
        help="Confirm secrets were not shared in prompts/files/logs.",
    )
    parser.add_argument(
        "--data-age-seconds",
        type=validate_non_negative_int,
        help="Age of quote data used for this ticket.",
    )
    parser.add_argument(
        "--max-data-age-seconds",
        type=validate_non_negative_int,
        default=300,
        help="Maximum allowed quote data age. Default: 300.",
    )
    parser.add_argument(
        "--observed-price-drift-pct",
        type=validate_non_negative_float,
        help="Observed broker-vs-reference drift percentage.",
    )
    parser.add_argument(
        "--max-price-drift-pct",
        type=validate_non_negative_float,
        default=1.0,
        help="Maximum allowed drift percentage. Default: 1.0.",
    )
    parser.add_argument(
        "--allow-market-order-live",
        action="store_true",
        help="Explicitly allow market order type in live mode.",
    )
    parser.add_argument(
        "--include-sensitive",
        action="store_true",
        help="Include raw account identifier in output. Default behavior masks it.",
    )
    parser.add_argument(
        "--local-sensitive-storage-confirm",
        action="store_true",
        help="Confirm local-only handling when include-sensitive output is requested.",
    )
    parser.add_argument("--notes", default="", help="Optional extra notes.")
    parser.add_argument("--out", required=True, help="Output markdown path.")

    args = parser.parse_args()

    if args.order_type == "limit" and args.limit_price is None:
        parser.error("--limit-price is required for order-type=limit")
    if args.order_type == "stop" and args.stop_price is None:
        parser.error("--stop-price is required for order-type=stop")
    if args.order_type == "stop_limit":
        if args.stop_price is None or args.limit_price is None:
            parser.error(
                "--stop-price and --limit-price are required for order-type=stop_limit"
            )
    enforce_special_safety_checks(args, parser)
    return args


def main() -> int:
    try:
        args = parse_args()
        markdown = build_markdown(args)
        out_path = Path(args.out)
        out_path.parent.mkdir(parents=True, exist_ok=True)
        out_path.write_text(markdown, encoding="utf-8")
        print(f"Wrote checklist to {out_path}")
        return 0
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/market_snapshot.py
content: #!/usr/bin/env python3
"""
Build a monitoring snapshot using Yahoo Finance quotes plus optional
Questrade-exported CSV data.
"""

from __future__ import annotations

import argparse
import csv
import datetime as dt
import json
import math
import sys
import urllib.request
from pathlib import Path
from typing import Dict, Iterable, List, Optional


YAHOO_CHART_URL = "https://query2.finance.yahoo.com/v8/finance/chart/{symbol}"


def parse_symbols(symbols_text: str) -> List[str]:
    symbols = []
    for part in symbols_text.split(","):
        symbol = part.strip().upper()
        if symbol:
            symbols.append(symbol)
    if not symbols:
        raise ValueError("At least one symbol is required.")
    return sorted(set(symbols))


def to_float(value: Optional[str]) -> Optional[float]:
    if value is None:
        return None
    text = str(value).strip()
    if not text:
        return None
    try:
        return float(text.replace(",", ""))
    except ValueError:
        return None


def to_int(value: Optional[str]) -> Optional[int]:
    number = to_float(value)
    if number is None:
        return None
    return int(number)


def find_latest(values: Iterable[Optional[float]]) -> Optional[float]:
    for value in reversed(list(values)):
        if value is not None:
            return float(value)
    return None


def fetch_yahoo_chart_quote(symbol: str) -> Optional[dict]:
    url = YAHOO_CHART_URL.format(symbol=symbol)
    url += "?range=1d&interval=1m&includePrePost=true&events=div,split"
    request = urllib.request.Request(
        url,
        headers={
            "User-Agent": (
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/122.0.0.0 Safari/537.36"
            )
        },
    )
    with urllib.request.urlopen(request, timeout=20) as response:
        payload = json.loads(response.read().decode("utf-8"))

    chart = payload.get("chart", {})
    if chart.get("error"):
        return None
    results = chart.get("result") or []
    if not results:
        return None

    result = results[0]
    meta = result.get("meta", {})
    quote = (result.get("indicators", {}).get("quote") or [{}])[0]
    latest_close = find_latest(quote.get("close") or [])
    latest_volume = find_latest(quote.get("volume") or [])

    market_time = meta.get("regularMarketTime")
    market_ts = None
    if isinstance(market_time, (int, float)):
        market_ts = dt.datetime.fromtimestamp(
            int(market_time), tz=dt.timezone.utc
        ).isoformat()

    market_price = meta.get("regularMarketPrice")
    if market_price is None:
        market_price = latest_close

    market_volume = meta.get("regularMarketVolume")
    if market_volume is None:
        market_volume = latest_volume

    return {
        "symbol": symbol,
        "price": market_price,
        "bid": None,
        "ask": None,
        "volume": market_volume,
        "currency": meta.get("currency"),
        "exchange": meta.get("fullExchangeName") or meta.get("exchangeName"),
        "market_state": "unknown",
        "market_time_utc": market_ts,
    }


def fetch_yahoo_quotes(symbols: Iterable[str]) -> Dict[str, dict]:
    quotes: Dict[str, dict] = {}
    for symbol in symbols:
        try:
            quote = fetch_yahoo_chart_quote(symbol)
            if quote is not None:
                quotes[symbol] = quote
        except Exception as exc:
            print(f"Warning: Yahoo request failed for {symbol}: {exc}", file=sys.stderr)
    return quotes


def lower_map(row: dict) -> Dict[str, str]:
    return {str(key).strip().lower(): value for key, value in row.items()}


def first_value(row: Dict[str, str], keys: Iterable[str]) -> Optional[str]:
    for key in keys:
        if key in row and str(row[key]).strip():
            return str(row[key]).strip()
    return None


def load_questrade_csv(path: Path) -> Dict[str, dict]:
    if not path.exists():
        raise FileNotFoundError(f"Questrade CSV not found: {path}")

    quotes: Dict[str, dict] = {}
    with path.open("r", encoding="utf-8-sig", newline="") as handle:
        reader = csv.DictReader(handle)
        if reader.fieldnames is None:
            raise ValueError("Questrade CSV has no header row.")

        for raw_row in reader:
            row = lower_map(raw_row)
            symbol = first_value(row, ("symbol", "ticker", "security", "instrument"))
            if not symbol:
                continue
            symbol = symbol.upper()
            quotes[symbol] = {
                "symbol": symbol,
                "last": to_float(
                    first_value(row, ("last", "lastprice", "price", "mark"))
                ),
                "bid": to_float(first_value(row, ("bid", "bidprice"))),
                "ask": to_float(first_value(row, ("ask", "askprice"))),
                "volume": to_int(first_value(row, ("volume", "vol"))),
                "timestamp": first_value(
                    row,
                    (
                        "timestamp",
                        "time",
                        "updatedat",
                        "quote_time",
                        "quote time",
                    ),
                ),
            }
    return quotes


def diff_pct(a: Optional[float], b: Optional[float]) -> Optional[float]:
    if a is None or b is None:
        return None
    if b == 0:
        return None
    return ((a - b) / b) * 100.0


def build_snapshot(
    symbols: List[str], yahoo_quotes: Dict[str, dict], questrade_quotes: Dict[str, dict]
) -> dict:
    now = dt.datetime.now(tz=dt.timezone.utc).isoformat()
    rows = []
    for symbol in symbols:
        y = yahoo_quotes.get(symbol, {})
        q = questrade_quotes.get(symbol, {})

        y_price = to_float(y.get("price"))
        q_last = to_float(q.get("last"))
        price_delta = None
        if q_last is not None and y_price is not None:
            price_delta = q_last - y_price

        row = {
            "symbol": symbol,
            "snapshot_time_utc": now,
            "yahoo_price": y_price,
            "yahoo_bid": to_float(y.get("bid")),
            "yahoo_ask": to_float(y.get("ask")),
            "yahoo_volume": to_int(y.get("volume")),
            "yahoo_market_state": y.get("market_state"),
            "yahoo_exchange": y.get("exchange"),
            "yahoo_market_time_utc": y.get("market_time_utc"),
            "questrade_last": q_last,
            "questrade_bid": to_float(q.get("bid")),
            "questrade_ask": to_float(q.get("ask")),
            "questrade_volume": to_int(q.get("volume")),
            "questrade_timestamp": q.get("timestamp"),
            "price_delta_qt_minus_yahoo": price_delta,
            "price_delta_pct": diff_pct(q_last, y_price),
            "symbol_status": "ok",
        }
        if not y and not q:
            row["symbol_status"] = "missing_all_sources"
        elif not y:
            row["symbol_status"] = "missing_yahoo"
        elif not q:
            row["symbol_status"] = "missing_questrade"

        rows.append(row)

    return {"generated_at_utc": now, "count": len(rows), "rows": rows}


def write_csv(path: Path, rows: List[dict]) -> None:
    fieldnames = [
        "symbol",
        "snapshot_time_utc",
        "symbol_status",
        "yahoo_price",
        "yahoo_bid",
        "yahoo_ask",
        "yahoo_volume",
        "yahoo_market_state",
        "yahoo_exchange",
        "yahoo_market_time_utc",
        "questrade_last",
        "questrade_bid",
        "questrade_ask",
        "questrade_volume",
        "questrade_timestamp",
        "price_delta_qt_minus_yahoo",
        "price_delta_pct",
    ]
    with path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=fieldnames)
        writer.writeheader()
        for row in rows:
            cleaned = {}
            for key in fieldnames:
                value = row.get(key)
                if isinstance(value, float) and (math.isnan(value) or math.isinf(value)):
                    cleaned[key] = ""
                else:
                    cleaned[key] = value
            writer.writerow(cleaned)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            "Create a monitoring snapshot by combining Yahoo quotes with "
            "optional Questrade CSV quote exports."
        )
    )
    parser.add_argument(
        "--symbols",
        required=True,
        help="Comma-separated symbols (example: AAPL,MSFT,TSLA).",
    )
    parser.add_argument(
        "--questrade-csv",
        default="",
        help="Path to Questrade CSV export with symbol and quote columns.",
    )
    parser.add_argument(
        "--out",
        required=True,
        help="Output file path.",
    )
    parser.add_argument(
        "--format",
        choices=("json", "csv"),
        default="json",
        help="Output format. Default: json.",
    )
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    try:
        symbols = parse_symbols(args.symbols)
        yahoo_quotes = fetch_yahoo_quotes(symbols)
        questrade_quotes = {}
        if args.questrade_csv:
            questrade_quotes = load_questrade_csv(Path(args.questrade_csv))
        snapshot = build_snapshot(symbols, yahoo_quotes, questrade_quotes)
        out_path = Path(args.out)
        out_path.parent.mkdir(parents=True, exist_ok=True)
        if args.format == "json":
            out_path.write_text(json.dumps(snapshot, indent=2), encoding="utf-8")
        else:
            write_csv(out_path, snapshot["rows"])
        print(f"Wrote {snapshot['count']} symbols to {out_path}")
        return 0
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())


path: references/data-contracts.md
content: # Open Claw Questrade Data Contracts

Use these contracts to keep monitoring snapshots and trade tickets consistent.

## 1) Symbol Set
- Keep symbols uppercase.
- Use comma-separated symbols for script input, example: `AAPL,MSFT,TSLA`.
- Reject empty symbols or unknown side/order fields.

## 2) Questrade CSV Input (`market_snapshot.py --questrade-csv`)
Accept flexible broker-export headers. Supported aliases:

- Symbol: `Symbol`, `Ticker`, `Security`, `Instrument`
- Last: `Last`, `LastPrice`, `Price`, `Mark`
- Bid: `Bid`, `BidPrice`
- Ask: `Ask`, `AskPrice`
- Volume: `Volume`, `Vol`
- Timestamp: `Timestamp`, `Time`, `UpdatedAt`, `Quote_Time`, `Quote Time`

Recommended CSV shape:

```csv
Symbol,Bid,Ask,Last,Volume,Timestamp
AAPL,195.10,195.14,195.12,984322,2026-02-19T15:24:31-05:00
MSFT,421.88,421.95,421.90,612345,2026-02-19T15:24:30-05:00
```

## 3) Snapshot Output JSON (`market_snapshot.py --format json`)
Top-level fields:
- `generated_at_utc`: ISO-8601 timestamp
- `count`: integer symbol count
- `rows`: list of per-symbol objects

Per-symbol fields:
- `symbol`
- `snapshot_time_utc`
- `symbol_status`: `ok`, `missing_yahoo`, `missing_questrade`, `missing_all_sources`
- Yahoo fields: `yahoo_price`, `yahoo_bid`, `yahoo_ask`, `yahoo_volume`, `yahoo_market_state`, `yahoo_exchange`, `yahoo_market_time_utc`
- Questrade fields: `questrade_last`, `questrade_bid`, `questrade_ask`, `questrade_volume`, `questrade_timestamp`
- Comparison fields: `price_delta_qt_minus_yahoo`, `price_delta_pct`

## 4) Snapshot Output CSV (`market_snapshot.py --format csv`)
Use this exact column order:

1. `symbol`
2. `snapshot_time_utc`
3. `symbol_status`
4. `yahoo_price`
5. `yahoo_bid`
6. `yahoo_ask`
7. `yahoo_volume`
8. `yahoo_market_state`
9. `yahoo_exchange`
10. `yahoo_market_time_utc`
11. `questrade_last`
12. `questrade_bid`
13. `questrade_ask`
14. `questrade_volume`
15. `questrade_timestamp`
16. `price_delta_qt_minus_yahoo`
17. `price_delta_pct`

## 5) Trade Checklist Input (`build_trade_checklist.py`)
Required:
- `--account-id`
- `--symbol`
- `--side` (`buy` or `sell`)
- `--quantity` (integer > 0)
- `--order-type` (`market`, `limit`, `stop`, `stop_limit`)
- `--policy-ack OPENCLAW_POLICY_ACK`
- `--confirm-user-authorized`
- `--confirm-manual-execution`
- `--confirm-no-secrets-shared`
- `--out`

Conditional:
- `--limit-price` required for `limit` and `stop_limit`
- `--stop-price` required for `stop` and `stop_limit`
- `--risk-cap-usd` required in `--environment live`
- `--data-age-seconds` required in `--environment live`
- `--allow-market-order-live` required for `--order-type market` in `--environment live`
- `--local-sensitive-storage-confirm` required with `--include-sensitive`

Optional:
- `--environment` (`paper`, `live`; default `live`)
- `--tif` (`day`, `gtc`, `gted`)
- `--max-data-age-seconds` (default `300`)
- `--observed-price-drift-pct`
- `--max-price-drift-pct` (default `1.0`)
- `--notes`

## 6) Trade Checklist Output
Markdown file with:
- Human-readable ticket summary
- Pre-trade hard checks
- Browser submission checklist
- Post-submission log placeholders
- Embedded JSON ticket payload

Privacy defaults:
- `account_id` is masked by default.
- Raw account identifier is included only when `--include-sensitive` is passed intentionally.

Special safety output:
- Includes `special_safety_check` block with pass-state details and thresholds used.


path: references/openclaw-policy-compliance.md
content: # OpenClaw.ai Policy Compliance

Use this file when enforcing policy behavior for `open-claw-questrade`.

## 1) Special Safety Check (Mandatory)
Before producing a live-trade checklist, confirm:
- Policy acknowledgement phrase is present (`OPENCLAW_POLICY_ACK`).
- User authorization is explicitly confirmed.
- Manual execution is explicitly confirmed (no autonomous submission).
- No secrets were shared in prompt or artifacts.

## 2) Live Mode Enforcement
In `live` mode, block output unless:
- `risk_cap_usd` is provided.
- `data_age_seconds` is provided and within `max_data_age_seconds`.
- Broker-vs-reference drift is within `max_price_drift_pct` when supplied.
- `market` order type is explicitly allowed via override flag.

## 3) Sensitive Data Controls
- Mask account identifiers by default.
- Only include raw identifiers with explicit intent:
  - `--include-sensitive`
  - `--local-sensitive-storage-confirm`
- Never include secrets (passwords, MFA codes, tokens, cookies) in outputs.

## 4) Default Refusal Conditions
Refuse or block checklist generation when:
- Symbol, side, quantity, or order parameters are ambiguous.
- Safety confirmation flags are missing.
- Required live-mode checks fail.
- User asks to bypass compliance checks.

## 5) Operational Notes
- This skill supports policy-compliant execution support, not autonomous brokerage control.
- Human confirmation in Questrade browser is the only valid source of execution truth.



path: references/questrade-browser-playbook.md
content: # Questrade Browser Playbook

Use this playbook when preparing or executing stock trades in Questrade Web for openclaw.ai workflows.

## 0) Privacy Baseline
- Keep credentials, MFA codes, and tokens on the user's device only.
- Never paste passwords, token secrets, or session cookies into prompts or logs.
- Use masked account/order identifiers in generated artifacts unless explicitly overridden.
- Keep broker exports local; redact personal fields before sharing externally.

## 1) Session Preparation
- Confirm the target account (cash, margin, or registered).
- Confirm market session status (pre-market, regular, after-hours).
- Confirm which order types are allowed in the current session.
- Confirm browser tab is logged in and session has not timed out.
- Complete the Special Safety Check before preparing final order ticket fields.

## 2) Build the Order Ticket
- Open the order entry panel in Questrade Web.
- Select the account intentionally. Do not rely on defaults.
- Search and select the exact symbol.
- Set side (`Buy` or `Sell`) and share quantity.
- Set order type:
  - `Market`: no limit/stop price
  - `Limit`: limit price required
  - `Stop`: stop trigger required
  - `Stop Limit`: stop + limit required
- Set time in force (`DAY`, `GTC`, `GTED`) according to strategy.

## 3) Cross-Check Data Before Submit
- Compare broker quote with Yahoo snapshot from `market_snapshot.py`.
- Flag divergence above user tolerance before submission.
- Confirm estimated notional, fees, and available buying power.
- Confirm stop-loss or exit logic is documented.
- If live-mode freshness/drift thresholds fail, block submission and refresh data.

## 4) Submit and Verify
- Review the full ticket one final time.
- Submit order in Questrade Web.
- Capture:
  - Confirmation ID
  - Submitted timestamp (UTC)
  - Initial status (open/partial/filled/rejected/cancelled)

## 5) Post-Trade Monitoring
- Track fills until terminal state.
- Record average fill price and remaining quantity.
- Reconcile against strategy plan and risk limits.
- Save an execution log entry with all final values.

## 6) Failure Modes and Recovery
- Session timeout: re-authenticate, reload ticket, re-validate all fields.
- Quote mismatch: refresh data on both broker and Yahoo sides; do not submit stale ticket.
- Rejected order: capture reject reason and update order type/price logic.
- Partial fill drift: recompute residual risk and adjust stop/exit plan.

## 7) Non-Negotiable Safety Rules
- Never imply order execution without human confirmation in the broker UI.
- Never reuse stale order parameters from previous symbols.
- Never submit with ambiguous side, quantity, or order type.
- Never skip risk checks for urgency.
- Never request or store user secrets outside local user-controlled context.


path: agents/openai.yaml
content: interface:
  display_name: "Open Claw Questrade"
  short_description: "openclaw.ai Questrade trading and monitoring"
  default_prompt: "Use $open-claw-questrade to monitor Questrade and Yahoo Finance data and run a privacy-safe browser trade checklist."


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)