finlab.market
The Market object defines how the backtesting system retrieves price data, benchmark indices, market characteristics, and other information. FinLab has built-in support for US stocks and ETFs, as well as Taiwan stocks and the Taiwan Emerging Stock Board. You can also create custom markets by inheriting the Market class.
Market Object Concept
Each market has different characteristics:
- Price data source: Where to get open, close, volume, and other price data
- Benchmark index: Benchmark for performance comparison (e.g., the S&P 500 for US stocks)
- Trading hours: Open/close times and timezone
- Market rules: Price limits, transaction costs, special stock categories
Through market objects, the backtesting engine can:
- Retrieve the correct price data
- Calculate correct returns
- Display performance comparison against the benchmark
- Support time-based decisions for live trading
Built-in Market Comparison
| Feature | USMarket | USFundMarket | TWMarket | ROTCMarket |
|---|---|---|---|---|
Market code (data.set_market) |
'us' |
'us_fund' |
'tw' |
'rotc' |
Market name (get_name()) |
'us_stock' |
'us_fund' |
'tw_stock' |
'rotc_stock' |
| Data frequency | Daily ('1D') |
Daily ('1D') |
Daily ('1D') |
Daily ('1D') |
| Benchmark | S&P 500 (^GSPC) |
S&P 500 (^GSPC) |
Taiwan weighted return index | Taiwan weighted return index |
| Timezone | US/Eastern | US/Eastern | Asia/Taipei | Asia/Taipei |
| Market close | 16:00 | 16:00 | 15:00 | 15:00 |
| Default fee / tax | 0 / 0 | 0 / 0 | 0.1425% / 0.3% | 0.1425% / 0.3% |
| Price limit | None | None | 10% | None |
| Data source | finlab.data |
finlab.data |
finlab.data |
finlab.data |
data.set_market() selects the market for both data.get() and backtest.sim(), so after data.set_market('us') you rarely need to pass market= yourself.
Use Cases
1. Using Built-in Markets
from finlab import data, backtest
from finlab.markets.us import USFundMarket, USMarket
# US stock backtest: set_market('us') selects USMarket automatically
data.set_market('us')
close = data.get('price:adj_close')[['AAPL', 'MSFT', 'NVDA']]
position = close > close.average(50)
report = backtest.sim(position, resample='W')
# Equivalent to
report = backtest.sim(position, resample='W', market=USMarket())
# US ETF backtest
data.set_market('us_fund')
etf_close = data.get('price:adj_close')
etf_position = etf_close[['SPY', 'QQQ']].pct_change(126).is_largest(1)
report = backtest.sim(etf_position, resample='M', market=USFundMarket())
Taiwan stocks are the package default (data.set_market('tw'), TWMarket); everything above works the same way there.
2. Custom Market - Cryptocurrency
from finlab.market import Market
import pandas as pd
class CryptoMarket(Market):
@staticmethod
def get_name():
return 'crypto'
def get_price(self, trade_at_price='close', adj=True):
df = pd.read_csv('crypto_close.csv', index_col=0, parse_dates=True)
return df
@staticmethod
def get_benchmark():
df = pd.read_csv('crypto_close.csv', index_col=0, parse_dates=True)
return df['BTC'] # Use BTC as benchmark
# Usage
report = backtest.sim(position, market=CryptoMarket())
3. Custom Market - Futures (with leverage)
Warning
This leveraged-price example resets exposure daily and omits per-lot costs, rounding, margin and rolls. For Taiwan single-stock futures with real contract lots, see TWStockFuturesMarket stage one.
class FuturesMarket(Market):
def __init__(self, leverage=10):
self.leverage = leverage
self.prices = pd.read_csv('futures_price.csv', index_col=0, parse_dates=True)
@staticmethod
def get_name():
return 'futures'
def get_price(self, trade_at_price='close', adj=True):
price = self.prices[trade_at_price]
daily_return = price.pct_change() * self.leverage
leveraged_price = (1 + daily_return).fillna(1).cumprod() * 100
return leveraged_price
# Use 10x leverage
report = backtest.sim(position, market=FuturesMarket(leverage=10))
Common Custom Market Scenarios
| Scenario | Description | Examples |
|---|---|---|
| Cryptocurrency | 24-hour trading, no price limits | BTC, ETH, BNB |
| Futures | Leverage effect, need to simulate leveraged returns | E-mini S&P 500, stock futures |
| Other markets | Markets not in the FinLab database | European or Canadian stocks |
| Custom data | Load prices from CSV, API | Private data, simulated data |
| Mixed markets | Backtest combinations of multiple markets | US stocks + ETFs + Crypto |
Detailed Guide
See Custom Market Object Guide for:
- Complete Market class API documentation
- Required vs optional methods
- How to handle multiple timezones
- How to implement market holiday logic
- Loading data from CSV, API, FinLab database (mixed)
- 3 practical examples (Gold ETF, Global Market, Crypto 4H)
API Reference
Market
Base class for markets. To backtest a new market, inherit from finlab.market.Market and override the methods below. USMarket, USFundMarket, TWMarket and the other built-in markets are subclasses.
A subclass that sets the class variables _price_table (for example 'us_price') and _adj_prefix (for example 'us_price:adj_') inherits a working get_price() that reads prices from finlab.data. Markets with a different schema override get_price().
Data methods
get_price()
def get_price(self, trade_at_price: str | pd.Series | pd.DataFrame, adj: bool = True) -> pd.DataFrame
Return the prices or volume used by the backtest.
| Parameter | Type | Description |
|---|---|---|
trade_at_price |
str, pd.Series or pd.DataFrame |
'open', 'close', 'high', 'low' or 'volume'; or custom price data. A Series is returned as a one-column DataFrame, a DataFrame is returned unchanged. |
adj |
bool |
Use adjusted prices. Ignored for 'volume'. |
Returns: pd.DataFrame indexed by date with one column per symbol.
Implement at least 'open', 'close', 'high' and 'low': they are the trading prices and feed get_trading_price(). Implementing 'volume' is strongly recommended: liquidity analysis, finlab.ml.qlib features and PortfolioSyncManager need it. Supporting custom Series/DataFrame input lets users backtest with their own prices.
get_trading_price()
Return the prices used to fill trades. name is 'open', 'close', 'high', 'low', 'open_close_avg', 'high_low_avg' or 'price_avg' (the average of open, close, high and low). Built on get_price(); raises ValueError for other names.
get_benchmark()
Return the benchmark price series (date index) that reports compare the strategy against. The base class returns an empty Series, which hides the benchmark. USMarket returns data.get('world_index:adj_close')['^GSPC'], the S&P 500.
get_market_value()
Return market values indexed by date with one column per symbol. The base class returns an empty DataFrame.
get_asset_id_to_name()
Return a mapping from symbol to display name, used to label holdings in reports. USMarket maps each symbol to its company name, for example {'AAPL': 'Apple Inc.'}. The base class returns {}.
get_industry()
Return a mapping from symbol to industry. USMarket reads the industry column of us_company_profile. The base class returns {}.
get_delisting_notices()
Return delisting notices announced by the exchange: one row per notice, indexed by symbol, with columns announced (announcement date) and delisted (delisting date). A symbol can have several rows. The backtest sells a holding on the first trading day after the announcement and stops buying it. Symbols without a notice exit at their last quote when their prices end early. The base class returns an empty table.
Market rules
| Method | Returns | Base class |
|---|---|---|
get_name() |
Name of the market data source, saved in reports (e.g. 'us_stock') |
'auto' |
get_freq() |
Data frequency used to resample non-daily data, saved in the report | '1D' |
market_open_time() |
(hour, minute) of the market open in local time |
(9, 0); USMarket: (9, 30) |
market_close_at_timestamp(timestamp=None) |
Market close for the given timestamp; defaults to the last price date | Last price date plus the smallest gap between price dates; USMarket: 16:00 US/Eastern |
tzinfo() |
Market timezone | None |
default_fee_ratio() |
Default commission ratio used by sim() |
0.001425; USMarket: 0 |
default_tax_ratio() |
Default transaction tax ratio used by sim() |
0.003; USMarket: 0 |
get_reference_price() |
Most recent close per symbol as a dict | Last row of get_price('close', adj=False) |
get_odd_lot() |
Odd-lot size | 1 |
get_board_lot_size() |
Board-lot size | 1 |
Quick Development Tips
The simplest custom market only requires implementing two methods:
get_name(): Return market nameget_price(): Return price DataFrame
All other methods are optional and can be implemented as needed.
Common Errors
get_price()must return a DataFrame with DatetimeIndex as index- columns should be stock tickers (str)
- Missing values should be handled with forward fill
FAQ
Q: How do I handle adjusted prices (adj=True)?
def get_price(self, trade_at_price='close', adj=True):
if adj:
# Return adjusted prices accounting for dividends and splits
return pd.read_csv('adj_close.csv', index_col=0, parse_dates=True)
else:
# Return raw closing prices
return pd.read_csv('raw_close.csv', index_col=0, parse_dates=True)
Q: Why is get_benchmark() needed?
The benchmark index is used for performance comparison. When report.display() is called, it shows the strategy vs benchmark return curve. If no benchmark is needed, return an empty Series:
@staticmethod
def get_benchmark():
return pd.Series(
dtype=float, index=pd.DatetimeIndex([])
) # Do not display benchmark
Q: Can I use finlab.data.get() inside get_price()?
Yes. This is the standard way to combine FinLab datasets, for example US stocks and ETFs in one market:
import pandas as pd
from finlab import data
from finlab.market import Market
class StocksAndEtfsMarket(Market):
def get_price(self, trade_at_price='close', adj=True):
stocks = data.get('us_price:adj_close')[['AAPL', 'MSFT']]
etfs = data.get('us_fund_price:adj_close')[['SPY', 'QQQ']]
return pd.concat([stocks, etfs], axis=1)
Q: How do I simulate transaction costs?
Transaction costs are set in the backtest.sim() function, not in the Market object:
from finlab.markets.us import USMarket
# A broker that charges 0.05% commission per trade; U.S. stocks have no transaction tax
report = backtest.sim(
position,
resample='W',
market=USMarket(),
fee_ratio=0.0005,
tax_ratio=0,
)