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.

Position in the data flow
Data sourceData processingCustom data processing[AI model]Custom trading logicTrading engine
How it differs from the strategy-node combination
ComparisonStrategy + position + risk control (visual)Custom trading logic node
ConfigurationThree connected nodes with expression settingsOne node with a Python code editor
ExpressivenessPreset timing, selection, equal-weight, and focused templates plus expressionsFull Python syntax with access to every Client API
Callback controlControlled by system templatesFull control over init, handle_data, and handle_tick
Learning curveLow; suitable for standard strategiesModerate; requires familiarity with the BeeQuant API
Important: a custom trading logic node cannot connect to the same trading engine as strategy type, position management, or risk control nodes. These are mutually exclusive decision paths; choose one.

2. Interactive configuration

The panel below is the actual custom trading logic node configuration. Write and test trading logic directly in its code editor.

Custom trading logic node configuration

3. Code template

3.1 Three lifecycle functions

The system calls these functions automatically at different points in execution
Function signatures
python
from 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.

FunctionWhen calledTypical uses
initOnce, when the strategy startsRead configuration, initialize state, and register custom parameters
handle_dataAt the signal interval; at the beginning of each price bar in a backtestRead one signal timestep, rebalance positions, and record entry prices
handle_tickOn 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
AttributeTypeDescription
context.exchange[0]ClientClient for the first trading account, used for quotes and orders
context.exchange[0].symbol_listlist[str]Trading pairs currently monitored by the strategy
context.logLogLogging service supporting info, warn, error, debug, and detail
context.signalSignalget() reads the current single-timestep signal
context.fileFileFile service for loading or saving models and state files
Dynamic attributesAnyUser-defined state, such as context.entry_price = {}

3.3 Common Client APIs

Obtain the client with client = context.exchange[0]
MethodDescription
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
Symbol naming: use 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 sign
python
from 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:
    pass

4.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_tick
python
from 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.
Data source: Example 4 in the upstream custom data-processing node merges BTC and ETH models horizontally by timestamp into a wide table, with 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.
python
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:
    pass
Key points: ① this wide table has one row per signal timestep; if upstream returns a multi-row instrument table, iterate over every row. ② pd.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:

Modehandle_data triggerhandle_tick triggerDescription
BacktestAt the signal interval, using the latest signal strictly earlier than the current price barNot called at bar precision; follows the synthetic minute-OHLC interpolation path at tick precisionHistorical matching; no real tick trades, quotes, or order-book events
Paper tradingOnce immediately at startup, then at signal-interval boundariesEvery market-data updateUses live market data without placing real orders
Live tradingOnce immediately at startup, then at signal-interval boundariesEvery market-data updatePlaces real orders and requires a connected exchange API
Recommendation: 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

Q: Can custom trading logic be used together with strategy, position, and risk-control nodes?

A: No. They form mutually exclusive paths: use either the three visual nodes or one custom trading logic node. Combining them fails workflow validation.

Q: Which node produces datas in handle_data?

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.

Q: How can callbacks share state?

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.

Q: What does target_percent's threshold parameter do?

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%.

Q: Can I use third-party libraries?

A: Yes. Pre-installed engine-container libraries include pandas, numpy, talib, scikit-learn, and scipy. Do not run pip install in node code.

Q: How do I trade through multiple accounts?

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").

    BeeQuant - AI Quantitative Trading Platform