426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
623 lines
19 KiB
Python
623 lines
19 KiB
Python
"""
|
|
Action Executor for GRPO Training
|
|
|
|
Executes parsed actions against a simulated game state and calculates
|
|
resulting P&L for reward computation.
|
|
|
|
This is a lightweight executor for training that:
|
|
1. Takes parsed action JSON from model responses
|
|
2. Validates action parameters
|
|
3. Simulates execution against market state
|
|
4. Calculates simulated P&L based on scenario outcomes
|
|
|
|
The executor does NOT interact with real markets - it simulates outcomes
|
|
based on scenario data for training purposes.
|
|
"""
|
|
|
|
import logging
|
|
import random
|
|
from dataclasses import dataclass, field
|
|
from typing import Literal
|
|
|
|
from .scenario_pool import MarketState, PerpetualState, Scenario
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Module-level RNG for reproducibility in testing
|
|
_rng: random.Random | None = None
|
|
|
|
|
|
def set_simulation_seed(seed: int) -> None:
|
|
"""Set seed for deterministic P&L simulation. Useful for testing."""
|
|
global _rng
|
|
_rng = random.Random(seed)
|
|
logger.debug(f"Action executor RNG seeded with {seed}")
|
|
|
|
|
|
def reset_simulation_rng() -> None:
|
|
"""Reset to non-deterministic mode (uses global random)."""
|
|
global _rng
|
|
_rng = None
|
|
|
|
|
|
def _gauss(mu: float, sigma: float) -> float:
|
|
"""Get gaussian random value, using seeded RNG if set."""
|
|
if _rng is not None:
|
|
return _rng.gauss(mu, sigma)
|
|
return random.gauss(mu, sigma)
|
|
|
|
|
|
def _uniform(a: float, b: float) -> float:
|
|
"""Get uniform random value, using seeded RNG if set."""
|
|
if _rng is not None:
|
|
return _rng.uniform(a, b)
|
|
return random.uniform(a, b)
|
|
|
|
|
|
# =============================================================================
|
|
# Execution Result
|
|
# =============================================================================
|
|
|
|
|
|
@dataclass
|
|
class ActionResult:
|
|
"""Result of executing an action"""
|
|
|
|
success: bool
|
|
action_type: str
|
|
pnl: float = 0.0
|
|
cost: float = 0.0
|
|
message: str = ""
|
|
|
|
# Trade details
|
|
market_id: str | None = None
|
|
ticker: str | None = None
|
|
size: float = 0.0
|
|
side: str | None = None
|
|
entry_price: float = 0.0
|
|
|
|
# Portfolio after execution
|
|
new_balance: float = 0.0
|
|
new_positions: int = 0
|
|
|
|
|
|
@dataclass
|
|
class PortfolioState:
|
|
"""Current portfolio state"""
|
|
|
|
balance: float = 10000.0
|
|
pnl: float = 0.0
|
|
positions: dict[str, dict] = field(default_factory=dict)
|
|
trade_count: int = 0
|
|
|
|
@property
|
|
def position_count(self) -> int:
|
|
return len(self.positions)
|
|
|
|
|
|
# =============================================================================
|
|
# Action Validation
|
|
# =============================================================================
|
|
|
|
|
|
def validate_action(action: dict) -> tuple[bool, str]:
|
|
"""
|
|
Validate action parameters.
|
|
|
|
Returns:
|
|
(is_valid, error_message)
|
|
"""
|
|
action_type = action.get("action")
|
|
|
|
if not action_type:
|
|
return False, "Missing 'action' field"
|
|
|
|
valid_actions = {"buy", "sell", "open_perp", "close_perp", "wait"}
|
|
if action_type not in valid_actions:
|
|
return False, f"Invalid action type: {action_type}"
|
|
|
|
if action_type == "wait":
|
|
return True, ""
|
|
|
|
# Validate trading actions
|
|
if action_type in ("buy", "sell"):
|
|
if "market" not in action:
|
|
return False, "Buy/sell requires 'market' field"
|
|
if "amount" not in action:
|
|
return False, "Buy/sell requires 'amount' field"
|
|
amount = action.get("amount")
|
|
if not isinstance(amount, (int, float)) or amount <= 0:
|
|
return False, f"Invalid amount: {amount}"
|
|
|
|
if action_type in ("open_perp", "close_perp"):
|
|
if "ticker" not in action:
|
|
return False, "Perp trade requires 'ticker' field"
|
|
if "size" not in action:
|
|
return False, "Perp trade requires 'size' field"
|
|
size = action.get("size")
|
|
if not isinstance(size, (int, float)) or size <= 0:
|
|
return False, f"Invalid size: {size}"
|
|
|
|
if action_type == "open_perp":
|
|
if "direction" not in action:
|
|
return False, "open_perp requires 'direction' field"
|
|
direction = action.get("direction")
|
|
if direction not in ("long", "short"):
|
|
return False, f"Invalid direction: {direction}"
|
|
|
|
return True, ""
|
|
|
|
|
|
# =============================================================================
|
|
# Outcome Simulation
|
|
# =============================================================================
|
|
|
|
|
|
def simulate_market_outcome(
|
|
market: MarketState,
|
|
side: str,
|
|
amount: float,
|
|
hold_duration: Literal["short", "medium", "long"] = "short",
|
|
) -> tuple[float, str]:
|
|
"""
|
|
Simulate outcome for a prediction market trade.
|
|
|
|
Uses market state to probabilistically determine outcome.
|
|
|
|
Args:
|
|
market: The market being traded
|
|
side: "yes" or "no"
|
|
amount: Trade size
|
|
hold_duration: How long the position is held
|
|
|
|
Returns:
|
|
(pnl, outcome_description)
|
|
"""
|
|
# Get current price
|
|
entry_price = market.yes_price if side == "yes" else market.no_price
|
|
|
|
# Simulate price movement based on market state
|
|
# Higher volume = more stable, lower volume = more volatile
|
|
volatility = 0.1 * (1 + 1 / (1 + market.volume_24h / 100000))
|
|
|
|
# Duration affects expected return
|
|
duration_multiplier = {"short": 0.5, "medium": 1.0, "long": 2.0}.get(hold_duration, 1.0)
|
|
|
|
# Random price movement with slight mean reversion
|
|
mean_reversion = (0.5 - entry_price) * 0.1 # Pull towards 50%
|
|
drift = _gauss(mean_reversion, volatility * duration_multiplier)
|
|
|
|
# Calculate new price (bounded 0-1)
|
|
new_price = max(0.01, min(0.99, entry_price + drift))
|
|
|
|
# Calculate P&L
|
|
if side == "yes":
|
|
pnl = (new_price - entry_price) * amount
|
|
else:
|
|
pnl = (entry_price - new_price) * amount
|
|
|
|
# Apply transaction cost
|
|
cost = amount * 0.002 # 0.2% fee
|
|
pnl -= cost
|
|
|
|
# Generate description
|
|
price_change = (new_price - entry_price) / entry_price * 100
|
|
direction = "up" if price_change > 0 else "down"
|
|
outcome = f"Market moved {direction} {abs(price_change):.1f}%, P&L: ${pnl:.2f}"
|
|
|
|
return round(pnl, 2), outcome
|
|
|
|
|
|
def simulate_perp_outcome(
|
|
perp: PerpetualState,
|
|
direction: str,
|
|
size: float,
|
|
hold_duration: Literal["short", "medium", "long"] = "short",
|
|
) -> tuple[float, str]:
|
|
"""
|
|
Simulate outcome for a perpetual trade.
|
|
|
|
Uses current price and volatility to simulate outcome.
|
|
|
|
Args:
|
|
perp: The perpetual market
|
|
direction: "long" or "short"
|
|
size: Position size in units
|
|
hold_duration: How long the position is held
|
|
|
|
Returns:
|
|
(pnl, outcome_description)
|
|
"""
|
|
entry_price = perp.mark_price
|
|
|
|
# Use 24h change as volatility indicator
|
|
volatility = abs(perp.change_24h) * 2 + 0.02 # Minimum 2% volatility
|
|
|
|
# Duration affects expected return
|
|
duration_multiplier = {"short": 0.3, "medium": 1.0, "long": 2.5}.get(hold_duration, 1.0)
|
|
|
|
# Funding rate impact
|
|
funding_impact = -perp.funding_rate * size * entry_price * duration_multiplier
|
|
|
|
# Random price movement
|
|
price_change_pct = _gauss(0, volatility * duration_multiplier)
|
|
new_price = entry_price * (1 + price_change_pct)
|
|
|
|
# Calculate P&L based on direction
|
|
if direction == "long":
|
|
pnl = (new_price - entry_price) * size
|
|
else:
|
|
pnl = (entry_price - new_price) * size
|
|
|
|
# Add funding impact
|
|
pnl += funding_impact
|
|
|
|
# Apply transaction cost
|
|
cost = abs(size) * entry_price * 0.001 # 0.1% fee
|
|
pnl -= cost
|
|
|
|
# Generate description
|
|
direction_str = "up" if price_change_pct > 0 else "down"
|
|
outcome = (
|
|
f"{perp.ticker} moved {direction_str} {abs(price_change_pct * 100):.1f}%, P&L: ${pnl:.2f}"
|
|
)
|
|
|
|
return round(pnl, 2), outcome
|
|
|
|
|
|
# =============================================================================
|
|
# Action Executor
|
|
# =============================================================================
|
|
|
|
|
|
class ActionExecutor:
|
|
"""
|
|
Executes actions against simulated game state.
|
|
|
|
For training, this simulates action outcomes based on scenario data.
|
|
Does not interact with real markets.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
scenario: Scenario,
|
|
starting_balance: float = 10000.0,
|
|
max_positions: int = 10,
|
|
max_position_size: float = 1000.0,
|
|
):
|
|
self.scenario = scenario
|
|
self.max_positions = max_positions
|
|
self.max_position_size = max_position_size
|
|
|
|
# Initialize portfolio
|
|
starting = scenario.portfolio.balance if scenario.portfolio else starting_balance
|
|
self.portfolio = PortfolioState(balance=starting)
|
|
|
|
# Build market lookups
|
|
self.markets: dict[str, MarketState] = {}
|
|
for market in scenario.markets:
|
|
self.markets[market.market_id] = market
|
|
|
|
self.perpetuals: dict[str, PerpetualState] = {}
|
|
for perp in scenario.perpetuals:
|
|
self.perpetuals[perp.ticker] = perp
|
|
|
|
def execute(self, action: dict) -> ActionResult:
|
|
"""
|
|
Execute an action and return the result.
|
|
|
|
Args:
|
|
action: Parsed action dictionary
|
|
|
|
Returns:
|
|
ActionResult with success status and P&L
|
|
"""
|
|
# Validate action
|
|
is_valid, error = validate_action(action)
|
|
if not is_valid:
|
|
return ActionResult(
|
|
success=False,
|
|
action_type=action.get("action", "unknown"),
|
|
message=f"Invalid action: {error}",
|
|
)
|
|
|
|
action_type = action["action"]
|
|
|
|
if action_type == "wait":
|
|
return ActionResult(
|
|
success=True,
|
|
action_type="wait",
|
|
message="Waiting for better opportunity",
|
|
new_balance=self.portfolio.balance,
|
|
new_positions=self.portfolio.position_count,
|
|
)
|
|
|
|
if action_type == "buy":
|
|
return self._execute_prediction_buy(action)
|
|
|
|
if action_type == "sell":
|
|
return self._execute_prediction_sell(action)
|
|
|
|
if action_type == "open_perp":
|
|
return self._execute_perp_open(action)
|
|
|
|
if action_type == "close_perp":
|
|
return self._execute_perp_close(action)
|
|
|
|
return ActionResult(
|
|
success=False,
|
|
action_type=action_type,
|
|
message=f"Unhandled action type: {action_type}",
|
|
)
|
|
|
|
def _execute_prediction_buy(self, action: dict) -> ActionResult:
|
|
"""Execute prediction market buy"""
|
|
market_id = action["market"]
|
|
amount = min(action["amount"], self.max_position_size)
|
|
side = action.get("side", "yes")
|
|
|
|
# Find market
|
|
market = self.markets.get(market_id)
|
|
if not market:
|
|
# Try to find by partial match
|
|
for mid, m in self.markets.items():
|
|
if market_id in mid or mid in market_id:
|
|
market = m
|
|
market_id = mid
|
|
break
|
|
|
|
if not market:
|
|
return ActionResult(
|
|
success=False,
|
|
action_type="buy",
|
|
message=f"Market not found: {market_id}",
|
|
)
|
|
|
|
# Check balance
|
|
cost = amount
|
|
if self.portfolio.balance < cost:
|
|
return ActionResult(
|
|
success=False,
|
|
action_type="buy",
|
|
message=f"Insufficient balance: ${self.portfolio.balance:.2f} < ${cost:.2f}",
|
|
)
|
|
|
|
# Check position limit
|
|
if self.portfolio.position_count >= self.max_positions:
|
|
return ActionResult(
|
|
success=False,
|
|
action_type="buy",
|
|
message=f"Position limit reached: {self.max_positions}",
|
|
)
|
|
|
|
# Simulate outcome
|
|
pnl, outcome_msg = simulate_market_outcome(market, side, amount)
|
|
|
|
# Update portfolio
|
|
self.portfolio.balance -= cost
|
|
self.portfolio.balance += cost + pnl # Return principal + P&L
|
|
self.portfolio.pnl += pnl
|
|
self.portfolio.trade_count += 1
|
|
|
|
# Track position
|
|
position_id = f"pred-{market_id}-{side}"
|
|
self.portfolio.positions[position_id] = {
|
|
"type": "prediction",
|
|
"market_id": market_id,
|
|
"side": side,
|
|
"amount": amount,
|
|
"entry_price": market.yes_price if side == "yes" else market.no_price,
|
|
"pnl": pnl,
|
|
}
|
|
|
|
return ActionResult(
|
|
success=True,
|
|
action_type="buy",
|
|
pnl=pnl,
|
|
cost=cost,
|
|
message=outcome_msg,
|
|
market_id=market_id,
|
|
side=side,
|
|
size=amount,
|
|
entry_price=market.yes_price if side == "yes" else market.no_price,
|
|
new_balance=self.portfolio.balance,
|
|
new_positions=self.portfolio.position_count,
|
|
)
|
|
|
|
def _execute_prediction_sell(self, action: dict) -> ActionResult:
|
|
"""Execute prediction market sell"""
|
|
market_id = action["market"]
|
|
amount = action["amount"]
|
|
side = action.get("side", "yes")
|
|
|
|
# Find existing position
|
|
position_id = f"pred-{market_id}-{side}"
|
|
position = self.portfolio.positions.get(position_id)
|
|
|
|
if not position:
|
|
return ActionResult(
|
|
success=False,
|
|
action_type="sell",
|
|
message=f"No position found for {market_id} {side}",
|
|
)
|
|
|
|
# Close position
|
|
del self.portfolio.positions[position_id]
|
|
self.portfolio.trade_count += 1
|
|
|
|
return ActionResult(
|
|
success=True,
|
|
action_type="sell",
|
|
pnl=0, # P&L was already realized on buy
|
|
message=f"Closed position in {market_id}",
|
|
market_id=market_id,
|
|
side=side,
|
|
size=amount,
|
|
new_balance=self.portfolio.balance,
|
|
new_positions=self.portfolio.position_count,
|
|
)
|
|
|
|
def _execute_perp_open(self, action: dict) -> ActionResult:
|
|
"""Execute perpetual position open"""
|
|
ticker = action["ticker"]
|
|
size = min(action["size"], self.max_position_size)
|
|
direction = action["direction"]
|
|
|
|
# Find perpetual market
|
|
perp = self.perpetuals.get(ticker)
|
|
if not perp:
|
|
# Try uppercase
|
|
perp = self.perpetuals.get(ticker.upper())
|
|
if perp:
|
|
ticker = ticker.upper()
|
|
|
|
if not perp:
|
|
return ActionResult(
|
|
success=False,
|
|
action_type="open_perp",
|
|
message=f"Perpetual not found: {ticker}",
|
|
)
|
|
|
|
# Check margin
|
|
margin_required = size * perp.mark_price * 0.1 # 10x leverage
|
|
if self.portfolio.balance < margin_required:
|
|
return ActionResult(
|
|
success=False,
|
|
action_type="open_perp",
|
|
message=f"Insufficient margin: need ${margin_required:.2f}",
|
|
)
|
|
|
|
# Check position limit
|
|
if self.portfolio.position_count >= self.max_positions:
|
|
return ActionResult(
|
|
success=False,
|
|
action_type="open_perp",
|
|
message=f"Position limit reached: {self.max_positions}",
|
|
)
|
|
|
|
# Simulate outcome
|
|
pnl, outcome_msg = simulate_perp_outcome(perp, direction, size)
|
|
|
|
# Update portfolio
|
|
self.portfolio.balance += pnl
|
|
self.portfolio.pnl += pnl
|
|
self.portfolio.trade_count += 1
|
|
|
|
# Track position
|
|
position_id = f"perp-{ticker}-{direction}"
|
|
self.portfolio.positions[position_id] = {
|
|
"type": "perpetual",
|
|
"ticker": ticker,
|
|
"direction": direction,
|
|
"size": size,
|
|
"entry_price": perp.mark_price,
|
|
"pnl": pnl,
|
|
}
|
|
|
|
return ActionResult(
|
|
success=True,
|
|
action_type="open_perp",
|
|
pnl=pnl,
|
|
cost=margin_required,
|
|
message=outcome_msg,
|
|
ticker=ticker,
|
|
side=direction,
|
|
size=size,
|
|
entry_price=perp.mark_price,
|
|
new_balance=self.portfolio.balance,
|
|
new_positions=self.portfolio.position_count,
|
|
)
|
|
|
|
def _execute_perp_close(self, action: dict) -> ActionResult:
|
|
"""Execute perpetual position close"""
|
|
ticker = action["ticker"]
|
|
size = action["size"]
|
|
|
|
# Find existing position
|
|
position_id_long = f"perp-{ticker}-long"
|
|
position_id_short = f"perp-{ticker}-short"
|
|
|
|
position_id = None
|
|
if position_id_long in self.portfolio.positions:
|
|
position_id = position_id_long
|
|
elif position_id_short in self.portfolio.positions:
|
|
position_id = position_id_short
|
|
|
|
if not position_id:
|
|
return ActionResult(
|
|
success=False,
|
|
action_type="close_perp",
|
|
message=f"No position found for {ticker}",
|
|
)
|
|
|
|
# Close position
|
|
del self.portfolio.positions[position_id]
|
|
self.portfolio.trade_count += 1
|
|
|
|
return ActionResult(
|
|
success=True,
|
|
action_type="close_perp",
|
|
pnl=0, # P&L was realized on open
|
|
message=f"Closed {ticker} position",
|
|
ticker=ticker,
|
|
size=size,
|
|
new_balance=self.portfolio.balance,
|
|
new_positions=self.portfolio.position_count,
|
|
)
|
|
|
|
def get_portfolio_summary(self) -> dict:
|
|
"""Get current portfolio summary"""
|
|
return {
|
|
"balance": self.portfolio.balance,
|
|
"pnl": self.portfolio.pnl,
|
|
"position_count": self.portfolio.position_count,
|
|
"trade_count": self.portfolio.trade_count,
|
|
"positions": list(self.portfolio.positions.keys()),
|
|
}
|
|
|
|
def get_total_pnl(self) -> float:
|
|
"""Get total realized P&L"""
|
|
return self.portfolio.pnl
|
|
|
|
|
|
def execute_action_for_training(
|
|
action: dict,
|
|
scenario: Scenario,
|
|
) -> ActionResult:
|
|
"""
|
|
Convenience function to execute a single action for training scoring.
|
|
|
|
Creates a fresh executor and runs the action.
|
|
"""
|
|
executor = ActionExecutor(scenario)
|
|
return executor.execute(action)
|
|
|
|
|
|
def calculate_action_quality_bonus(result: ActionResult) -> float:
|
|
"""
|
|
Calculate quality bonus based on action execution.
|
|
|
|
Used in reward calculation to encourage:
|
|
- Valid action formatting
|
|
- Profitable trades
|
|
- Risk-appropriate sizing
|
|
"""
|
|
bonus = 0.0
|
|
|
|
if result.success:
|
|
bonus += 0.1 # Base bonus for valid action
|
|
|
|
# P&L based bonus
|
|
if result.pnl > 0:
|
|
bonus += min(0.2, result.pnl / 1000) # Up to 0.2 for profit
|
|
elif result.pnl < -100:
|
|
bonus -= 0.1 # Penalty for large losses
|
|
|
|
# Bonus for trading (not just waiting)
|
|
if result.action_type not in ["wait"]:
|
|
bonus += 0.05
|
|
else:
|
|
bonus -= 0.1 # Penalty for failed actions
|
|
|
|
return max(-0.3, min(0.3, bonus))
|