Skip to content

finlab.online

Live trading module for deploying backtested strategies to real broker accounts with automated order execution.

Important Risk Warning

Live trading involves real money. Errors can lead to financial losses!

Mandatory Safety Principles:

  1. Always preview orders with view_only=True and start with a small account
  2. Start with no more than 10-20% of total capital
  3. Monitor positions and order status daily
  4. Set stop-loss mechanisms to avoid large single losses
  5. Use separate accounts for development/testing and production environments
  6. NEVER use all capital on an untested strategy
  7. NEVER ignore error messages or abnormal states

Use Cases

  • Automated trading: Automatically place orders and rebalance based on strategy signals
  • Multiple brokers: Charles Schwab for U.S. stocks; SinoPac, E.SUN, Fubon, Masterlink and Pocket for Taiwan stocks; Binance for crypto
  • Live tracking: Record actual trading performance and compare with backtest results
  • Risk control: Preview orders, trade a fraction of the difference at a time, and cancel or reprice open orders

Supported Brokers

Broker Market Account class
Charles Schwab U.S. stocks finlab.online.schwab_account.SchwabAccount
SinoPac (Shioaji) Taiwan stocks finlab.online.sinopac_account.SinopacAccount
E.SUN (Fugle) Taiwan stocks finlab.online.esun_account.EsunAccount (alias of FugleAccount)
Fubon Taiwan stocks finlab.online.fubon_account.FubonAccount
Masterlink Taiwan stocks finlab.online.masterlink_account.MasterlinkAccount
Pocket Taiwan stocks finlab.online.pocket_account.PocketAccount
Binance Crypto finlab.online.binance_account.BinanceAccount

Quantities follow the account's market: shares for U.S. accounts (board lot of 1 share) and lots of 1,000 shares for Taiwan accounts, where a fraction is an odd lot.

Transition from Backtesting to Live Trading

graph LR
    A[Strategy Backtest] --> B[Out-of-Sample Test]
    B --> C[Upload to Cloud]
    C --> D[Preview Orders]
    D --> E{Test Passed?}
    E -->|Yes| F[Small Capital Live]
    E -->|No| A
    F --> G[Gradually Increase Capital]

Recommended Timeline: 1. Strategy backtesting (1-2 weeks): Confirm logic is correct 2. Out-of-sample testing (2-4 weeks): Use unseen data 3. Order previews (4-8 weeks): Run the full workflow daily with view_only=True 4. Small capital live (8-12 weeks): 10-20% capital test 5. Gradual expansion (as appropriate): Increase capital only after confirming stability

Quick Examples

Basic Usage: Automated Order Execution

import os

from finlab import data
from finlab.backtest import sim
from finlab.online.order_executor import OrderExecutor, Position
from finlab.online.schwab_account import SchwabAccount

# 1. Run the strategy on the U.S. market
data.set_market('us')
with data.us_universe(index='SP500'):
    close = data.get('price:adj_close')
position = (close / close.shift(126)).is_largest(20)
report = sim(position, resample='M', upload=False)

# 2. Turn the latest target weights into share quantities for $100,000
target = Position.from_report(report, fund=100_000)

# 3. Connect to Charles Schwab (pip install schwab-py)
os.environ['SCHWAB_API_KEY'] = 'YOUR_APP_KEY'
os.environ['SCHWAB_SECRET'] = 'YOUR_APP_SECRET'
os.environ['SCHWAB_TOKEN_PATH'] = '/path/to/token.json'
account = SchwabAccount()

# 4. Preview the orders, then place them
executor = OrderExecutor(target, account=account)
executor.create_orders(view_only=True)  # prints the orders, places nothing
# executor.create_orders()  # places the orders

SchwabAccount reads an existing token file created with the schwab-py authentication flow. Taiwan broker setup is covered in the single-strategy order tutorial.

Detailed Guide

See the following tutorials for the complete workflow:

API Reference

Account

class Account(ABC)

Abstract base class for broker accounts. Each broker class above implements it, and OrderExecutor and PortfolioSyncManager only use this interface, so you can implement your own broker by subclassing Account.

Method Returns Description
create_order(action, stock_id, quantity, price=None, odd_lot=False, market_order=False, best_price_limit=None, order_cond=OrderCondition.CASH) str Place a new order and return the broker's order ID.
update_order(order_id, price=None, quantity=None) None Change the limit price or the unfilled quantity of an open order. Returns without error on success.
cancel_order(order_id) None Cancel an open order. Call get_orders() first to work with the latest order status.
get_orders() dict[str, Order] All current orders, keyed by order ID.
get_stocks(stock_ids) dict[str, Stock] Latest quotes for a list of symbols, for example ['AAPL', 'MSFT'].
get_position() Position Current holdings of the account.
get_total_balance() float Net value of the account (for Schwab, the current liquidation value).
get_cash() float Cash balance.
get_settlement() Number Pending settlement amount (not implemented for Schwab).
get_market() Market Market of the account (USMarket for Schwab; FinLab's default market unless the broker class overrides it).
get_price(stock_ids) dict[str, float] Last trade price per symbol, falling back to the bid/ask midpoint when there is no trade yet.

create_order parameters

Parameter Type Description
action Action Action.BUY or Action.SELL.
stock_id str Symbol, for example 'AAPL'.
quantity Number Quantity in the account's unit (shares in the U.S., lots in Taiwan; decimals allowed where the broker supports them).
price Number, optional Limit price.
odd_lot bool Taiwan brokers: place an odd-lot order (fewer than 1,000 shares).
market_order bool Execute as soon as possible (a market order on Schwab; on Taiwan brokers, a buy at the upper price limit or a sell at the lower one).
best_price_limit bool Post a patient order at the most favorable limit (buy at the lower price limit, sell at the upper one).
order_cond OrderCondition CASH (default), MARGIN_TRADING, SHORT_SELLING, DAY_TRADING_LONG or DAY_TRADING_SHORT.

Account Security Best Practices

Use environment variables for sensitive information:

import os

from finlab.online.schwab_account import SchwabAccount

# Set SCHWAB_API_KEY, SCHWAB_SECRET and SCHWAB_TOKEN_PATH outside the code
# (shell profile, secret manager, CI secrets)
account = SchwabAccount()

# Or pass them explicitly from your own configuration
account = SchwabAccount(
    api_key=os.environ['SCHWAB_API_KEY'],
    app_secret=os.environ['SCHWAB_SECRET'],
    token_path=os.environ['SCHWAB_TOKEN_PATH'],
)
# Never hardcode credentials in source code!

Separate development/production runs:

# Development: run the full workflow but only print the orders
executor = OrderExecutor(target, account=account)
if os.environ.get('ENV') == 'development':
    executor.create_orders(view_only=True)
else:
    executor.create_orders()

Position

Position(
    stocks,
    weights=None,
    margin_trading=False,
    short_selling=False,
    day_trading_long=False,
    day_trading_short=False,
) -> Position

Target holdings to deploy with OrderExecutor. Each entry has a symbol, a quantity and an order_condition.

Parameter Type Description
stocks dict[str, Number] Symbol to quantity, for example {'AAPL': 10} for 10 shares in a U.S. account. Negative quantities are short positions.
weights dict[str, float] or pd.Series, optional Target weight per symbol, kept with each entry.
margin_trading bool Use margin for long positions.
short_selling bool Use short selling for short positions.
day_trading_long bool Long positions are day trades (buy first). Cannot be combined with margin_trading.
day_trading_short bool Short positions are day trades (sell first). Cannot be combined with short_selling.
from finlab.online.order_executor import Position

p1 = Position({'AAPL': 10})
p2 = Position({'MSFT': 5})
total = p1 + p2  # positions can be added, subtracted, and scaled (p1 * 2, p1 / 2)

Position.from_report()

Position.from_report(report, fund, **kwargs) -> Position

Build the position to hold now from a backtest report. Before the next rebalance it uses report.weights; once the rebalance time has arrived (the same rule as report.is_rebalance_due()) it switches to report.next_weights. Stocks with pending stop-loss or take-profit exits get a weight of 0. Prices default to the market's latest reference price, and the board lot size comes from report.market (1 share for U.S. stocks, 1,000 shares for Taiwan stocks). A report that holds several strategies must be handled with finlab.portfolio.Portfolio instead.

Parameter Type Description
report Report Backtest report from backtest.sim() (or a cloud report).
fund float Capital to deploy, in the market's currency.
**kwargs Passed to Position.from_weight(), for example odd_lot, price, margin_trading, leverage.
from finlab import backtest, data
from finlab.online.order_executor import Position

data.set_market('us')
with data.us_universe(index='SP500'):
    close = data.get('price:adj_close')

report1 = backtest.sim(
    (close / close.shift(126)).is_largest(10), resample='M'
)  # 6-month momentum
report2 = backtest.sim(
    (close / close.shift(20)).is_smallest(10), resample='M'
)  # 1-month reversal

position1 = Position.from_report(report1, 50_000)
position2 = Position.from_report(report2, 50_000)
total_position = position1 + position2

Position.from_weight()

Position.from_weight(
    weights,
    fund,
    price=None,
    odd_lot=False,
    board_lot_size=None,
    allocation=greedy_allocation,
    precision=None,
    leverage=1.0,
    price_history=None,
    **kwargs,
) -> Position

Convert target weights into quantities. As in backtest.sim(), weights whose absolute values sum to more than 1 are scaled down to a total of 1.

Parameter Type Description
weights dict[str, float] or pd.Series Symbol to weight.
fund float Capital to deploy.
price pd.Series or dict, optional Price per symbol; defaults to the current market's latest reference price.
odd_lot bool Allow quantities smaller than one board lot. When False, a stock whose allocation cannot buy one board lot gets 0 and a ZeroShareAllocationWarning is raised.
board_lot_size int, optional Shares per board lot; defaults to the current market's value (1 for U.S. stocks, 1,000 for Taiwan stocks).
allocation callable Allocation algorithm; defaults to finlab.online.utils.greedy_allocation, which deploys as much capital as possible.
precision int, optional Decimal places of the quantity when board_lot_size is 1.
leverage float Target leverage; above 1.0 the extra exposure is financed with margin on the lowest-volatility holdings first.
price_history pd.DataFrame, optional Historical prices, required when leverage > 1.0.
**kwargs margin_trading, short_selling, day_trading_long, day_trading_short (see the constructor).
from finlab import data

data.set_market('us')
Position.from_weight({'AAPL': 0.5, 'MSFT': 0.5}, fund=100_000)

Other Position methods

Method Description
Position.from_list(position) Build from a list of dicts with symbol (or stock_id), quantity and order_condition (an OrderCondition).
to_list() Return the entries as a list of dicts.
to_json(path) / Position.from_json(path) Save to or load from a JSON file.
Position.from_entries(entries) / to_entries() Convert from or to typed finlab.schemas.PositionEntry objects.

OrderExecutor

OrderExecutor(target_position, account)

Compares the target position with the account's current holdings and places the orders that close the difference.

Parameter Type Description
target_position Position or dict[str, Number] Holdings to deploy.
account Account A logged-in broker account.

create_orders()

create_orders(
    market_order=False,
    best_price_limit=False,
    view_only=False,
    extra_bid_pct=0,
    progress=1,
    progress_precision=0,
    buy_only=False,
    sell_only=False,
)

Cancel the account's open orders, then place orders that move the account to target_position. By default each order is a limit order at the latest trade price. Returns the list of orders that were placed (or would be placed).

Parameter Type Description
market_order bool Execute as soon as possible (see Account.create_order).
best_price_limit bool Post patient orders at the most favorable price limit.
view_only bool Print the orders without placing them or cancelling open orders.
extra_bid_pct float Move the limit price by this fraction: 0.01 buys 1% above (sells 1% below) the latest price to fill faster; -0.01 waits for a better price. Valid range -0.1 to 0.1. Only one of market_order, best_price_limit and extra_bid_pct can be set.
progress float Fraction (0 to 1) of the remaining difference to trade in this call.
progress_precision int Decimal places used when progress is below 1 (0 trades whole units).
buy_only / sell_only bool Place only buy orders or only sell orders.

Other OrderExecutor methods

Method Description
generate_orders(progress=1, progress_precision=0) Return the order list (target minus current holdings) without placing anything.
execute_orders(orders, market_order=False, best_price_limit=False, view_only=False, extra_bid_pct=0, cancel_orders=True, buy_only=False, sell_only=False) Place a given order list, usually from generate_orders().
update_order_price(extra_bid_pct=0, buy_only=False, sell_only=False) Reprice unfilled limit orders to the latest trade price (optionally moved by extra_bid_pct, valid range -0.1 to 0.1).
cancel_orders(buy_only=False, sell_only=False) Cancel all open orders, or only the buy or sell side.
get_order_info() Per symbol: price, current, target and order quantity, sorted by order value.
show_alerting_stocks() Taiwan market only: lists pending orders for stocks on the disposition or credit-alert lists, which need cash or shares deposited with the broker in advance.

Order Execution Best Practices

Limit orders with price adjustment:

# Default uses the last trade price as the limit; extra_bid_pct adjusts it
# Negative = below market (conservative), positive = above market (aggressive)
executor.create_orders(
    extra_bid_pct=-0.01  # Place orders 1% below the current price to avoid slippage
)

Execute immediately:

# Market orders on Schwab; on Taiwan brokers buy at the upper and sell at the lower price limit
executor.create_orders(market_order=True)

Batch orders (large orders):

import time

# Split large orders into 3 executions
# progress is a fraction of the *remaining* difference, not the original target
# After each fill the remaining gap shrinks, so the fraction must increase
for p in [1 / 3, 1 / 2, 1]:
    executor.create_orders(
        progress=p  # Execute 1/3, 1/2, then all of the remaining gap
    )
    time.sleep(60)  # 1-minute interval

Common Errors and Risks

1. Exceeding available capital

# Compare the target value with the account value before ordering
price = account.get_price([p['symbol'] for p in target])
required_cash = sum(float(p['quantity']) * price[p['symbol']] for p in target)
available = account.get_total_balance()

if required_cash > available:
    print(
        f"Insufficient capital! Required {required_cash:,.0f}, available {available:,.0f}"
    )
    # Lower the fund passed to Position.from_report() or add capital
else:
    executor.create_orders()

2. Quantity units differ by market

U.S. accounts trade in shares ({'AAPL': 10} is 10 shares). Taiwan accounts trade in lots of 1,000 shares, so a quantity of 1.5 means 1,500 shares, with the fraction sent as an odd-lot order.

3. Taiwan-only checks

Taiwan stocks have daily price limits and disposition (restricted-trading) lists. create_orders() clips limit prices to the price limits reported by Taiwan brokers, and show_alerting_stocks() lists stocks that need advance deposits. U.S. stocks have no daily price limits.

Order

@dataclass
class Order

Status of an order returned by Account.get_orders().

Field Type Description
order_id str Order ID from the broker API.
stock_id str Symbol, for example 'AAPL' (also available as symbol).
action Action Action.BUY or Action.SELL.
price Number Limit price.
quantity Number Total order quantity in the account's unit (shares in the U.S., lots in Taiwan).
filled_quantity Number Quantity filled so far.
status OrderStatus NEW, PARTIALLY_FILLED, FILLED or CANCEL.
order_condition OrderCondition Cash, margin, short-selling or day-trading condition.
time datetime.datetime Order time.
org_order Any The broker's original order object.

FAQ

Q: How do I ensure order safety?

Implement the following safety checks:

def safe_order_check(target, account, max_stocks=50, max_weight=0.15):
    """Pre-order safety check"""
    price = account.get_price([p['symbol'] for p in target])
    values = {p['symbol']: float(p['quantity']) * price[p['symbol']] for p in target}
    total = sum(values.values())

    # 1. Check position count
    if len(values) > max_stocks:
        raise ValueError(
            f"Too many positions ({len(values)}), exceeds limit of {max_stocks}"
        )

    # 2. Check single stock weight
    largest = max(values.values()) / total if total else 0
    if largest > max_weight:
        raise ValueError(
            f"Single stock weight too high ({largest:.1%}), exceeds limit of {max_weight:.0%}"
        )

    # 3. Check available capital
    available = account.get_total_balance()
    if total > available * 1.1:  # 10% buffer
        raise ValueError(
            f"Insufficient capital! Required {total:,.0f}, available {available:,.0f}"
        )

    print("Safety check passed")
    return True


# Usage
if safe_order_check(target, account):
    executor.create_orders()

Q: What if live performance differs from backtest?

Common causes:

  1. Slippage: Actual execution price differs from expected
  2. Solution: Use limit orders or adjust extra_bid_pct

  3. Insufficient volume: Cannot fully execute

  4. Solution: Trade in batches with progress, or filter for liquid stocks (for example etl:us_liquid_stock_filter)

  5. Timing differences: Backtest assumes execution at the configured trade price

  6. Solution: Set reasonable order timing (e.g., 30 minutes before the close)

Q: How do I set up automatic trading schedules?

import time

import schedule


def auto_trade():
    """Daily automatic trading"""
    try:
        # 1. Get the latest target (rerun the strategy first)
        target = Position.from_report(report, fund=100_000)

        # 2. Safety check
        if safe_order_check(target, account):
            # 3. Execute orders
            OrderExecutor(target, account=account).create_orders()
            print(f"Order completed: {time.strftime('%Y-%m-%d %H:%M:%S')}")
    except Exception as e:
        # 4. Error notification (Email, chat webhook)
        print(f"Order failed: {e}")


# Run daily at 15:30, 30 minutes before the 16:00 U.S. close (machine clock on New York time)
schedule.every().day.at("15:30").do(auto_trade)

while True:
    schedule.run_pending()
    time.sleep(60)

Q: How do I handle order failures?

try:
    executor.create_orders()
    print("Order successful")
except Exception as e:
    print(f"Order failed: {e}")

    # Log error
    with open('order_errors.log', 'a') as f:
        f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {e}\n")

    # Decide whether to retry based on error type
    if "network" in str(e).lower():
        time.sleep(30)  # Network issue, retry after 30 seconds
        executor.create_orders()

Resources


Final Reminder

Always preview orders and start small before using real capital!

Recommended Test Checklist: - Order workflow executes without errors - Position calculations are correct (match expectations) - Capital control is normal (no overspending) - Error handling mechanisms work - Notification mechanisms function properly - Runs continuously for 1 month without issues

Only consider using real capital after all items above are confirmed!