Atlas

Published at Feb 24, 2024

#Atlas

Atlas is a high performance Algorthmic Trading Backtesting library written in C++23 with a Python wrapper build using pybind11 and a GUI based on QT. The core library is built on top of Eigen and uses vectorization and linear algebra to achieve high performance. The library is designed to be modular and extensible with the ability to create complex strategies from a set of simple building blocks. Currently the only supported build is windows with Visual Studio and vcpkg toolchain. A port to cmake and linux/mac is planned in the future.

While the core engine is written in C++, AtlasPy provides python bindings via pybind11 which allows for rapid prototyping and testing of strategies. This incurrs only a minor performance overhead as all the heavy lifting is done in C++, with python simply creating the building blocks. Furthermore, it was designed to be mixture of VectorBT and bt. I have found VectorBT to be a great library, but it in my opnion it is not designed for horizontal scaling, i.e. multi asset and multi strategy simulations. A core feature of Atlas is the ability to test multiple strategies on multiple assets in parallel. Additionally, BT gets a lot of things right and is designed with proffessional quant trading features, but is still painfully slow at times. This limits the complexity and scale in which you can operate.

Example Simulations are broken down into several componenets, the core of which is the Exchange and the Strategy. The Exchange is a group of assets that share the same frequency and the same features. They do not nessecarily have to have the same datetime index, but the must have the same columns and have one column the matches “close” case insensitive. The easiest way to load, is to create a folder with a set of csv files. Each file represents a unique asset and the name of the file is the name of the asset. In this case, the first index is expected to be be a datetime index, with the rest being able to be parsed as a floating point number. To start, lets use bt data loading features:

tickers = 'aapl,msft,c,gs,ge,jnj,pg,ko,amzn,jpm,adbe,ma,dis,txn'
data = bt.get(
    tickers,
    start='2010-01-01',
    end = '2024-01-01'
)
data.tail()

out_path = r' ... /data/sp500'
for col in data.columns:
    df_ticker = pd.DataFrame(data[col])
    df_ticker.columns = ["close"]
    df_ticker.to_csv(os.path.join(out_path, col + '.csv'))

Now we have to load in the Atlas.dll and AtlasPy.pyd shared libraries. From there we can create a simple exchange that hold all of these assets listed above

atlas_path = "C:/Users/natha/OneDrive/Desktop/C++/Atlas/x64/Release"
sys.path.append(atlas_path)

from AtlasPy.core import Hydra, Portfolio, Strategy
from AtlasPy.ast import *

exchange_path = r'... test/data/sp500'
strategy_id = "test_strategy"
exchange_id = "test_exchange"
portfolio_id = "test_portfolio"

hydra = Hydra()
intial_cash = 100.0
exchange = hydra.addExchange(exchange_id, exchange_path, "%Y-%m-%d")
portfolio = hydra.addPortfolio(portfolio_id, exchange, intial_cash)
hydra.build()

The last function is the build that should be called after all data has been loaded. It is responsible for alligning all of the data in the indivual exchanges into a single matrix, and then creating the unified datetime index to loop over. We start with a simple strategy that goes long all assets whose closing price are above their 50 period moving average.

close = AssetReadNode.make("close", 0, exchange)
ma = MeanObserverNode(close, 50)
spread = AssetOpNode.make(close, ma, AssetOpType.SUBTRACT)
filter = ExchangeViewFilter(ExchangeViewFilterType.GREATER_THAN, 0.0, None)
ev = ExchangeViewNode.make(exchange, spread, filter)

allocation = AllocationNode.make(
    ev
)
strategy_node_signal = StrategyNode.make(allocation, portfolio)
strategy = hydra.addStrategy(Strategy(strategy_id, strategy_node_signal, 1.0), True)
strategy.enableTracerHistory(TracerType.NLV)

There is a lot at play here, but the core idea is every period we read in the closing prices, subtract the 50 period moving average, and the filter out those that are less than 0. We then allocate the strategy uniformly to all assets that remain. To run, we call the run method on the manager and time it to see how fast we are.

st = time.perf_counter_ns()
hydra.run()
et = time.perf_counter_ns()
print(f"Time to Run: {(et-st)/1e6}ms")

The above takes around 700 us running with the full release mode, compared to 1.7s taken for BT a speed up of over 2000x.

Atlas © 2024