Standard environments from Gymnasium (CartPole, LunarLander) are not suitable for trading. They ignore slippage, commissions, and market impact. The result: Sharpe 2 on backtest, but in reality—a deposit blowout. Imagine: you trained an agent on a standard environment, got Sharpe 2.5, run it on a live account—and within a month drawdown is 40%. Sound familiar? The problem is that standard environments don't account for market microstructure: slippage on execution, broker commissions (0.05–0.2% per trade), market impact of large orders (up to 5% of volume). The agent learns to trade on perfect data, but reality is different. We solve this by creating custom Gym environments that include all key factors: spreads, commissions, order book depth for HFT, and even risk management. This allows the agent to train in conditions as close to real as possible and deliver consistent results on live markets. We will assess your project in one day—get in touch with us.
Why a custom Gym environment is critical for a trading strategy?
Standard environments produce inflated results by 20–40%. For example, a strategy with limit orders on multiple exchanges will show excellent results in a standard environment, but in reality slippage of 0.2% and commission of 0.1% eat 30% of profit. The agent learns to trade incorrectly, treating noise as signal. In monetary terms, for a $100,000 portfolio, this means a loss of $30,000 per year—just from costs. A custom environment models these effects, and the agent learns to avoid them.
How we develop a custom environment
We build the environment based on Gymnasium with precise modeling of all trading aspects.
Class structure CustomTradingEnv
import gymnasium as gym
from gymnasium import spaces
import numpy as np
class CustomTradingEnv(gym.Env):
metadata = {'render_modes': ['human', 'rgb_array']}
def __init__(self, df, config):
super().__init__()
self.df = df
self.config = config
# observation space: OHLCV + indicators + portfolio state
n_features = config['n_features']
self.observation_space = spaces.Box(
low=-np.inf, high=np.inf,
shape=(n_features,), dtype=np.float32
)
# action space: position size [-1, 1] per asset
n_assets = config['n_assets']
self.action_space = spaces.Box(
low=-1.0, high=1.0,
shape=(n_assets,), dtype=np.float32
)
self._reset_portfolio()
def reset(self, seed=None, options=None):
super().reset(seed=seed)
self._reset_portfolio()
self.current_step = self.config['window_size']
obs = self._get_observation()
return obs, {}
def step(self, action):
# 1. Execute action (with realistic modeling)
executed_action = self._execute_order(action)
# 2. Move to next step
self.current_step += 1
# 3. Update portfolio with new prices
self._update_portfolio()
# 4. Calculate reward
reward = self._compute_reward()
# 5. Observation
obs = self._get_observation()
# 6. Termination condition
terminated = self.current_step >= len(self.df) - 1
truncated = self.portfolio_value < self.config['min_capital']
info = {
'portfolio_value': self.portfolio_value,
'positions': self.positions.copy(),
'total_trades': self.total_trades
}
return obs, reward, terminated, truncated, info
Realistic order execution
Large orders move the market—ignoring this gives false signals. We model slippage and commissions:
def _execute_order(self, target_weights):
current_prices = self.df.iloc[self.current_step][['open', 'high', 'low', 'close']]
# slippage: depends on order size and spread
order_size = np.abs(target_weights - self.current_weights)
slippage = order_size * self.config['slippage_factor']
execution_price = current_prices['open'] * (1 + slippage)
# commission
trade_value = np.abs(order_size) * execution_price * self.portfolio_value
commission = trade_value * self.config['commission_rate']
self.portfolio_value -= commission.sum()
self.current_weights = target_weights.copy()
self.total_trades += (order_size > 0.01).sum()
return target_weights
For HFT strategies, we add order book simulation:
class LOBSimulator:
"""Level-2 order book simulation"""
def __init__(self, spread_bps=5, depth_levels=10):
self.spread_bps = spread_bps
self.depth_levels = depth_levels
def get_fill_price(self, mid_price, order_size_usd):
# fill price depends on order book depth
spread = mid_price * self.spread_bps / 10000
market_impact = np.sqrt(order_size_usd / 1e6) * spread
return mid_price + spread/2 + market_impact
How to choose a reward function for a trading strategy?
Choosing the reward is the most important part of a custom environment. We offer several options:
| Function | Advantages | Disadvantages | When to use |
|---|---|---|---|
Simple return (daily_return) |
Transparency, simplicity | Does not account for risk | For strategies with low risk tolerance? No. Better for tests |
| Sharpe-adjusted | Accounts for risk via rolling window | Sensitive to window | For long-term strategies where stability is important |
| Penalized drawdown | Penalizes drawdown | More complex penalty tuning | For capital protection strategies |
def _compute_reward(self):
daily_return = (self.portfolio_value / self.prev_portfolio_value) - 1
# option 1: simple return
reward = daily_return
# option 2: Sharpe-adjusted (rolling window)
self.returns_history.append(daily_return)
if len(self.returns_history) >= 20:
sharpe = np.mean(self.returns_history[-20:]) / (np.std(self.returns_history[-20:]) + 1e-8)
reward = daily_return * (1 + sharpe)
# option 3: penalized drawdown
current_dd = (self.peak_value - self.portfolio_value) / self.peak_value
reward = daily_return - self.config['dd_penalty'] * current_dd
# penalty for excessive trading
reward -= self.config['turnover_penalty'] * self.daily_turnover
return float(reward)
The turnover penalty is especially important for HFT—it teaches the agent not to trade when there's no edge.
Observation Engineering
def _get_observation(self):
window = self.df.iloc[self.current_step - self.window_size:self.current_step]
features = []
# price returns (normalized)
returns = window['close'].pct_change().fillna(0).values
features.extend(returns[-self.window_size:])
# technical indicators
features.extend([
window['rsi'].iloc[-1] / 100,
window['macd_norm'].iloc[-1],
window['bb_position'].iloc[-1] # (price - lower) / (upper - lower)
])
# portfolio state
features.extend(self.current_weights)
features.append(self.portfolio_value / self.initial_capital - 1)
return np.array(features, dtype=np.float32)
We add normalized features—this accelerates agent convergence.
Comparison of standard vs custom environment
| Parameter | Standard environment | Custom environment |
|---|---|---|
| Slippage | Not accounted | Modeled based on volume and spread (0.1–0.5 bps) |
| Commissions | None | Broker + exchange fees (0.05–0.2%) |
| Market impact | None | Accounted via LOB (impact up to 5%) |
| Multi-asset support | Single only | Any number (up to 20+ in projects) |
| Reward | Simple return | Sharpe, drawdown, turnover penalty |
Process and timelines
| Stage | Duration |
|---|---|
| Strategy analysis | 1 day |
| Environment design | 1–2 days |
| Implementation | 2–7 days |
| Testing (validation) | 1–2 days |
| Deployment and documentation | 1 day |
Total time: from 3–5 days for a single asset to 3–4 weeks for multi-asset with order book. Cost is calculated individually—leave a request for a consultation, and we will assess the project.
What you get as a result
- Source code of the custom environment in Python (Gymnasium) with support for your data.
- Configuration files for single-asset, multi-asset, and HFT modes.
- Documentation on API, reward functions, and execution modeling.
- Jupyter notebook with agent training and result visualization.
- Support for integration into the pipeline (MLflow, Weights & Biases).
- Guaranteed to pass gymnasium.utils.env_checker checks.
Get in touch with us to discuss your strategy—and receive a consultation within a day.
Typical problems when developing an environment
- Look-ahead bias: normalization on the entire dataset. We check temporal splitting and prevent future data leakage.
- Ignoring commissions in reward: the agent learns to trade frequently, reducing returns by 20–30%.
- Too simple reward (only return): leads to high volatility and drawdowns. We use combined rewards with penalties.
For environment verification, we use gymnasium.utils.env_checker and sanity checks:
- Random policy loses capital due to commissions.
- Buy & Hold is reproduced with constant weights.
- No look-ahead: observation does not contain future prices.
Our engineers with certifications in machine learning and trading guarantee quality. Get a consultation on your strategy—get in touch with us.







