#Trading engine

1 messages · Page 1 of 1 (latest)

tall glade
#

I want to create a fully automated trading engine in openclaw. Below is the flow:

workflow: auto_trader

trigger:
schedule: every_5_minutes

steps:

  • name: fetch_market_data
    action: call_api
    url: /market-data

  • name: generate_signal
    agent: trading_agent

  • name: risk_check
    agent: risk_agent

  • name: execute_trade
    action: call_api
    url: /place-order

  • name: log_trade
    action: call_api
    url: /log

The final system should loon like

OpenClaw Trigger →
Fetch Data →
Generate Signal (Agent) →
Risk Check →
Backend API →
Kite Order →
Store Result →
Next Cycle

thin trellisBOT
heavy jacinthBOT
#

Yes, but I’d build it a little differently.

Your YAML is a good design sketch, but OpenClaw does not currently expose a “drop in this workflow YAML and run it” engine. Task Flow exists, but the docs expose inspection/cancel, not public YAML authoring.

Recommended architecture

OpenClaw cron → orchestrator → backend market API → trading_agent → risk_agent → backend order service → Kite → DB/logs

Best split

  • OpenClaw: scheduling, agent reasoning, coordination
  • Your backend: market data fetch, broker auth, order placement, logging
  • Agents: generate_signal, risk_check

I would not let an LLM place live Kite orders directly without a hard backend guardrail.

Use these OpenClaw pieces

#

Important nuance
Your call_api steps are not a native OpenClaw YAML action. For that part, use:

  1. a custom skill/plugin/tool, or
  2. a backend service/script that OpenClaw invokes

MVP I’d recommend

  1. Create trading and risk agents
  2. Add a cron job for */5 * * * *
  3. Cron wakes an isolated orchestrator run
  4. Fetch market snapshot from your backend
  5. Ask trading_agent for signal
  6. Ask risk_agent for approve/reject and size
#
  1. Backend executes via Kite
  2. Backend logs result
  3. OpenClaw stores a summary for the next cycle

Hard safety bits

  • paper trade first
  • max position / loss caps in backend
  • idempotency key per cycle
  • kill switch
  • never execute from raw model output alone

If you want, I can sketch the exact OpenClaw setup next, agents, cron command, and backend API contract.

tall glade
#

Yes

heavy jacinthBOT
#

Nice. Here’s the setup I’d actually use.

1. Architecture I recommend

Do not let the LLM talk to Kite directly.

Use this split instead:

OpenClaw cron → orchestrator agent → backend API/script → trading agent → risk agent → backend /place-order → Kite → /log

So OpenClaw handles schedule + reasoning, your backend handles execution + auth + idempotency + audit.

Docs:

#

2. Create 3 agents

openclaw agents add orchestrator --workspace ~/.openclaw/workspace-orchestrator
openclaw agents add trading --workspace ~/.openclaw/workspace-trading
openclaw agents add risk --workspace ~/.openclaw/workspace-risk

You don’t need bindings if you only call them with openclaw agent --agent ....


3. Put standing orders in each agent

~/.openclaw/workspace-orchestrator/AGENTS.md

#
# Orchestrator

## Program: Auto Trader

Authority:
- Run the 5 minute trading cycle
- Fetch market snapshot from backend
- Call trading and risk agents
- Call backend order API only if risk approves
- Log every cycle

Rules:
- Never place orders directly with broker APIs
- Never bypass backend risk checks
- Never retry order placement blindly
#
- Stop on malformed JSON, stale data, or duplicate cycleId
- Return a short summary only

~/.openclaw/workspace-trading/AGENTS.md

# Trading Agent

You analyze market snapshots and return JSON only.

Schema:
{
  "action": "buy|sell|hold",
  "symbol": "string",
  "confidence": 0.0,
#
  "size_hint": 0,
  "thesis": "short reason",
  "timeframe": "5m"
}

Rules:
- No prose outside JSON
- If signal quality is weak, return hold
- Never mention order execution

~/.openclaw/workspace-risk/AGENTS.md

# Risk Agent
#
You review a proposed trade and return JSON only.

Schema:
{
  "approved": true,
  "max_size": 0,
  "reason": "short reason",
  "blocks": []
}

Rules:
- Reject on stale data, oversized position, cooldown breach, or missing fields
- Conservative by default
- No prose outside JSON
#

Standing orders docs: https://docs.openclaw.ai/automation/standing-orders


4. Use one deterministic runner script

Put the real orchestration in a script or service, for example:

/opt/trader/run-cycle.sh

That script should:

  1. create cycleId
  2. fetch /market-data
  3. call openclaw agent --agent trading
  4. call openclaw agent --agent risk
  5. if approved, call /place-order
#
  1. call /log
  2. exit non-zero on any hard failure

Very rough skeleton:

#!/usr/bin/env bash
set -euo pipefail

CYCLE_ID="$(date -u +%Y%m%dT%H%M%SZ)"
API="http://127.0.0.1:9000"

MARKET_JSON="$(curl -sS -X POST "$API/market-data" \
  -H 'Content-Type: application/json' \
  -d "{\"cycleId\":\"$CYCLE_ID\",\"timeframe\":\"5m\"}")"
#
SIGNAL_JSON="$(openclaw agent --agent trading \
  --message "Return JSON only. Market snapshot: $MARKET_JSON" \
  --json)"

RISK_JSON="$(openclaw agent --agent risk \
  --message "Return JSON only. Market snapshot: $MARKET_JSON Signal: $SIGNAL_JSON" \
  --json)"

# If approved, backend executes with Kite
curl -sS -X POST "$API/place-order" \
  -H 'Content-Type: application/json' \
  -d "{\"cycleId\":\"$CYCLE_ID\",\"market\":$MARKET_JSON,\"signal\":$SIGNAL_JSON,\"risk\":$RISK_JSON}"

curl -sS -X POST "$API/log" \
  -H 'Content-Type: application/json' \
#
  -d "{\"cycleId\":\"$CYCLE_ID\",\"market\":$MARKET_JSON,\"signal\":$SIGNAL_JSON,\"risk\":$RISK_JSON}"

For production, I’d make this a proper backend service, not a shell script.


5. Let OpenClaw cron trigger the orchestrator

Use an isolated cron run every 5 minutes:

openclaw cron add \
  --name "auto-trader" \
  --cron "*/5 * * * *" \
#
  --session isolated \
  --agent orchestrator \
  --message "Run /opt/trader/run-cycle.sh. Follow standing orders. Return a 3 line summary only." \
  --no-deliver

Then inspect runs with:

openclaw cron list
openclaw cron runs --id <job-id> --limit 50

Cron docs: https://docs.openclaw.ai/cli/cron


#

6. Backend API contract

I’d use these payloads.

POST /market-data

{
  "cycleId": "20260420T110500Z",
  "timeframe": "5m",
  "symbols": ["NIFTY", "BANKNIFTY"]
}

Response:

{
#
  "cycleId": "20260420T110500Z",
  "asOf": "2026-04-20T11:05:00Z",
  "cash": 250000,
  "positions": [],
  "quotes": [],
  "indicators": {},
  "stale": false
}

POST /place-order

{
  "cycleId": "20260420T110500Z",
  "signal": {
#
    "action": "buy",
    "symbol": "NIFTY",
    "confidence": 0.74,
    "size_hint": 1
  },
  "risk": {
    "approved": true,
    "max_size": 1
  }
}

Response:

{
#
  "cycleId": "20260420T110500Z",
  "accepted": true,
  "broker": "kite",
  "orderId": "abc123",
  "status": "placed"
}

POST /log

{
  "cycleId": "20260420T110500Z",
  "market": {},
  "signal": {},
  "risk": {},
#
  "execution": {}
}

7. Hard rules I’d enforce in backend

  • paper mode first
  • reject duplicate cycleId
  • idempotency key on every /place-order
  • max daily loss
  • max position size
  • cooldown between trades
  • stale market data rejection
  • market-hours check
#
  • kill switch

This part should be deterministic, not AI-driven.


8. Practical note

Your original YAML is a good spec, but not a native executable DSL in OpenClaw today. The real implementation is:

  • OpenClaw cron
  • OpenClaw agents
  • your own runner/backend for API calls and Kite execution

If you want, I can do the next layer and sketch:

  1. the exact openclaw.json agent config, or
  2. a proper Python/FastAPI backend for /market-data, /place-order, /log.
tall glade
#

I have some rules too like:

CAPITAL = 3000

RISK_PER_TRADE = 20%

MAX_DAILY_LOSS = 15%

MAX_TRADES_PER_DAY = 10

TIMEFRAME = "1minute"

heavy jacinthBOT
#

Good, and this changes the design a lot.

Two important issues jump out:

  1. Your risk rules conflict
    • CAPITAL = 3000
    • RISK_PER_TRADE = 20% → $600 risk/trade
    • MAX_DAILY_LOSS = 15% → $450/day

So one full-loss trade can already break your daily limit.
I’m worried that’s too aggressive for unattended automation.

  1. Your timing conflicts
    • trigger: every 5 minutes
    • timeframe: 1 minute

If you want 1m trading, the engine should usually run every minute, not every 5.

#

I would encode your rules like this

{
  "capital": 3000,
  "riskPerTradePct": 0.20,
  "maxDailyLossPct": 0.15,
  "maxTradesPerDay": 10,
  "timeframe": "1m"
}

Derived limits

#
{
  "riskPerTradeDollars": 600,
  "maxDailyLossDollars": 450
}

Effective backend guard

effectiveRisk = min(
  capital * riskPerTradePct,
  maxDailyLossDollars - realizedLossToday
)

If effectiveRisk <= 0, block trading for the day.

#

Hard checks that must live in backend, not the agent

Before /place-order, backend should reject if:

  • tradesToday >= 10
  • realizedLossToday >= 450
  • requestedRisk > remainingDailyLoss
  • market data is stale
  • candle is not closed yet
  • exchange is outside market hours
  • duplicate cycleId

That means the agent can suggest a trade, but the backend is the final gate.


#

Updated workflow I’d use

If you really want TIMEFRAME = "1minute":

OpenClaw cron every 1m
→ fetch last closed 1m candle
→ trading_agent generates signal
→ risk_agent reviews
→ backend hard-checks rules
→ Kite order
→ log result

So I’d change your trigger to:

openclaw cron edit <job-id> --cron "*/1 * * * *"
#

Or create it that way from the start.


What the risk agent should check

Have risk_agent return something like:

{
  "approved": false,
  "maxSize": 0,
  "riskDollars": 450,
  "reason": "Configured per-trade risk exceeds remaining daily loss",
  "blocks": ["daily_loss_conflict"]
}
#

Important: the agent explains risk, the backend enforces risk.


My recommendation

For an automated system, I’d change these before going live:

  • lower RISK_PER_TRADE
  • keep MAX_DAILY_LOSS
  • run paper trading first
  • align trigger with timeframe

Right now the clean system answer is:

  • 1m timeframe => 1m schedule
  • 20% per trade is too high for auto mode
#
  • backend must cap first trade risk to remaining daily loss

If you want, I can turn your numbers into:

  1. the exact backend validation logic, or
  2. the exact risk_agent JSON schema + prompt.
tall glade
#

What if i build candles for realtime trade like

from kiteconnect import KiteTicker

def on_ticks(ws, ticks):

process_ticks(ticks)

def start_ws(api_key, access_token):

kws = KiteTicker(api_key, access_token)

kws.on_ticks = on_ticks

kws.connect(threaded=True)
heavy jacinthBOT
#

Yep, that’s the right direction for 1m trading.

But I’d keep KiteTicker + candle building in your backend, not inside the OpenClaw agent loop.

Best pattern

KiteTicker → build 1m candles → close candle → OpenClaw agent(s) → backend risk/execution → Kite order

So OpenClaw sees closed candle snapshots, not raw ticks.

Why

Raw ticks are too noisy and too latency-sensitive for an agent path.
For real-time trading, the deterministic parts should stay in code:

  • websocket reconnects
  • tick dedupe
#
  • candle aggregation
  • market-hours logic
  • order placement
  • risk limits

OpenClaw should do the reasoning layer, not the socket layer.


Two good options

Option 1, simpler, recommended first

  • backend builds candles from KiteTicker
  • writes latest closed 1m candle to DB/Redis
  • OpenClaw cron runs every minute
  • cron reads latest closed candle from backend
  • trading_agent and risk_agent evaluate it
#

Good if you want simpler ops.

Option 2, cleaner event-driven

  • backend builds candles
  • when a candle closes, backend calls OpenClaw hook
  • OpenClaw runs trading_agent immediately on candle close

Good if you want less polling.

For this, look at hooks/webhooks:
https://docs.openclaw.ai/automation/cron-jobs#webhooks


Very important

If you go websocket-based, I would not send ticks to the agent like:

#
  • tick 1
  • tick 2
  • tick 3
  • ...

Instead send one payload per closed candle, for example:

{
  "symbol": "NIFTY",
  "timeframe": "1m",
  "candle": {
    "ts": "2026-04-20T11:32:00Z",
    "open": 22450.1,
    "high": 22461.8,
    "low": 22448.9,
#
    "close": 22459.4,
    "volume": 12840,
    "closed": true
  },
  "position": {
    "side": "flat",
    "qty": 0
  },
  "risk": {
    "capital": 3000,
    "pnlToday": -40,
    "tradesToday": 2,
    "remainingDailyLoss": 410
  }
}
#

That is the right unit for agent decisioning.


What your candle builder must handle

  • minute bucket rollover
  • reconnect/resubscribe
  • duplicate ticks
  • late ticks
  • missing candle backfill after disconnect
  • timezone/session boundaries
  • emit candle only once

If you miss this stuff, the strategy will drift fast.


#

My honest recommendation

For 1m candles, websocket candle building is good.
For sub-second execution, I would keep OpenClaw out of the hot path entirely.

So:

  • 1m closed-candle strategy → OpenClaw can fit
  • tick-by-tick / very low latency → use deterministic strategy engine, let OpenClaw supervise only

If you want, I can sketch the exact event-driven architecture next:
KiteTicker → candle builder → OpenClaw hook → trading_agent → risk_agent → place-order.

tall glade
#

Yes.. add realtime news bias too. It is for indian stock market only. Give full detailed drop-in implementation openclaw, python, backend everything

heavy jacinthBOT
#

Yep. For 1m Indian-market trading with realtime candles + news bias, this is the version I’d actually build.

Small but important opinion first: for this setup, Python should own the hot path, and OpenClaw should own the reasoning path. Trying to force raw ticks through OpenClaw first is the wrong shape.

Final architecture

KiteTicker WebSocket
  -> tick normalizer
  -> 1m candle builder
  -> on closed candle:
       load latest news bias
       load account + PnL + trade count
       call OpenClaw trading_agent
       call OpenClaw risk_agent
       backend hard-checks limits
#
       place order via Kite
       store result
       optional notify / summary

Parallel path:
News ingest (RSS/API/provider)
  -> symbol resolver (Indian stocks only)
  -> OpenClaw news_agent scores symbol bias
  -> cache latest bias per symbol

So OpenClaw is used for:

  • news_agent
  • trading_agent
  • risk_agent
#
auto-trader/
├── .env
├── openclaw/
│   ├── workspace-trading/
│   │   └── AGENTS.md
│   ├── workspace-risk/
│   │   └── AGENTS.md
│   └── workspace-news/
│       └── AGENTS.md
├── backend/
│   ├── app.py
│   ├── settings.py
│   ├── schemas.py
│   ├── state.py
│   ├── agent_client.py
#
│   ├── candle_builder.py
│   ├── broker_kite.py
│   ├── news_ingestor.py
│   ├── news_bias_worker.py
│   ├── symbol_map.py
│   ├── risk_math.py
│   ├── trading_engine.py
│   └── storage.py
└── requirements.txt

2. OpenClaw agent setup

Create the agents:

#
openclaw agents add trading --workspace /ABS/PATH/auto-trader/openclaw/workspace-trading
openclaw agents add risk --workspace /ABS/PATH/auto-trader/openclaw/workspace-risk
openclaw agents add news --workspace /ABS/PATH/auto-trader/openclaw/workspace-news

workspace-trading/AGENTS.md

# Trading Agent

You are the intraday signal engine for Indian equities and F&O.
You analyze only the provided input.
Do not invent data.
Return JSON only.
#
Output schema:
{
  "action": "buy|sell|hold",
  "symbol": "string",
  "confidence": 0.0,
  "setup": "trend_breakout|mean_reversion|pullback|none",
  "entry_type": "market|limit|none",
  "entry_price": 0.0,
  "stop_loss": 0.0,
  "take_profit": 0.0,
  "size_hint": 0,
  "reason": "short string",
  "news_adjustment": -1.0
}
#
Rules:
- If the candle is not closed, return hold.
- If news bias is strongly negative for a long setup, reduce confidence.
- If news bias is strongly positive for a short setup, reduce confidence.
- If signal quality is weak, return hold.
- Never discuss execution, only signal quality.
- No prose outside JSON.

workspace-risk/AGENTS.md

# Risk Agent

You are the hard risk reviewer.
#
Return JSON only.

Output schema:
{
  "approved": false,
  "max_qty": 0,
  "risk_dollars": 0.0,
  "reason": "short string",
  "blocks": [],
  "size_multiplier": 0.0
}

Rules:
- Be conservative by default.
- Reject malformed input.
#
- Reject if remaining daily loss is too small.
- Reject if max trades per day reached.
- Reject if signal confidence is low.
- Reject if news bias is strongly adverse.
- Never exceed the backend risk cap.
- No prose outside JSON.

workspace-news/AGENTS.md

# News Agent

You score recent Indian market headlines for one symbol.
Return JSON only.
#

Output schema:
{
  "symbol": "string",
  "bias": -1.0,
  "confidence": 0.0,
  "summary": "short string",
  "expires_minutes": 15
}

Meaning:
- bias = -1 strongly bearish
- bias = 0 neutral
- bias = +1 strongly bullish
#
Rules:
- Use only the provided headlines.
- Prefer neutral if headlines are ambiguous.
- Ignore duplicate headlines.
- Penalize rumors or low-quality sources.
- No prose outside JSON.

3. Environment

.env

#
KITE_API_KEY=xxx
KITE_ACCESS_TOKEN=xxx

OPENCLAW_BIN=openclaw

CAPITAL=3000
RISK_PER_TRADE_PCT=0.20
MAX_DAILY_LOSS_PCT=0.15
MAX_TRADES_PER_DAY=10

TIMEFRAME_SEC=60
TZ=Asia/Kolkata

WATCHLIST=RELIANCE,TCS,INFY,HDFCBANK,ICICIBANK,SBIN
NEWS_POLL_SEC=30
#

requirements.txt

fastapi
uvicorn
pydantic
python-dotenv
kiteconnect
requests
orjson

If you want DB persistence, add sqlalchemy and psycopg later. For now I’ll keep the core scaffold cleaner.


4. Backend config

#

backend/settings.py

from dataclasses import dataclass
from dotenv import load_dotenv
import os

load_dotenv()

@dataclass
class Settings:
    kite_api_key: str = os.getenv("KITE_API_KEY", "")
    kite_access_token: str = os.getenv("KITE_ACCESS_TOKEN", "")
    openclaw_bin: str = os.getenv("OPENCLAW_BIN", "openclaw")

    capital: float = float(os.getenv("CAPITAL", "3000"))
#
    risk_per_trade_pct: float = float(os.getenv("RISK_PER_TRADE_PCT", "0.20"))
    max_daily_loss_pct: float = float(os.getenv("MAX_DAILY_LOSS_PCT", "0.15"))
    max_trades_per_day: int = int(os.getenv("MAX_TRADES_PER_DAY", "10"))

    timeframe_sec: int = int(os.getenv("TIMEFRAME_SEC", "60"))
    tz: str = os.getenv("TZ", "Asia/Kolkata")

    watchlist: list[str] = [s.strip() for s in os.getenv("WATCHLIST", "").split(",") if s.strip()]
    news_poll_sec: int = int(os.getenv("NEWS_POLL_SEC", "30"))

settings = Settings()

5. Schemas

#

backend/schemas.py

from dataclasses import dataclass, field
from typing import Any

@dataclass
class Candle:
    symbol: str
    ts: str
    open: float
    high: float
    low: float
    close: float
    volume: float
    closed: bool = True
#

@dataclass
class NewsBias:
    symbol: str
    bias: float
    confidence: float
    summary: str
    expires_minutes: int

@dataclass
class AccountState:
    capital: float
    realized_pnl_today: float
    trades_today: int
    remaining_daily_loss: float
#

@dataclass
class TradingSignal:
    action: str
    symbol: str
    confidence: float
    setup: str
    entry_type: str
    entry_price: float
    stop_loss: float
    take_profit: float
    size_hint: int
    reason: str
    news_adjustment: float = 0.0
#
@dataclass
class RiskDecision:
    approved: bool
    max_qty: int
    risk_dollars: float
    reason: str
    blocks: list[str] = field(default_factory=list)
    size_multiplier: float = 0.0

6. Shared state

backend/state.py

#
from collections import defaultdict
from threading import Lock
from dataclasses import asdict
from backend.schemas import NewsBias

class InMemoryState:
    def __init__(self):
        self.lock = Lock()
        self.news_bias: dict[str, NewsBias] = {}
        self.realized_pnl_today = 0.0
        self.trades_today = 0
        self.trade_log = []

    def set_news_bias(self, symbol: str, bias: NewsBias):
        with self.lock:
#
            self.news_bias[symbol] = bias

    def get_news_bias(self, symbol: str):
        with self.lock:
            return self.news_bias.get(symbol)

    def add_trade_log(self, row: dict):
        with self.lock:
            self.trade_log.append(row)

STATE = InMemoryState()

7. Risk math

#

backend/risk_math.py

from backend.settings import settings

def max_daily_loss_dollars() -> float:
    return settings.capital * settings.max_daily_loss_pct

def configured_trade_risk_dollars() -> float:
    return settings.capital * settings.risk_per_trade_pct

def remaining_daily_loss(realized_pnl_today: float) -> float:
    # assuming realized_pnl_today is negative on loss
    loss_used = abs(min(realized_pnl_today, 0))
    return max(0.0, max_daily_loss_dollars() - loss_used)
#
def effective_trade_risk(realized_pnl_today: float) -> float:
    return min(configured_trade_risk_dollars(), remaining_daily_loss(realized_pnl_today))

def can_trade_today(trades_today: int, realized_pnl_today: float) -> tuple[bool, list[str]]:
    blocks = []
    if trades_today >= settings.max_trades_per_day:
        blocks.append("max_trades_reached")
    if remaining_daily_loss(realized_pnl_today) <= 0:
        blocks.append("daily_loss_reached")
    return (len(blocks) == 0, blocks)

Important note: with your numbers:

  • risk_per_trade = $600
  • max_daily_loss = $450
#

So backend will cap first-trade risk to $450 max, and realistically I’d set it lower.


8. Candle builder from KiteTicker

backend/candle_builder.py

from dataclasses import dataclass
from datetime import datetime
from zoneinfo import ZoneInfo
from backend.settings import settings
from backend.schemas import Candle

IST = ZoneInfo(settings.tz)
#

@dataclass
class LiveBucket:
    symbol: str
    bucket_ts: int
    open: float
    high: float
    low: float
    close: float
    volume: float

class CandleBuilder:
    def __init__(self, on_closed_candle):
        self.on_closed_candle = on_closed_candle
        self.current: dict[str, LiveBucket] = {}
#

    def _bucket(self, epoch_sec: int) -> int:
        return epoch_sec - (epoch_sec % settings.timeframe_sec)

    def on_tick(self, symbol: str, ltp: float, volume: float, epoch_sec: int):
        bucket = self._bucket(epoch_sec)
        cur = self.current.get(symbol)

        if cur is None:
            self.current[symbol] = LiveBucket(symbol, bucket, ltp, ltp, ltp, ltp, volume)
            return

        if cur.bucket_ts != bucket:
            closed = Candle(
                symbol=symbol,
#
                ts=datetime.fromtimestamp(cur.bucket_ts, tz=IST).isoformat(),
                open=cur.open,
                high=cur.high,
                low=cur.low,
                close=cur.close,
                volume=cur.volume,
                closed=True,
            )
            self.on_closed_candle(closed)

            self.current[symbol] = LiveBucket(symbol, bucket, ltp, ltp, ltp, ltp, volume)
            return

        cur.high = max(cur.high, ltp)
        cur.low = min(cur.low, ltp)
#
        cur.close = ltp
        cur.volume += volume

9. Kite broker

backend/broker_kite.py

from kiteconnect import KiteConnect
from backend.settings import settings

class KiteBroker:
#
    def __init__(self):
        self.kite = KiteConnect(api_key=settings.kite_api_key)
        self.kite.set_access_token(settings.kite_access_token)

    def place_order(self, symbol: str, side: str, qty: int):
        txn = "BUY" if side.lower() == "buy" else "SELL"
        # exchange/product/order_type will depend on your segment
        order_id = self.kite.place_order(
            variety="regular",
            exchange="NSE",
            tradingsymbol=symbol,
            transaction_type=txn,
            quantity=qty,
            product="CNC",
            order_type="MARKET"
#
        )
        return {"order_id": order_id, "status": "placed"}

You’ll need to adapt exchange/product for NSE cash vs NFO.


10. OpenClaw agent client

backend/agent_client.py

import json
import subprocess
#
from backend.settings import settings

def _extract_json(text: str):
    text = text.strip()
    start = text.find("{")
    end = text.rfind("}")
    if start == -1 or end == -1:
        raise ValueError(f"No JSON found in agent output: {text}")
    return json.loads(text[start:end+1])

def call_agent(agent_id: str, payload: dict) -> dict:
    prompt = json.dumps(payload, ensure_ascii=False)
    proc = subprocess.run(
        [settings.openclaw_bin, "agent", "--agent", agent_id, "--message", prompt],
        capture_output=True,
#
        text=True,
        check=True,
    )
    return _extract_json(proc.stdout)

This is the simplest reliable bridge.


11. Indian symbol mapping for news

backend/symbol_map.py

#
SYMBOL_ALIASES = {
    "RELIANCE": ["reliance", "reliance industries", "ril"],
    "TCS": ["tcs", "tata consultancy services"],
    "INFY": ["infosys", "infy"],
    "HDFCBANK": ["hdfc bank", "hdfcbank"],
    "ICICIBANK": ["icici bank", "icicibank"],
    "SBIN": ["state bank of india", "sbi", "sbin"],
}

def resolve_symbols_from_headline(headline: str) -> list[str]:
    hl = headline.lower()
    out = []
    for symbol, aliases in SYMBOL_ALIASES.items():
        if any(a in hl for a in aliases):
            out.append(symbol)
#
    return out

12. News ingest + bias scoring

Use whatever provider you trust, RSS/API/vendor. For Indian markets, I’d keep it provider-agnostic here.

backend/news_ingestor.py

import time
import requests
from backend.symbol_map import resolve_symbols_from_headline
#

class NewsIngestor:
    def fetch(self) -> list[dict]:
        # Replace with your real providers
        # Example item: {"headline": "...", "source": "...", "url": "...", "ts": "..."}
        return []

    def grouped_by_symbol(self) -> dict[str, list[dict]]:
        grouped: dict[str, list[dict]] = {}
        for item in self.fetch():
            symbols = resolve_symbols_from_headline(item["headline"])
            for symbol in symbols:
                grouped.setdefault(symbol, []).append(item)
        return grouped
#

backend/news_bias_worker.py

import time
from backend.agent_client import call_agent
from backend.schemas import NewsBias
from backend.state import STATE
from backend.news_ingestor import NewsIngestor
from backend.settings import settings

def run_news_bias_loop():
    ingestor = NewsIngestor()

    while True:
        grouped = ingestor.grouped_by_symbol()
#
        for symbol, items in grouped.items():
            payload = {
                "symbol": symbol,
                "market": "India",
                "headlines": items[-10:],
            }
            try:
                result = call_agent("news", payload)
                bias = NewsBias(
                    symbol=symbol,
                    bias=float(result["bias"]),
                    confidence=float(result["confidence"]),
                    summary=result["summary"],
                    expires_minutes=int(result.get("expires_minutes", 15)),
                )
#
                STATE.set_news_bias(symbol, bias)
            except Exception as e:
                print("news bias error", symbol, e)

        time.sleep(settings.news_poll_sec)

This keeps news scoring off the hot execution path.


13. Trading engine

backend/trading_engine.py

#
from dataclasses import asdict
from backend.agent_client import call_agent
from backend.broker_kite import KiteBroker
from backend.risk_math import remaining_daily_loss, effective_trade_risk, can_trade_today
from backend.schemas import Candle
from backend.state import STATE
from backend.settings import settings

broker = KiteBroker()

def on_closed_candle(candle: Candle):
    ok, blocks = can_trade_today(STATE.trades_today, STATE.realized_pnl_today)
    if not ok:
        STATE.add_trade_log({
            "symbol": candle.symbol,
#
            "ts": candle.ts,
            "status": "blocked_precheck",
            "blocks": blocks,
        })
        return

    news_bias = STATE.get_news_bias(candle.symbol)
    risk_cap = effective_trade_risk(STATE.realized_pnl_today)

    signal_input = {
        "market": "India",
        "exchange": "NSE",
        "timeframe": "1m",
        "candle": asdict(candle),
        "news_bias": asdict(news_bias) if news_bias else None,
#
        "account": {
            "capital": settings.capital,
            "realized_pnl_today": STATE.realized_pnl_today,
            "trades_today": STATE.trades_today,
            "remaining_daily_loss": remaining_daily_loss(STATE.realized_pnl_today),
        }
    }

    signal = call_agent("trading", signal_input)

    risk_input = {
        "signal": signal,
        "candle": asdict(candle),
        "news_bias": asdict(news_bias) if news_bias else None,
        "limits": {
#
            "capital": settings.capital,
            "risk_per_trade_pct": settings.risk_per_trade_pct,
            "max_daily_loss_pct": settings.max_daily_loss_pct,
            "max_trades_per_day": settings.max_trades_per_day,
            "effective_trade_risk": risk_cap,
            "remaining_daily_loss": remaining_daily_loss(STATE.realized_pnl_today),
            "trades_today": STATE.trades_today,
        }
    }

    risk = call_agent("risk", risk_input)

    if signal["action"] == "hold":
        STATE.add_trade_log({
            "symbol": candle.symbol,
#
            "ts": candle.ts,
            "status": "hold",
            "signal": signal,
            "risk": risk,
        })
        return

    if not risk["approved"]:
        STATE.add_trade_log({
            "symbol": candle.symbol,
            "ts": candle.ts,
            "status": "risk_rejected",
            "signal": signal,
            "risk": risk,
        })
#
        return

    qty = int(risk["max_qty"])
    if qty <= 0:
        STATE.add_trade_log({
            "symbol": candle.symbol,
            "ts": candle.ts,
            "status": "zero_qty",
            "signal": signal,
            "risk": risk,
        })
        return

    order = broker.place_order(
        symbol=signal["symbol"],
#
        side=signal["action"],
        qty=qty,
    )

    STATE.trades_today += 1

    STATE.add_trade_log({
        "symbol": candle.symbol,
        "ts": candle.ts,
        "status": "placed",
        "signal": signal,
        "risk": risk,
        "order": order,
    })
#

14. WebSocket bootstrap

Your original KiteTicker idea is good. Wire it like this:

backend/app.py

import threading
import time
from datetime import datetime
from zoneinfo import ZoneInfo
from fastapi import FastAPI
from kiteconnect import KiteTicker
from backend.settings import settings
#
from backend.candle_builder import CandleBuilder
from backend.trading_engine import on_closed_candle
from backend.news_bias_worker import run_news_bias_loop
from backend.state import STATE

app = FastAPI()
builder = CandleBuilder(on_closed_candle=on_closed_candle)
IST = ZoneInfo(settings.tz)

# Example token map, replace with real instrument token mapping
TOKEN_TO_SYMBOL = {}

def process_ticks(ticks):
    now_epoch = int(time.time())
    for t in ticks:
#
        token = t["instrument_token"]
        symbol = TOKEN_TO_SYMBOL.get(token)
        if not symbol:
            continue

        ltp = float(t.get("last_price", 0))
        volume = float(t.get("volume_traded", 0))
        builder.on_tick(symbol=symbol, ltp=ltp, volume=volume, epoch_sec=now_epoch)

def start_ws():
    kws = KiteTicker(settings.kite_api_key, settings.kite_access_token)

    def on_ticks(ws, ticks):
        process_ticks(ticks)
#
    def on_connect(ws, response):
        tokens = list(TOKEN_TO_SYMBOL.keys())
        ws.subscribe(tokens)
        ws.set_mode(ws.MODE_FULL, tokens)

    kws.on_ticks = on_ticks
    kws.on_connect = on_connect
    kws.connect(threaded=True)

@app.on_event("startup")
def startup():
    threading.Thread(target=start_ws, daemon=True).start()
    threading.Thread(target=run_news_bias_loop, daemon=True).start()

@app.get("/health")
#
def health():
    return {"ok": True, "trades_today": STATE.trades_today, "pnl_today": STATE.realized_pnl_today}

@app.get("/log")
def log():
    return {"items": STATE.trade_log[-100:]}

Run it:

uvicorn backend.app:app --host 0.0.0.0 --port 9000

#

15. Optional REST endpoints matching your original workflow

If you want your original workflow shape preserved, expose these:

  • GET /market-data
  • POST /place-order
  • POST /log

But internally, I’d still keep the realtime engine event-driven.

Example shapes:

@app.get("/market-data")
def market_data():
    return {"message": "Use realtime candle-close engine, not polling, for 1m strategy"}
#

@app.post("/place-order")
def place_order(payload: dict):
    return broker.place_order(
        symbol=payload["symbol"],
        side=payload["side"],
        qty=int(payload["qty"]),
    )

@app.post("/log")
def write_log(payload: dict):
    STATE.add_trade_log(payload)
    return {"ok": True}

#

16. Indian market specific rules you should hard-code

I’d absolutely add these:

  • timezone = Asia/Kolkata
  • normal hours: 09:15 to 15:30 IST
  • ignore pre-open unless you explicitly support it
  • restrict symbols to NSE/BSE watchlist only
  • if using F&O, enforce lot sizes
  • reject stale data
  • reject duplicate candle timestamps
  • no order if news bias is stale
  • no order on reconnect gap until candle stream stabilized
  • daily reset at start of session

You can do daily reset with a tiny cron or just reset in backend when IST date changes.

#

17. Where news bias enters the trade

Use it in two places:

Trading agent

Adjust confidence and setup quality

Risk agent

Reduce size or hard block if bias strongly opposes the trade

Example:

  • candle says long breakout
  • news bias = -0.8
  • trading agent lowers confidence
  • risk agent blocks or cuts size sharply
#

That’s much better than blindly appending headlines to the signal prompt.


18. What I would not do

I would not:

  • send raw ticks to OpenClaw
  • let OpenClaw call Kite directly
  • let the model decide position size without backend caps
  • rely only on agent output for risk
  • use 5m polling for a 1m strategy
  • treat public news scraping as truly “realtime” without provider quality checks

19. Optional OpenClaw cron jobs that are actually useful

#

Realtime trigger should stay in Python, but OpenClaw cron is still good for:

  • morning market readiness check
  • midday risk summary
  • EOD trade summary
  • daily log compaction/reporting

Example EOD summary:

openclaw cron add \
  --name "trading-eod-summary" \
  --cron "35 15 * * 1-5" \
  --tz "Asia/Kolkata" \
  --session isolated \
  --agent risk \
#
  --message "Summarize today's trading log, risk rejects, wins, losses, and anomalies." \
  --no-deliver

20. Final blunt note

This is the right architecture for what you described.
If you want “OpenClaw first, websocket second”, I think that will break in annoying ways.

For Indian 1m automated trading with news bias, the sane split is:

  • Python backend = market plumbing + broker + hard risk
  • OpenClaw = signal, news interpretation, risk reasoning
#

If you want, next I can give you either:

  1. a single-file FastAPI prototype you can run today, or
  2. a production version with Postgres + Redis + better news provider adapters.
tall glade
#

Production version

thin trellisBOT
tall glade
#

Give me production version postgres+redis+ better news provider adapters

heavy jacinthBOT
#

Yep. Here’s the production shape I’d use for postgres + redis + better news adapters.

This is the version I’d actually trust more than the single-process prototype.


Production architecture

Core split

  • Python backend owns:
    • Kite websocket
    • candle building
    • news ingest
    • Postgres writes
    • Redis streams/cache/locks
    • final order execution
#
  • hard risk enforcement

  • OpenClaw owns:

    • news agent
    • trading agent
    • risk agent
    • optional EOD summaries / operator chat

Event flow

KiteTicker
  -> market-streamer
  -> 1m candle close
  -> Redis Stream: stream:candles:1m
  -> signal-orchestrator
#
       -> load latest news bias from Redis/Postgres
       -> call OpenClaw trading agent
       -> call OpenClaw risk agent
       -> persist signal/risk decision
       -> enqueue order intent
  -> execution-worker
       -> final hard checks
       -> place Kite order
       -> persist order/fill/audit

News providers
  -> news-ingestor
  -> Postgres headlines
  -> Redis Stream: stream:news:raw
  -> news-scorer
#
       -> group recent headlines by symbol
       -> call OpenClaw news agent
       -> update Postgres news_bias + Redis cache

Recommended services

services/
  market-streamer      # KiteTicker -> candles -> Redis stream + Postgres
  news-ingestor        # provider adapters -> normalized headlines
  news-scorer          # headlines -> OpenClaw news agent -> bias cache
  signal-orchestrator  # candle-close event -> trading/risk agents
#
  execution-worker     # order intent -> Kite execution
  api                  # FastAPI health/admin/logs
  openclaw-gateway     # persistent OpenClaw runtime
  postgres
  redis

Tech stack

  • Python 3.12
  • FastAPI
  • SQLAlchemy + psycopg
  • redis-py
  • Postgres 16
#
  • Redis 7
  • OpenClaw gateway on same private network
  • optional: Nginx/Tailscale for admin access only

Postgres schema

I’d keep these tables.

1) instruments

create table instruments (
  symbol text primary key,
  exchange text not null,
#
  segment text not null,
  instrument_token bigint unique,
  isin text,
  company_name text,
  lot_size integer,
  tick_size numeric(18,8),
  active boolean not null default true
);

2) candles_1m

create table candles_1m (
  symbol text not null references instruments(symbol),
#
  ts timestamptz not null,
  open numeric(18,8) not null,
  high numeric(18,8) not null,
  low numeric(18,8) not null,
  close numeric(18,8) not null,
  volume numeric(20,4) not null default 0,
  source text not null default 'kite',
  primary key (symbol, ts)
);

create index candles_1m_symbol_ts_desc on candles_1m(symbol, ts desc);

3) headlines

#
create table headlines (
  id bigserial primary key,
  provider text not null,
  external_id text,
  url text,
  title text not null,
  body text,
  published_at timestamptz not null,
  received_at timestamptz not null default now(),
  language text,
  raw jsonb not null,
  unique (provider, external_id)
);

create index headlines_published_at_desc on headlines(published_at desc);
#

4) headline_symbols

create table headline_symbols (
  headline_id bigint not null references headlines(id) on delete cascade,
  symbol text not null references instruments(symbol),
  confidence numeric(6,4) not null default 1.0,
  primary key (headline_id, symbol)
);

5) news_bias

create table news_bias (
  id bigserial primary key,
#
  symbol text not null references instruments(symbol),
  as_of timestamptz not null,
  bias numeric(6,4) not null,          -- -1 to +1
  confidence numeric(6,4) not null,    -- 0 to 1
  summary text not null,
  source_count integer not null,
  expires_at timestamptz not null,
  raw jsonb not null
);

create index news_bias_symbol_asof_desc on news_bias(symbol, as_of desc);

6) daily_risk_state

#
create table daily_risk_state (
  trading_day date primary key,
  capital numeric(18,2) not null,
  realized_pnl numeric(18,2) not null default 0,
  unrealized_pnl numeric(18,2) not null default 0,
  trades_count integer not null default 0,
  max_daily_loss numeric(18,2) not null,
  trading_blocked boolean not null default false,
  block_reason text,
  updated_at timestamptz not null default now()
);

7) signals

#
create table signals (
  id uuid primary key,
  cycle_id text not null unique,
  symbol text not null references instruments(symbol),
  candle_ts timestamptz not null,
  timeframe text not null,
  signal jsonb not null,
  created_at timestamptz not null default now()
);

create index signals_symbol_created_desc on signals(symbol, created_at desc);

8) risk_decisions

#
create table risk_decisions (
  id uuid primary key,
  signal_id uuid not null references signals(id) on delete cascade,
  approved boolean not null,
  decision jsonb not null,
  created_at timestamptz not null default now()
);

9) orders

create table orders (
  id uuid primary key,
  cycle_id text not null unique,
#
  signal_id uuid references signals(id),
  symbol text not null references instruments(symbol),
  side text not null,
  qty integer not null,
  status text not null,
  broker_order_id text,
  request jsonb not null,
  response jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index orders_symbol_created_desc on orders(symbol, created_at desc);

10) audit_events

#
create table audit_events (
  id bigserial primary key,
  event_type text not null,
  cycle_id text,
  symbol text,
  payload jsonb not null,
  created_at timestamptz not null default now()
);

Redis design

Use Redis for hot state, not long-term truth.

#

Streams

  • stream:candles:1m
  • stream:news:raw
  • stream:orders:intents
  • stream:orders:results

Keys

  • news:bias:{symbol} -> latest bias JSON
  • risk:day:{YYYY-MM-DD} -> quick mirror of daily state
  • lock:order:{cycle_id} -> idempotent execution lock
  • health:{service} -> heartbeat info

Consumer groups

  • cg:signals on stream:candles:1m
#
  • cg:news-score on stream:news:raw
  • cg:orders-exec on stream:orders:intents

This gives you replay, ack, and failure recovery without inventing your own queue.


Better news provider adapters

For Indian markets, I’d rank providers like this:

Tier 1, highest trust

  • NSE corporate announcements
  • BSE corporate announcements
  • exchange filings / official disclosures

These matter more than media commentary.

#

Tier 2, very useful

  • paid/newswire provider if you have one
  • broker research/news APIs

Tier 3, useful but lower trust weight

  • Economic Times Markets
  • Moneycontrol
  • Business Standard Markets
  • Mint Markets
  • RSS sources where allowed

I would not let random web scraping dominate bias.

Adapter interface

#
from typing import Protocol

class NewsAdapter(Protocol):
    name: str
    def fetch(self, since_ts) -> list[dict]:
        ...

Normalized headline shape

{
  "provider": "moneycontrol",
  "external_id": "abc123",
  "url": "https://...",
#
  "title": "Infosys Q4 beats estimates",
  "body": "optional",
  "published_at": "2026-04-20T11:20:00+05:30",
  "language": "en",
  "raw": {...}
}

Symbol resolution

Do not rely only on ticker text in title.

Use:

  • alias map
  • company name map
  • ISIN map where possible
#
  • exchange filing metadata when available

Example:

  • "Reliance Industries" -> RELIANCE
  • "State Bank of India" -> SBIN

Source weighting

Keep weights in config:

SOURCE_WEIGHTS = {
    "nse_filings": 1.00,
    "bse_filings": 1.00,
    "paid_wire": 0.95,
    "economic_times": 0.75,
#
    "moneycontrol": 0.70,
    "business_standard": 0.65,
    "mint": 0.65,
}

Bias freshness

Bias should expire quickly.

  • exchange filing shock: 30 to 60 min
  • normal media headlines: 10 to 20 min
  • old bias should decay to neutral

#

News scoring flow

Ingest

  • poll adapters every 15 to 30 seconds
  • normalize
  • dedupe on (provider, external_id) or URL hash
  • write headlines
  • resolve symbols
  • write headline_symbols
  • XADD stream:news:raw

Score

Every 30 to 60 seconds, news-scorer:

  • loads unscored recent headlines by symbol
  • batches per symbol
  • calls OpenClaw news agent once per symbol batch
  • stores:
#
  • Postgres news_bias
    • Redis news:bias:{symbol}

That keeps agent traffic sane.


OpenClaw integration in prod

I’d keep OpenClaw on the same host or same private LAN.

Use 3 agents:

  • news
  • trading
  • risk

Python workers call:

#
openclaw agent --agent news --message '...'
openclaw agent --agent trading --message '...'
openclaw agent --agent risk --message '...'

Do not spawn fresh ad hoc agents per event. Use persistent configured agents.

Production wrapper

Wrap calls with:

  • timeout
  • retry once on transient failure
  • strict JSON extraction
  • audit logging

Pseudo-wrapper:

#
def call_openclaw(agent_id: str, payload: dict, timeout_sec: int = 30) -> dict:
    ...

News agent can have a longer timeout than trading/risk, because news is off the hot path.


Signal orchestration flow

On each closed 1m candle:

  1. read candle from Redis stream
  2. build cycle_id = "{symbol}:{candle_ts}"
  3. acquire Redis execution lock for cycle
  4. load latest news:bias:{symbol}
#
  1. load daily_risk_state
  2. call trading agent
  3. call risk agent
  4. write signals and risk_decisions
  5. if approved, XADD stream:orders:intents
  6. write audit_events

This is where OpenClaw sits in the loop.


Final execution flow

execution-worker should do one last hard check even after risk agent says yes.

Checks:

  • market hours valid for India
#
  • daily block flag not set
  • trades count < max
  • remaining daily loss > 0
  • no duplicate cycle_id
  • symbol active
  • qty > 0
  • broker session healthy

Then place order with Kite and persist result.

OpenClaw should never be the last line of defense.


Risk rules with your numbers

Your current config still conflicts:

#
  • capital = 3000
  • risk_per_trade = 20% = 600
  • max_daily_loss = 15% = 450

So prod backend must enforce:

effective_trade_risk = min(600, remaining_daily_loss)

I’d be blunt here, I would not ship 20% per trade live.
Even if backend caps it, that config is still too aggressive for unattended execution.


Docker Compose shape

#

I’d run something like this:

version: "3.9"

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: trader
      POSTGRES_USER: trader
      POSTGRES_PASSWORD: trader
    ports: ["5432:5432"]
    volumes:
      - pgdata:/var/lib/postgresql/data
#
  redis:
    image: redis:7
    ports: ["6379:6379"]

  openclaw:
    image: ghcr.io/openclaw/openclaw:latest
    volumes:
      - ./openclaw:/workspace
      - ~/.openclaw:/root/.openclaw
    ports: ["18789:18789"]

  api:
    build: .
    command: uvicorn backend.api:app --host 0.0.0.0 --port 9000
    env_file: .env
#
    depends_on: [postgres, redis, openclaw]

  market-streamer:
    build: .
    command: python -m backend.market_streamer
    env_file: .env
    depends_on: [postgres, redis]

  news-ingestor:
    build: .
    command: python -m backend.news_ingestor
    env_file: .env
    depends_on: [postgres, redis]

  news-scorer:
#
    build: .
    command: python -m backend.news_scorer
    env_file: .env
    depends_on: [postgres, redis, openclaw]

  signal-orchestrator:
    build: .
    command: python -m backend.signal_orchestrator
    env_file: .env
    depends_on: [postgres, redis, openclaw]

  execution-worker:
    build: .
    command: python -m backend.execution_worker
    env_file: .env
#
    depends_on: [postgres, redis]

volumes:
  pgdata:

Not magic, but good enough to start.


Operational rules I’d add

Health

  • every service publishes health:{service} heartbeat to Redis
  • API exposes consolidated /health
#

Dead letter handling

  • failed Redis stream messages go to stream:dead:*
  • alert on retries > N

Audit

Everything important writes audit_events:

  • candle received
  • signal created
  • risk rejected
  • order intent
  • order placed
  • broker error

Daily reset

At IST day rollover:

  • reset daily_risk_state
  • archive prior day summary
#
  • clear stale locks

Backfill / reconnect

On websocket reconnect:

  • backfill the last missing 1m candles from broker/historical endpoint
  • mark reconnect gap in audit
  • suspend trading until candle stream is consistent again

News bias logic I’d actually use

Don’t just pass headlines blindly.

Compute bias with:

  • source trust weight
  • recency decay
#
  • dedupe
  • headline clustering
  • symbol relevance confidence

Then pass the agent:

  • top 3 to 10 recent headlines
  • source names
  • prior bias if still valid

And store:

{
  "symbol": "RELIANCE",
  "bias": -0.42,
  "confidence": 0.78,
#
  "summary": "Mixed headlines, but latest exchange disclosure is mildly negative",
  "expires_at": "2026-04-20T12:15:00+05:30"
}

That becomes input to trading and risk.


What I would deploy first

Phase 1

  • Postgres
  • Redis
  • Kite stream
  • closed-candle stream
#
  • trading + risk agents
  • execution worker
  • no news yet

Phase 2

  • add exchange filing adapters
  • add one media adapter
  • add news agent scoring
  • use bias only as size reducer, not hard blocker

Phase 3

  • add paid provider if needed
  • better dashboards
  • better anomaly alerts
  • operator override UI

That rollout is much safer.

#

If you want, next I can give you the actual production file skeleton for:

  1. signal_orchestrator.py
  2. news_adapter implementations
  3. SQLAlchemy models + migrations
tall glade
#

There is a kite mcp server. Can i use it and ask ai to follow rules and do trading for me?

heavy jacinthBOT
#

Technically, yes. Practically, for live money, not the way you phrased it.

You can use a Kite MCP server in the stack, but I would not do:

“give the AI direct Kite access and trust prompt rules”

That’s too fragile.

The safe answer

Use Kite MCP in read-only mode for:

  • quotes
  • positions
  • holdings
  • margins
  • orders history
#

Then keep actual order placement behind a deterministic backend/execution worker with hard rules.

What I recommend

Option 1, recommended

  • AI reads via Kite MCP
  • backend executes via Kite SDK/API
  • backend enforces:
    • symbol allowlist
    • max qty
    • max trades/day
    • max daily loss
    • market hours
    • duplicate cycle_id protection
    • kill switch

This is the one I’d ship.

#

Option 2, okay for paper trading

  • AI reads and places through Kite MCP
  • but only in paper mode or with manual approval

Good for testing agent behavior.

Option 3, not recommended

  • AI has unrestricted live Kite MCP tools
  • prompt says “follow the rules”

I wouldn’t trust that in production.


Important OpenClaw nuance

OpenClaw can store outbound MCP server definitions under mcp.servers, using:

#
openclaw mcp set kite '{"command":"python","args":["/srv/kite-mcp/server.py"]}'

or remote transport:

openclaw mcp set kite '{"url":"https://mcp.example.com/stream","transport":"streamable-http"}'

Docs: https://docs.openclaw.ai/cli/mcp

But the docs are explicit about this part:

  • openclaw mcp set stores config only
  • it does not validate the server
  • whether a runtime actually consumes that MCP server depends on the runtime
#

So yes, MCP fits, but it is not “magic trading mode”.


My strong recommendation for your trading engine

Split broker access into two layers:

kite_read surface

Allowed to AI:

  • quote lookup
  • positions
  • holdings
  • funds/margins
  • order history

execution surface

#

Not directly exposed to the trading agent.
Only your backend/execution worker can call it after:

  • risk agent says yes
  • backend math says yes
  • lock/idempotency check passes

That gives you:

Kite MCP read → AI signal/risk → backend final gate → Kite execution


Why prompt rules are not enough

Even a good agent can:

  • drift after long context
  • misread stale state
#
  • retry the wrong thing
  • call the wrong tool at the wrong time
  • fail on malformed broker/tool responses

For live trading, rules must be enforced in code, not only in the prompt.


What I’d do if you insist on MCP

Use Kite MCP for:

  • market/account context
  • maybe paper order simulation

Use backend SDK/API for:

  • real order placement
  • state updates
#
  • final limits

That gives you the upside of MCP without letting the model directly own your capital.

If you want, I can sketch the exact safe architecture with Kite MCP included, meaning:

  1. which tools the AI can see,
  2. which tools stay backend-only, and
  3. the exact order approval flow.
tall glade
#

Yes

heavy jacinthBOT
#

Perfect. Here’s the safe Kite MCP architecture I’d use.

Safe architecture with Kite MCP

1. What the AI can see

Expose only read-only broker/data tools to the agents.

AI-visible tools

  • kite_read.get_quotes
  • kite_read.get_positions
  • kite_read.get_holdings
  • kite_read.get_margins
  • kite_read.get_orders
  • market_state.get_latest_closed_candle
  • market_state.get_recent_candles
  • news_state.get_symbol_bias
#
  • risk_state.get_daily_snapshot

These are safe because they inspect, not execute.


2. What the AI should NOT see

Keep these backend-only:

Backend-only tools

  • kite_execute.place_order
  • kite_execute.modify_order
  • kite_execute.cancel_order
  • risk_admin.set_block
  • risk_admin.reset_day
  • state.write_trade_log
#
  • state.update_daily_risk
  • state.acquire_cycle_lock

This is the key boundary.

The agent proposes.
The backend decides.
The broker executes.


3. Agent roles

trading_agent

Input:

  • latest closed 1m candle
  • last N candles
#
  • positions
  • margins
  • news bias

Output:

{
  "action": "buy|sell|hold",
  "symbol": "RELIANCE",
  "confidence": 0.78,
  "entry_type": "market",
  "entry_price": 0,
  "stop_loss": 12.5,
  "take_profit": 25.0,
  "size_hint": 1,
  "reason": "short explanation"
#
}

risk_agent

Input:

  • trading signal
  • daily PnL
  • trades today
  • remaining daily loss
  • news bias
  • broker/account state

Output:

{
#
  "approved": true,
  "max_qty": 1,
  "risk_dollars": 150,
  "blocks": [],
  "reason": "fits daily rules"
}

4. Exact order approval flow

1. KiteTicker builds closed 1m candle
2. backend creates cycle_id = symbol + candle_ts
#
3. backend acquires Redis lock for cycle_id
4. backend loads latest news bias + account state
5. backend calls trading_agent
6. backend calls risk_agent
7. backend validates hard rules again in code
8. if valid, backend sends order to kite_execute
9. backend writes Postgres logs + audit event
10. backend updates daily risk state

5. Hard backend rules

These must run after the risk agent too:

#
  • symbol in allowlist
  • market hours valid for India
  • trades_today < MAX_TRADES_PER_DAY
  • remaining_daily_loss > 0
  • requested_risk <= remaining_daily_loss
  • no duplicate cycle_id
  • kill switch not enabled
  • broker session healthy
  • qty > 0
  • candle is closed, not live

If any fail, reject even if the AI said “approved”.


6. Recommended service split

#

OpenClaw side

  • news_agent
  • trading_agent
  • risk_agent

Python/backend side

  • market-streamer
  • news-ingestor
  • news-scorer
  • signal-orchestrator
  • execution-worker
  • postgres
  • redis

Broker access split

  • kite_read via MCP, safe for agents
  • kite_execute via backend only
#

That’s the cleanest setup.


7. Live vs paper mode

Paper mode

You can let the AI go end-to-end through a simulated executor.

Live mode

I’d require:

  • backend-only execution
  • strict locks/idempotency
  • daily block flag
  • operator override
  • full audit log
#

8. My final recommendation

Yes, use the Kite MCP server.
But use it as a read-only context layer, not as unrestricted live execution.

Best production path

  • AI reads via Kite MCP
  • backend enforces rules
  • backend executes via restricted execution service

That gives you the benefits of AI without handing it raw broker control.

If you want, I can do the next step and give you the exact tool contract names + Redis/Postgres flow + agent prompts for this MCP-safe version.

tall glade
#

Gove me full setup for paper mode only

heavy jacinthBOT
#

Good call. Paper mode only is the right place to start.

Here’s the full setup I’d use for a paper-only OpenClaw trading engine.


1. Final paper-mode architecture

KiteTicker / market-data source
  -> candle builder
  -> Redis stream: closed 1m candles
  -> signal orchestrator
       -> OpenClaw news_agent
       -> OpenClaw trading_agent
       -> OpenClaw risk_agent
#
       -> paper executor
  -> Postgres
  -> dashboard / logs

Important difference from live mode

  • No live broker write access
  • No Kite order placement
  • only:
    • market data
    • optional read-only positions/margins
    • simulated fills

2. Services

#

For paper mode, keep these:

  • openclaw
  • postgres
  • redis
  • market-streamer
  • news-ingestor
  • news-scorer
  • signal-orchestrator
  • paper-executor
  • api

Skip:

  • live execution worker
  • broker write service
  • any direct place_order
#

3. OpenClaw agents

Create 3 agents:

openclaw agents add news --workspace /srv/trader/openclaw/workspace-news
openclaw agents add trading --workspace /srv/trader/openclaw/workspace-trading
openclaw agents add risk --workspace /srv/trader/openclaw/workspace-risk

news agent

Returns:

#
{
  "symbol": "RELIANCE",
  "bias": -0.3,
  "confidence": 0.7,
  "summary": "mildly negative"
}

trading agent

Returns:

{
  "action": "buy|sell|hold",
  "symbol": "RELIANCE",
#
  "confidence": 0.76,
  "stop_loss": 12.5,
  "take_profit": 25.0,
  "size_hint": 1,
  "reason": "breakout + positive bias"
}

risk agent

Returns:

{
  "approved": true,
  "max_qty": 1,
#
  "risk_dollars": 120,
  "blocks": [],
  "reason": "within paper-mode limits"
}

4. Config

.env

MODE=paper
#
CAPITAL=3000
RISK_PER_TRADE_PCT=0.20
MAX_DAILY_LOSS_PCT=0.15
MAX_TRADES_PER_DAY=10
TIMEFRAME_SEC=60
TZ=Asia/Kolkata

WATCHLIST=RELIANCE,TCS,INFY,HDFCBANK,ICICIBANK,SBIN

OPENCLAW_BIN=openclaw
NEWS_POLL_SEC=30

POSTGRES_DSN=postgresql://trader:trader@postgres:5432/trader
REDIS_URL=redis://redis:6379/0
#
KITE_API_KEY=xxx
KITE_ACCESS_TOKEN=xxx

5. Postgres tables

For paper mode, you only need these:

  • candles_1m
  • headlines
  • news_bias
  • signals
  • risk_decisions
  • paper_orders
#
  • paper_positions
  • daily_risk_state
  • audit_events

paper_orders

create table paper_orders (
  id uuid primary key,
  cycle_id text not null unique,
  symbol text not null,
  side text not null,
  qty integer not null,
  signal jsonb not null,
  risk jsonb not null,
  fill_price numeric(18,8),
#
  fill_ts timestamptz,
  status text not null,
  created_at timestamptz not null default now()
);

paper_positions

create table paper_positions (
  symbol text primary key,
  qty integer not null,
  avg_price numeric(18,8) not null,
  side text not null,
  updated_at timestamptz not null default now()
#
);

daily_risk_state

create table daily_risk_state (
  trading_day date primary key,
  capital numeric(18,2) not null,
  realized_pnl numeric(18,2) not null default 0,
  trades_count integer not null default 0,
  max_daily_loss numeric(18,2) not null,
  blocked boolean not null default false,
  updated_at timestamptz not null default now()
);
#

6. Redis use

Use Redis for:

  • stream:candles:1m
  • stream:news:raw
  • news:bias:{symbol}
  • lock:cycle:{symbol}:{ts}

Postgres is the source of truth.
Redis is just hot path + coordination.


7. Paper executor rules

#

This is the most important piece.

Fill model

For paper mode, I’d simulate fills as:

  • market buy → next candle open + slippage
  • market sell → next candle open - slippage

Example slippage

slippage_bps = 2 to 5

So if next candle open is 100.00:

  • buy fill = 100.02 at 2 bps
  • sell fill = 99.98 at 2 bps
#

Exit model

Check each new candle against:

  • stop loss
  • take profit
  • manual close rule
  • session close cutoff

Daily block rules

Before creating a paper order:

  • trades_today < MAX_TRADES_PER_DAY
  • remaining_daily_loss > 0
  • no duplicate cycle_id
  • symbol in allowlist
  • candle is closed

#

8. Signal flow

Step-by-step

1. Market streamer

  • subscribe to KiteTicker
  • build 1m candles
  • when candle closes:
    • write to Postgres
    • XADD stream:candles:1m

2. News scorer

  • pull headlines every 30s
  • map headlines to Indian symbols
  • call OpenClaw news agent
  • store latest bias in Postgres + Redis
#

3. Signal orchestrator

On each candle-close event:

  • acquire cycle_id lock
  • load latest candle
  • load latest news bias
  • load current paper position
  • load daily risk state
  • call trading agent
  • call risk agent
  • if approved, insert paper_orders with status='pending_fill'

4. Paper executor

When next candle appears:

  • fill pending orders at simulated price
  • update paper_positions
  • update daily_risk_state
  • write audit_events
#

9. Recommended paper-mode rule changes

I’m still worried about your risk sizing, even in paper mode.

With your rules:

  • CAPITAL = 3000
  • RISK_PER_TRADE = 20% = 600
  • MAX_DAILY_LOSS = 15% = 450

That means one loss can exceed your daily budget.

So even in paper mode I’d enforce:

#
effective_trade_risk = min(
  capital * risk_per_trade_pct,
  remaining_daily_loss
)

So the engine caps it automatically.


10. News for Indian market

For paper mode, use:

  • NSE/BSE filings
  • Economic Times markets
  • Moneycontrol
#
  • Mint / Business Standard if you want more coverage

Bias should be:

  • cached per symbol
  • decayed over time
  • expired after 10 to 30 minutes

Do not let one stale headline dominate all day.


11. Docker Compose

version: "3.9"
#
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: trader
      POSTGRES_USER: trader
      POSTGRES_PASSWORD: trader
    ports: ["5432:5432"]

  redis:
    image: redis:7
    ports: ["6379:6379"]

  openclaw:
    image: ghcr.io/openclaw/openclaw:latest
#
    ports: ["18789:18789"]
    volumes:
      - ./openclaw:/workspace
      - ~/.openclaw:/root/.openclaw

  api:
    build: .
    command: uvicorn backend.api:app --host 0.0.0.0 --port 9000
    env_file: .env
    depends_on: [postgres, redis, openclaw]

  market-streamer:
    build: .
    command: python -m backend.market_streamer
    env_file: .env
#
    depends_on: [postgres, redis]

  news-ingestor:
    build: .
    command: python -m backend.news_ingestor
    env_file: .env
    depends_on: [postgres, redis]

  news-scorer:
    build: .
    command: python -m backend.news_scorer
    env_file: .env
    depends_on: [postgres, redis, openclaw]

  signal-orchestrator:
#
    build: .
    command: python -m backend.signal_orchestrator
    env_file: .env
    depends_on: [postgres, redis, openclaw]

  paper-executor:
    build: .
    command: python -m backend.paper_executor
    env_file: .env
    depends_on: [postgres, redis]

12. What AI is allowed to access in paper mode

#

Safe to expose

  • latest closed candles
  • recent candles
  • paper positions
  • daily PnL snapshot
  • news bias
  • read-only market/account context

Do not expose

  • anything that can place a real order
  • anything that mutates broker state

That way paper mode stays paper mode.


13. What I’d test before trusting results

#
  • duplicate candle handling
  • websocket reconnect gaps
  • stale news bias expiry
  • duplicate cycle_id protection
  • daily reset at IST day boundary
  • stop loss / take profit simulation
  • slippage assumptions
  • mismatch between news timestamp and candle timestamp

If those are wrong, the paper results will lie to you.


14. My recommendation

This is the clean paper stack:

#
  • Kite data only
  • OpenClaw for news/signal/risk
  • paper executor only
  • Postgres + Redis
  • no live order write path at all

That gives you a realistic test harness without the scary part.

If you want, next I can give you the exact paper-mode file skeleton for:

  1. signal_orchestrator.py
  2. paper_executor.py
  3. the AGENTS.md files for all 3 agents