QuantHive / agents /trader.py
ARKAISW's picture
Hackathon Final Submission: PettingZoo multi-agent arch, GRPO training, docs
9cb3002
Raw
History Blame Contribute Delete
5.07 kB
"""
Quant Trader (Executor Agent).
Reads state + signals + constraints β†’ outputs actions + reasoning.
"""
import numpy as np
from typing import Dict, Tuple, Any
from policy.local_model import LocalPolicyModel
class QuantTrader:
"""
Execution agent that decides final trade direction and size
based on signals from Researcher/FA and constraints from Risk Model.
"""
def __init__(self, aggression: float = 0.5):
self.name = "QuantTrader"
self.aggression = aggression
self.policy = LocalPolicyModel()
def __call__(
self,
observation: np.ndarray,
researcher_data: Tuple[str, float, str],
fa_data: Tuple[float, str],
risk_data: Tuple[float, Dict, str],
) -> Tuple[int, float, float, float, str]:
"""
Decide action based on signals and reasoning.
Returns:
(direction, size, sl, tp, reasoning)
"""
signal, confidence, researcher_reasoning = researcher_data
fa_sentiment, fa_reasoning = fa_data
position_limit, constraints, risk_reasoning = risk_data
current_exposure = float(observation[15]) if len(observation) > 15 else 0.0
short_exposure = float(observation[18]) if len(observation) > 18 else 0.0
raw_price = constraints.get("raw_price")
sl_ratio = constraints.get("suggested_sl_ratio", 0.02)
# --- Format signals for the policy model ---
if signal == "bullish":
ta_score = confidence
elif signal == "bearish":
ta_score = -confidence
else:
ta_score = 0.0
signals = {
"raw_state": observation.tolist() if hasattr(observation, "tolist") else observation,
"ta_score": ta_score,
"fa_sentiment": (fa_sentiment * 2.0) - 1.0, # Map back to [-1, 1]
"position_limit": position_limit,
"constraints": constraints,
"text_context": {
"researcher": researcher_reasoning,
"fundamental": fa_reasoning,
"risk": risk_reasoning
}
}
# --- Delegate to policy (Fine-tuned model will use text_context) ---
direction, size = self.policy.predict(observation, signals)
# ═══════════════════════════════════════════════════
# STRICT 1% RISK RULE + FIXED 2:1 RRR
# ═══════════════════════════════════════════════════
# Max loss per trade = 1% of current portfolio value.
# RRR is fixed at 2:1 (TP distance = 2 Γ— SL distance).
sl = 0.0
tp = 0.0
RISK_PER_TRADE = 0.01 # 1% of portfolio
REWARD_RISK_RATIO = 2.0 # Fixed 2:1 RRR
reasoning = f"Trader executing {['HOLD', 'BUY', 'SELL/SHORT'][direction]} based on signals. Risk check: {risk_reasoning}."
if raw_price is not None and raw_price > 0 and direction != 0:
# SL distance from ATR-based ratio (from risk model)
sl_distance = raw_price * sl_ratio # e.g. 2% of price
if direction == 1: # BUY / Cover short
sl = raw_price - sl_distance
tp = raw_price + (sl_distance * REWARD_RISK_RATIO)
elif direction == 2: # SELL long / Open short
sl = raw_price + sl_distance
tp = raw_price - (sl_distance * REWARD_RISK_RATIO)
# ── 1% Risk Position Sizing ──
# If SL is hit, loss = qty Γ— sl_distance
# We want: qty Γ— sl_distance ≀ portfolio_value Γ— 0.01
# So: qty ≀ (portfolio_value Γ— 0.01) / sl_distance
# And: size = qty Γ— price / portfolio_value
# β†’ size ≀ (0.01 Γ— price) / (sl_distance Γ— (1 + commission))
# Simplified: size = RISK_PER_TRADE / (sl_ratio Γ— (1 + 0.001))
max_risk_size = RISK_PER_TRADE / (sl_ratio + 1e-10)
# ── Volatility-Adjusted Target Sizing ──
# Target a constant volatility layout. If market vol is 2x normal, halve position.
current_volatility = float(observation[12]) if len(observation) > 12 else 0.015
vol_target = 0.015 # Benchmark normal volatility
vol_scalar = np.clip(vol_target / max(current_volatility, 1e-4), 0.2, 1.5)
size = min(size * vol_scalar, max_risk_size, position_limit)
reasoning = (
f"Trader executing {['HOLD', 'BUY', 'SELL/SHORT'][direction]} | "
f"Vol Adjusted (x{vol_scalar:.2f}), Size: {size:.1%} of portfolio | "
f"SL: {sl:.2f}, TP: {tp:.2f} (2:1 RRR) | "
f"{risk_reasoning}"
)
else:
size = min(size, position_limit)
size = float(np.clip(size, 0.0, 1.0))
return direction, size, sl, tp, reasoning