indicators-env / env /indicators_env_client.py
bawsi99's picture
Final clean submission
3a7314c
Raw
History Blame Contribute Delete
7.32 kB
"""
indicators_env_client.py β€” Typed sync/async client for IndicatorsEnv.
Mirrors the openenv-core EnvClient pattern so it works seamlessly with
TRL's GRPOTrainer and the openenv course module interface.
Usage (sync):
from indicators_env_client import IndicatorsEnvClient, IndicatorsAction
with IndicatorsEnvClient(base_url="http://localhost:8000").sync() as env:
obs = env.reset()
result = env.step(IndicatorsAction(direction="Bullish", conviction=0.8))
print(result.reward)
Usage (async):
async with IndicatorsEnvClient(base_url="http://localhost:8000") as env:
obs = await env.reset()
result = await env.step(IndicatorsAction(direction="Bullish", conviction=0.8))
"""
from __future__ import annotations
import asyncio
import contextlib
import json
from dataclasses import dataclass
from typing import Any, Dict, Optional
import httpx
import websockets
# ─── Data classes (mirror server schemas) ────────────────────────────────────
@dataclass
class IndicatorsAction:
direction: str # "Bullish" | "Bearish" | "Neutral"
conviction: float = 0.5
@dataclass
class IndicatorsObservation:
symbol: str
date: str
term: str
current_price: float
indicators: Dict[str, Any]
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "IndicatorsObservation":
return cls(
symbol=d["symbol"],
date=d["date"],
term=d["term"],
current_price=d["current_price"],
indicators=d["indicators"],
)
def to_prompt(self) -> str:
"""Render observation as a structured LLM prompt string."""
ind = self.indicators
ma = ind.get("moving_averages", {})
rsi = ind.get("rsi", {})
mac = ind.get("macd", {})
bb = ind.get("bollinger_bands", {})
adx = ind.get("adx", {})
vol = ind.get("enhanced_volume", {})
vlt = ind.get("volatility", {})
sto = ind.get("stochastic", {})
piv = ind.get("pivot_points", {})
return f"""[TERM: {self.term}]
Stock: {self.symbol} | Date: {self.date} | Price: {self.current_price}
MOVING AVERAGES
SMA20={ma.get('sma_20')} | SMA50={ma.get('sma_50')} | SMA200={ma.get('sma_200')}
EMA20={ma.get('ema_20')} | EMA50={ma.get('ema_50')}
Signal: {ma.get('signal')} | Golden Cross: {ma.get('golden_cross')} | Death Cross: {ma.get('death_cross')}
MOMENTUM
RSI14={rsi.get('rsi_14')} ({rsi.get('status')}) | Trend: {rsi.get('trend')}
MACD={mac.get('macd_line')} | Signal={mac.get('signal_line')} | Hist={mac.get('histogram')} | {mac.get('signal')} | Crossover: {mac.get('crossover')}
Stochastic K={sto.get('k')} D={sto.get('d')} ({sto.get('signal')})
VOLATILITY / BANDS
BB Upper={bb.get('upper')} | Mid={bb.get('middle')} | Lower={bb.get('lower')}
%B={bb.get('percent_b')} | Bandwidth={bb.get('bandwidth')} | Squeeze={bb.get('squeeze')}
ATR={vlt.get('atr_14')} | Vol Regime={vlt.get('regime')} | Vol Ratio={vlt.get('volatility_ratio')}
TREND
ADX={adx.get('adx')} ({adx.get('trend_strength')}) | +DI={adx.get('plus_di')} | -DI={adx.get('minus_di')} | Direction: {adx.get('trend_direction')}
VOLUME
VWAP={vol.get('vwap')} | Price vs VWAP={vol.get('price_vs_vwap_pct')}%
MFI={vol.get('mfi')} ({vol.get('mfi_status')}) | CMF={vol.get('cmf')} ({vol.get('cmf_signal')})
OBV Trend={ind.get('volume', {}).get('obv_trend')} | Volume Ratio={ind.get('volume', {}).get('volume_ratio')}x
A/D Line Trend={vol.get('ad_line_trend')}
PIVOT POINTS
R2={piv.get('r2')} | R1={piv.get('r1')} | Pivot={piv.get('pivot')} | S1={piv.get('s1')} | S2={piv.get('s2')}
Based on the above technical indicators, predict the {self.term}-term direction.
Respond ONLY with a JSON object in this exact format:
{{"direction": "Bullish" | "Bearish" | "Neutral", "conviction": <float 0.0-1.0>}}"""
@dataclass
class StepResult:
observation: Optional[IndicatorsObservation]
reward: float
done: bool
info: Dict[str, Any]
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "StepResult":
obs_d = d.get("observation")
return cls(
observation=IndicatorsObservation.from_dict(obs_d) if obs_d else None,
reward=d["reward"],
done=d["done"],
info=d.get("info", {}),
)
@dataclass
class ResetResult:
observation: IndicatorsObservation
info: Dict[str, Any]
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "ResetResult":
return cls(
observation=IndicatorsObservation.from_dict(d["observation"]),
info=d.get("info", {}),
)
# ─── Async client ─────────────────────────────────────────────────────────────
class IndicatorsEnvClient:
"""Async client for IndicatorsEnv using WebSocket for step() calls."""
def __init__(self, base_url: str = "http://localhost:8000", term: str = "medium"):
self.base_url = base_url.rstrip("/")
self.term = term
self._ws_url = self.base_url.replace("http://", "ws://").replace("https://", "wss://") + "/ws"
self._ws = None
self._http: Optional[httpx.AsyncClient] = None
async def __aenter__(self):
self._http = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)
self._ws = await websockets.connect(f"{self._ws_url}?term={self.term}")
return self
async def __aexit__(self, *args):
if self._ws:
await self._ws.close()
if self._http:
await self._http.aclose()
async def reset(self) -> ResetResult:
await self._ws.send(json.dumps({"method": "reset"}))
raw = await self._ws.recv()
return ResetResult.from_dict(json.loads(raw))
async def step(self, action: IndicatorsAction) -> StepResult:
await self._ws.send(json.dumps({
"method": "step",
"action": {"direction": action.direction, "conviction": action.conviction},
}))
raw = await self._ws.recv()
return StepResult.from_dict(json.loads(raw))
async def state(self) -> Dict[str, Any]:
await self._ws.send(json.dumps({"method": "state"}))
raw = await self._ws.recv()
return json.loads(raw)
def sync(self) -> "_SyncWrapper":
return _SyncWrapper(self)
class _SyncWrapper:
"""Synchronous context manager wrapper (mirrors openenv-core .sync() pattern)."""
def __init__(self, client: IndicatorsEnvClient):
self._client = client
self._loop = asyncio.new_event_loop()
def __enter__(self):
self._loop.run_until_complete(self._client.__aenter__())
return self
def __exit__(self, *args):
self._loop.run_until_complete(self._client.__aexit__(*args))
self._loop.close()
def reset(self) -> ResetResult:
return self._loop.run_until_complete(self._client.reset())
def step(self, action: IndicatorsAction) -> StepResult:
return self._loop.run_until_complete(self._client.step(action))
def state(self) -> Dict[str, Any]:
return self._loop.run_until_complete(self._client.state())