Custom trading logic node
1. Node overview
The custom trading logic node lets Python code fully control trading decisions, position management, and risk control. It replaces the visual combination of a strategy type node, position management node, and risk control node. Use it for complex decision rules, specialized execution, or custom risk controls.
| Comparison | Strategy + position + risk control (visual) | Custom trading logic node |
|---|---|---|
| Configuration | Three connected nodes with expression settings | One node with a Python code editor |
| Expressiveness | Preset timing, selection, equal-weight, and focused templates plus expressions | Full Python syntax with access to every Client API |
| Callback control | Controlled by system templates | Full control over init, handle_data, and handle_tick |
| Learning curve | Low; suitable for standard strategies | Moderate; requires familiarity with the BeeQuant API |
2. Interactive configuration
The panel below is the actual custom trading logic node configuration. Write and test trading logic directly in its code editor.
3. Code template
3.1 Three lifecycle functions
The system calls these functions automatically at different points in executionfrom beequant.api.context import UserContext
from beequant.api.client import Client
import pandas as pd
def init(context: UserContext) -> None:
"""Called once when the strategy starts; initialize state here."""
...
def handle_data(context: UserContext, datas: pd.DataFrame) -> None:
"""Process one signal timestep at the configured signal interval."""
...
def handle_tick(context: UserContext) -> None:
"""Price-update callback; tick-precision backtests use a synthetic price path."""
...Time range contained in datas
datas contains only one signal timestep: every row at the same signal timestamp, potentially for several symbol values. It is not the complete table or a historical window.
In a backtest, this timestamp is strictly earlier than the current price bar, and it may be None before the first signal is available. Calculate historical features in an upstream data-processing node, output them as columns, and set historyPeriods no lower than the required window.
| Function | When called | Typical uses |
|---|---|---|
| init | Once, when the strategy starts | Read configuration, initialize state, and register custom parameters |
| handle_data | At the signal interval; at the beginning of each price bar in a backtest | Read one signal timestep, rebalance positions, and record entry prices |
| handle_tick | On market updates online; along a synthetic price path in tick-precision backtests (optional) | Take-profit and stop-loss logic, peak-price tracking, and price monitoring |
3.2 Available context attributes
Context members available in init, handle_data, and handle_tick| Attribute | Type | Description |
|---|---|---|
| context.exchange[0] | Client | Client for the first trading account, used for quotes and orders |
| context.exchange[0].symbol_list | list[str] | Trading pairs currently monitored by the strategy |
| context.log | Log | Logging service supporting info, warn, error, debug, and detail |
| context.signal | Signal | get() reads the current single-timestep signal |
| context.file | File | File service for loading or saving models and state files |
| Dynamic attributes | Any | User-defined state, such as context.entry_price = {} |
3.3 Common Client APIs
Obtain the client withclient = context.exchange[0]| Method | Description |
|---|---|
| client.target_percent(symbol, pct) | Rebalance to a percentage of total account equity; the most common order method |
| client.target(symbol, qty) | Rebalance to a target quantity by ordering the difference automatically |
| client.place(symbol, side, qty=, cost=) | General order API; provide either qty or cost |
| client.close_all(symbol) | Close every position for the specified pair |
| client.get_position(symbol, position_side) | Get position information for one side |
| client.get_price(symbol) | Get the current price |
| client.get_total_equity() | Get total account equity denominated in USDT |
BTCUSDT for spot, BTC_USDT for USDT-margined futures, and BTC_USD for coin-margined futures. The format must match the market type configured in the data source node.4. Complete code examples
4.1 Example 1: Basic signal execution
Read the upstream prediction column and open or close positions according to its signfrom beequant.api.context import UserContext
from beequant.api.client import Client
import pandas as pd
def init(context: UserContext) -> None:
context.position_size = 0.5 # Target 50% of equity per instrument
context.log.info("Strategy initialized", source="Custom trading logic")
def handle_data(context: UserContext, datas: pd.DataFrame) -> None:
client: Client = context.exchange[0]
if datas is None or datas.empty:
return
for _, row in datas.iterrows():
symbol = row["symbol"]
pred = float(row.get("prediction", 0.0))
if pred > 0:
order_id = client.target_percent(
symbol, context.position_size, threshold=0.03, auto_partial=True
)
context.log.info(f"Opened long {symbol}; order_id={order_id}", source="Custom trading logic")
elif pred < 0:
order_id = client.target_percent(symbol, 0, auto_partial=True)
context.log.info(f"Closed {symbol}; order_id={order_id}", source="Custom trading logic")
def handle_tick(context: UserContext) -> None:
pass4.2 Example 2: Ranked multi-asset rebalancing with take-profit and stop-loss
Hold at most N assets at X% each and monitor take-profit and stop-loss conditions in handle_tickfrom beequant.api.context import UserContext
from beequant.api.client import Client
import pandas as pd
def init(context: UserContext) -> None:
context.max_hold = 2 # Hold no more than two assets
context.per_pct = 0.5 # Allocate 50% per asset
context.take_profit = 0.05 # Take profit at 5%
context.stop_loss = 0.02 # Stop loss at 2%
context.entry_price = {} # Record each symbol's entry price
context.log.info("Ranked multi-asset rebalancing initialized", source="Custom trading logic")
def handle_data(context: UserContext, datas: pd.DataFrame) -> None:
client: Client = context.exchange[0]
if datas is None or datas.empty:
return
# 1) Sort the current slice by prediction score, descending
latest = datas.sort_values("prediction", ascending=False)
target_symbols = latest.head(context.max_hold)["symbol"].tolist()
# 2) Close positions that are no longer in the target set
for pos in client.get_positions():
if pos.symbol not in target_symbols and pos.amount != 0:
client.target_percent(pos.symbol, 0, auto_partial=True)
context.entry_price.pop(pos.symbol, None)
context.log.info(f"Rotated out of {pos.symbol}", source="Custom trading logic")
# 3) Rebalance targets and record their entry prices
for symbol in target_symbols:
order_id = client.target_percent(
symbol, context.per_pct, threshold=0.03, auto_partial=True
)
if order_id and symbol not in context.entry_price:
context.entry_price[symbol] = client.get_price(symbol)
context.log.info(
f"Opened long {symbol}; entry={context.entry_price[symbol]}",
source="Custom trading logic",
)
def handle_tick(context: UserContext) -> None:
"""Monitor take-profit and stop-loss conditions on every tick."""
client: Client = context.exchange[0]
for symbol, entry in list(context.entry_price.items()):
price = client.get_price(symbol)
if not price or not entry:
continue
ret = price / entry - 1
if ret >= context.take_profit:
client.target_percent(symbol, 0, auto_partial=True)
context.entry_price.pop(symbol, None)
context.log.info(f"Took profit on {symbol}; return={ret:.2%}", source="Custom trading logic")
elif ret <= -context.stop_loss:
client.target_percent(symbol, 0, auto_partial=True)
context.entry_price.pop(symbol, None)
context.log.info(f"Stopped out of {symbol}; return={ret:.2%}", source="Custom trading logic")4.3 Example 3: Independent per-instrument decisions from a wide table
Multi-source strategy: upstream logic merges several models into a wide table with one prediction column per instrument, and this node opens or closes each instrument independently by column.pred_btc and pred_eth side by side. Read the only row in the current signal slice and evaluate each instrument column separately. Unlike Example 2, this consumes columns from a wide table rather than rows from a long table.from beequant.api.context import UserContext
from beequant.api.client import Client
import pandas as pd
# One threshold set and prediction column per instrument, matching upstream predictionColumn
BTC = dict(symbol="BTC_USDT", pred_col="pred_btc",
open_long=0.07, close_long=0.02, open_short=-0.03, max_pos=0.30)
ETH = dict(symbol="ETH_USDT", pred_col="pred_eth",
open_long=0.09, close_long=-0.02, open_short=-0.03, max_pos=0.30)
def init(context: UserContext) -> None:
context.log.info("BTC+ETH strategy initialized", source="Custom trading logic")
def _handle_symbol(context: UserContext, client: Client, cfg: dict, pred: float) -> None:
"""Reusable per-instrument entry and exit logic; long and short branches are independent."""
symbol, max_pos = cfg["symbol"], cfg["max_pos"]
# Long: move to +max_pos on a strong score; close only the long when it weakens
if pred > cfg["open_long"]:
client.target_percent(symbol, max_pos, threshold=0.01, auto_partial=True)
elif pred < cfg["close_long"]:
pos = client.get_position(symbol, "long")
if pos and abs(pos.amount) > 0:
client.close_long(symbol)
# Short: move to -max_pos on a low score; close only the short when it turns positive
if pred < cfg["open_short"]:
client.target_percent(symbol, -max_pos, threshold=0.01, auto_partial=True)
elif pred > 0:
pos = client.get_position(symbol, "short")
if pos and abs(pos.amount) > 0:
client.close_short(symbol)
def handle_data(context: UserContext, datas: pd.DataFrame) -> None:
client: Client = context.exchange[0]
if datas is None or datas.empty:
context.log.warn("Upstream data is empty; skipping", source="Custom trading logic")
return
# This upstream input is a wide table with one row per signal timestep
row = datas.iloc[0]
for cfg in (BTC, ETH):
col = cfg["pred_col"]
# An outer merge can produce NaN for one asset; skip it to avoid an invalid trade
if col in row and pd.notna(row[col]):
_handle_symbol(context, client, cfg, float(row[col]))
else:
context.log.warn(f"Missing {col}; skipping {cfg['symbol']}", source="Custom trading logic")
def handle_tick(context: UserContext) -> None:
passpd.notna() skips NaN values introduced by an outer merge and prevents invalid trades. ③ storing thresholds and column names in a dict lets every instrument reuse _handle_symbol; add another configuration to support another instrument. ④ each instrument has an independent max_pos; maximum aggregate exposure is their sum, 60% of account equity here.5. Supported execution modes
Custom trading logic behaves consistently in all three modes. The trading engine node determines the execution environment:
| Mode | handle_data trigger | handle_tick trigger | Description |
|---|---|---|---|
| Backtest | At the signal interval, using the latest signal strictly earlier than the current price bar | Not called at bar precision; follows the synthetic minute-OHLC interpolation path at tick precision | Historical matching; no real tick trades, quotes, or order-book events |
| Paper trading | Once immediately at startup, then at signal-interval boundaries | Every market-data update | Uses live market data without placing real orders |
| Live trading | Once immediately at startup, then at signal-interval boundaries | Every market-data update | Places real orders and requires a connected exchange API |
handle_tick runs very frequently in live trading. Avoid expensive computation or high-volume logging in it, which can add latency. Put complex logic in handle_data.6. FAQ
A: No. They form mutually exclusive paths: use either the three visual nodes or one custom trading logic node. Combining them fails workflow validation.
A: It is the DataFrame returned by the directly connected custom data-processing node, data-processing node, or AI model node. It contains at least timestamp / symbol and usually prediction. open/high/low/close/volume are not added automatically. To use a raw value such as close, pass it through explicitly in the upstream data-processing factor expression, for example close = close.
A: Use dynamic attributes on context. For example, define context.entry_price = {} in init and read or update it in handle_data. The value persists for the entire strategy run.
A: It skips a rebalance when the relative difference between current and target position is below the threshold, avoiding fees from frequent small adjustments. For example, threshold=0.03 makes no change when the difference is below 3%.
A: Yes. Pre-installed engine-container libraries include pandas, numpy, talib, scikit-learn, and scipy. Do not run pip install in node code.
A: Configure several exchange accounts in the trading engine. Access them by index through context.exchange[0] and context.exchange[1], or by name with context.exchange.get_by_name("account_name").