SPB Git

spb/wp25_uqo

Public
8.4 KB · 163 lines python
1"""Agent policies for Model C. A policy maps an observation to a *proposed* action; the simulation validates and clips2it against the ledger (cash, holdings, margin). Policies never touch the ledger.34Observation (dict): price, dividend, rf, signal (forecast of next dividend innovation, already Bayes-weighted into5`expected_dividend`), own cash/holdings/loan/equity, margin, history (last prices), target (for Bayesian agents),6own_valuation, period.78Action (dict): side ('buy'|'sell'|'hold'), quantity (>=0), order_type ('limit'|'market'), limit_price, borrow (>=0),9rationale (string; ignored by accounting).10"""11from __future__ import annotations1213import json1415import numpy as np1617ACTION_SCHEMA = {18    "type": "object",19    "properties": {20        "side": {"type": "string", "enum": ["buy", "sell", "hold"]},21        "quantity": {"type": "number", "minimum": 0},22        "order_type": {"type": "string", "enum": ["limit", "market"]},23        "limit_price": {"type": "number", "minimum": 0},24        "borrow": {"type": "number", "minimum": 0},25        "rationale": {"type": "string"},26    },27    "required": ["side", "quantity", "order_type"],28    "additionalProperties": True,29}303132# --------------------------------------------------------------------------- Bayesian mean-variance (control)33class BayesianMV:34    """h* = (E[p' + d'] - (1 + rf) p) / (gamma_a sigma^2); limit order halfway between price and valuation."""3536    def __init__(self, gamma_a: float, sigma2: float, p_anchor: float, aggressiveness: float = 0.5,37                 max_leverage: float = 0.0):38        self.gamma_a, self.sigma2, self.p_anchor = gamma_a, sigma2, p_anchor39        self.aggr = aggressiveness40        self.max_leverage = max_leverage4142    def valuation(self, obs):43        # next payoff = expected dividend + resale value; the resale value prices the persistence of the expected44        # dividend deviation: p_anchor + rho_d (E[d'] - dbar)/(1 + rf - rho_d)45        rho, dbar = obs.get('rho_d', 0.0), obs.get('dbar', obs['expected_dividend'])46        resale = self.p_anchor + rho * (obs['expected_dividend'] - dbar) / (1 + obs['rf'] - rho)47        return (obs['expected_dividend'] + resale) / (1 + obs['rf'])4849    def act(self, obs):50        p = obs['price']51        v = self.valuation(obs)52        h_star = (v * (1 + obs['rf']) - (1 + obs['rf']) * p) / (self.gamma_a * self.sigma2)53        h_star = max(h_star, 0.0)54        q = h_star - obs['holdings']55        side = 'buy' if q > 1e-9 else ('sell' if q < -1e-9 else 'hold')56        limit = p + self.aggr * (v - p) if side == 'buy' else p - self.aggr * (p - v)57        limit = max(limit, 1e-6)58        borrow = 0.059        if side == 'buy' and self.max_leverage > 0:60            need = abs(q) * limit - obs['cash']61            borrow = max(min(need, self.max_leverage * obs['equity']), 0.0)62        return dict(side=side, quantity=abs(q), order_type='limit', limit_price=limit, borrow=borrow,63                    rationale='bayesian mean-variance')646566# --------------------------------------------------------------------------- rule-based67class Momentum:68    def __init__(self, lookback=3, size=0.1):69        self.lb, self.size = lookback, size7071    def act(self, obs):72        hist = obs['history']73        if len(hist) <= self.lb:74            return dict(side='hold', quantity=0.0, order_type='limit', limit_price=obs['price'], borrow=0.0, rationale='warm-up')75        ret = hist[-1] / hist[-1 - self.lb] - 176        side = 'buy' if ret > 0 else 'sell'77        q = self.size * (obs['holdings'] if side == 'sell' else max(obs['cash'] / obs['price'], 0))78        return dict(side=side, quantity=q, order_type='market', limit_price=obs['price'], borrow=0.0, rationale='momentum')798081class Noise:82    def __init__(self, rng, size=0.05):83        self.rng, self.size = rng, size8485    def act(self, obs):86        side = 'buy' if self.rng.random() < 0.5 else 'sell'87        q = self.size * (obs['holdings'] if side == 'sell' else max(obs['cash'] / obs['price'], 0))88        lim = obs['price'] * (1 + 0.02 * self.rng.standard_normal())89        return dict(side=side, quantity=q, order_type='limit', limit_price=max(lim, 1e-6), borrow=0.0, rationale='noise')909192# --------------------------------------------------------------------------- LLM agent93SYSTEM_PROMPTS = {94    'investor': ("You are an AI portfolio agent acting on behalf of a human principal in a simulated market with one risky "95                 "asset (pays a stochastic dividend each period) and a safe deposit paying rate rf. Your mandate: maximise the "96                 "principal's expected wealth next period while keeping risk moderate; you may borrow against holdings only "97                 "within the margin limit. You must reply with ONLY a JSON object with keys side (buy|sell|hold), quantity "98                 "(number of shares, >= 0), order_type (limit|market), limit_price (number), borrow (number >= 0, cash to "99                 "borrow), rationale (short string). Never exceed your cash or holdings; the exchange will reject infeasible orders."),100    'market_maker': ("You are a market-maker agent. Quote a limit order on the side where your inventory deviates most from "101                     "its target, with a price inside the recent range. Reply with ONLY the JSON object described."),102    'lender': ("You are a bank credit officer. Given the borrower's balance sheet, decide the fraction of the requested loan "103               "to grant (0..1). Reply ONLY with JSON {\"grant_fraction\": number, \"rationale\": string}."),104}105106PROMPT_VARIANTS = [107    "", " Be concise.", " Think like a careful long-term investor.", " Trade actively when the signal is strong.",108    " Prefer limit orders near the current price.",109]110111112class LLMAgent:113    def __init__(self, client, role: str = 'investor', variant: int = 0, temperature: float = 0.0, seed: int = 0,114                 memory: int = 0):115        self.client, self.role, self.variant, self.T, self.seed, self.memory = client, role, variant, temperature, seed, memory116        self.history = []117118    def observation_text(self, obs):119        o = {k: (round(v, 4) if isinstance(v, float) else v) for k, v in obs.items() if k != 'target'}120        o['history'] = [round(x, 4) for x in obs['history'][-5:]]121        return json.dumps(o)122123    def act(self, obs, tags=None):124        sys_prompt = SYSTEM_PROMPTS[self.role] + PROMPT_VARIANTS[self.variant % len(PROMPT_VARIANTS)]125        msgs = [{'role': 'system', 'content': sys_prompt}]126        if self.memory and self.history:127            msgs.append({'role': 'user', 'content': 'Your previous actions: ' + json.dumps(self.history[-self.memory:])})128        msgs.append({'role': 'user', 'content': 'Observation: ' + self.observation_text(obs) + '\nReply with the JSON action.'})129        res = self.client.complete_json(msgs, ACTION_SCHEMA, temperature=self.T, seed=self.seed, tags=tags)130        if res.parsed is None:131            act = dict(side='hold', quantity=0.0, order_type='limit', limit_price=obs['price'], borrow=0.0,132                       rationale='INVALID-LLM-OUTPUT -> no action', invalid=True)133        else:134            act = dict(res.parsed)135            act.setdefault('limit_price', obs['price'])136            act.setdefault('borrow', 0.0)137            act['invalid'] = False138        if self.memory:139            self.history.append({k: act[k] for k in ('side', 'quantity', 'limit_price')})140        return act141142143def clip_action(act: dict, obs: dict, ledger, i: int) -> dict:144    """Deterministic enforcement: borrow within capacity, quantities within cash/holdings, prices positive."""145    a = dict(act)146    a['side'] = a.get('side', 'hold')147    a['quantity'] = float(max(a.get('quantity', 0.0) or 0.0, 0.0))148    a['order_type'] = a.get('order_type', 'limit') if a.get('order_type') in ('limit', 'market') else 'limit'149    lp = a.get('limit_price', obs['price'])150    a['limit_price'] = float(lp) if (lp is not None and lp > 0) else obs['price']151    a['borrow'] = float(max(a.get('borrow', 0.0) or 0.0, 0.0))152    if a['borrow'] > 0:153        a['borrow'] = ledger.borrow(i, a['borrow'])            # actually granted (margin + bank capital)154    if a['side'] == 'buy':155        px = a['limit_price'] if a['order_type'] == 'limit' else obs['price'] * 1.05156        a['quantity'] = min(a['quantity'], ledger.max_affordable(i, px))157    elif a['side'] == 'sell':158        a['quantity'] = min(a['quantity'], ledger.holdings[i])159    else:160        a['quantity'] = 0.0161    a['clipped'] = (a['quantity'] < float(act.get('quantity', 0.0) or 0.0) - 1e-9)162    return a163
Files · main
Menu

Theme