Package: trapeza
trapeza - simulation and backtesting of financial transactionsAbouttrapezais a library for simulation and backtesting of financial transactions and in particular of tradings strategies.There is a number of great backtesting libraries available for Python, which facilitate fast and easy-to-write
backtesting.On the other hand,trapezafocuses on:Flexibility:Thetrapezainterface is designed for flexibility and customization such that a fast variety of
real-life situation can be modelled appropriately, e.g. custom fees for certain transaction types (for instance
volume dependent fees).Simulation & Modelling:At its core,trapezaimplements a time-discrete (stack) automaton logic. Thereby,
transactions can be modelled with processing times, which would also occur in real-life situations such as time
needed to transmit messages via telecommunication networks. The elapsed time is controlled via setting an internal
clock. Furthermore, the user can control how much previous (historic) price and market information is available to
the user-defined trading strategy at each time step of simulation, i.e. when modelling path-dependent strategies.trapezacan handle multiple stocks and assets when defining a strategy.Stochastic Analysis:trapeza's backtest engine does not only backtest on historic market data, but furthermore
slices input data, such as price and volume market data, into many sub-samples to analyze the performance of
trading strategies. This roughly emulates the logic of Monte Carlo simulations by emulating random sub-samples of
historic market data, which is especially useful when analyzing path-dependent strategies.Additionally, common metrics assume very basic distribution models (e.g. Gaussian normal distribution). Instead,
custom and more suitable distributions can be applied for calculating metrics. Even toughtrapezaprovides some
basic metrics, the main goal oftrapezais not to provide sophisticated metrics or indicators.Visualization:Even though sophisticated visualizations of analysis results is not the main goal oftrapeza,trapezaoffers a very basic, browser-based visualization dashboard implemented via Python Dash and
Plotly.trapezadoes not mainly focus on speed. Nevertheless, large parts of the library are transpiled to C/ C++ via Cython.The name"trapeza"is derived from the tables, which were used by ancient greek moneychangers for their activities - one
of the first records in history regarding banking and monetary activities.Legal Disclaimertrapezais written for scientific, educational and research purpose only. The author of this software and accompanying
materials makes no representation or warranties with respect to the accuracy, applicability, fitness, or completeness of
the contents. Therefore, if you wish to apply this software or ideas contained in this software, you are taking full
responsibility for your action. All opinions stated in this software should not be taken as financial advice. Neither
as an investment recommendation. This software is provided "as is", use on your own risk and responsibility.This software is licensed underMIT License.Quickstarttrapezais built upon three main concepts:Account:A account conceptually represents a bank account/ trading depot. Basic transactions like deposit,
withdraw, sell or buy an asset are implemented on account level. Each account keeps a record of positions
regarding assets, which are hold by the account owner (called account.depot in this context).Strategy:Users can define a custom trading strategy, which only have to follow the call signaturestrategy_func(accounts, price_data_point, reference_currency, volume_data_point, fxstrategy, **strategy_kwargs).
This strategy function, and a list of accounts (strategy function may use multiple accounts in its function body)
is then passed to a strategy object, which handles simulation execution and collecting simulation results.Engine:Multiple strategy functions can be passed to the engine object. The engine object slices market data
(i.e. price and volume data) into random sub-samples in order to perform Monte-Carlo-like stochastic simulation.
The engine object furthermore evaluates results with regard to user-defined metrics based on the simulation
results for each of the supplied strategies. This way, different strategies can be benchmarked against the same
market at once.Accountfromtrapeza.accountimportFXAccount# initialize an account with reference currency set to EUR# if the user wants more performance at the cost of safety: parameter ignore_type_checking=Trueacc=FXAccount('EUR')# deposit 100 EUR and take a fee of 5 EURacc.deposit(100,'EUR',5)# get current depot status, returns a dictprint(acc.depot)# >>> {'EUR': 95}# suppose we have to transfer 50 EUR to a friend's account and the transaction takes 5 time steps to be processed at our# account and 10 time steps to arrive at our friend's accountacc.transfer(friend_account,50,'EUR',payer_processing_duration=5,payee_processing_duration=10)# check depot status againprint(acc.depot)# >>> {'EUR': 95}     transfer has not taken effect yet, we first have to proceed in time steps# proceed one time stepacc.tick()print(acc.depot)# >>> {'EUR': 95}   transfer has not taken effect yet as it takes 5 time steps# set internal clock to 5 (fast forward...)acc.tick(5)print(acc.depot)# >>> {'EUR': 45}   transfer is processed on our account nowStrategyimportnumpyasnpfrompandas_datareaderimportdatafromtrapeza.strategyimportFXStrategy# define a custom trading strategydefavg_strategy(accounts,price_data_point,reference_currency,volume_data_point,fxstrategy):# We sell if the average-10-days-line crosses the average-50-days-line from above and buy if it crosses from below# price_data_point and volume_data_point: dict, tuples(currency_1, currency_2) as dict key,#                                               list of floats representing exchange rates at distinct time steps#                                               (here 52 time steps: current time_step at index=-1, lookback data of#                                               51 time steps is prepended, see below for explanation) as dict value# reference_currency: str, reference currency in which to evaluate strategy, can be different then the reference#                     currency of accounts# accounts: list of accounts, which are passed to FXStrategy# fxstrategy: placeholder for FXStrategy, which calls itself, e.g. for adding trade signals to FXStrategyavg_10_today=np.sum(price_data_point['BTC','EUR'][-10:])/10avg_50_today=np.sum(price_data_point['BTC','EUR'][-50:])/50avg_10_yesterday=np.sum(price_data_point['BTC','EUR'][-11:-1])/10avg_50_yesterday=np.sum(price_data_point['BTC','EUR'][-51:-1])/50price_today=price_data['BTC','EUR'][-1]ifavg_10_yesterday<avg_50_yesterdayandavg_10_today>avg_50_today:accounts[0].buy(1,price_today,'BTC','EUR')# buy 10 unitsfxstrategy.add_signal(accounts[0],'buy')# add signal associated with account 0ifavg_10_yesterday>avg_50_yesterdayandavg_10_today<avg_50_today:accounts[0].sell(1,price_today,'BTC','EUR')# sell 10 unitsfxstrategy.add_signal(accounts[0],'sell')# add signal associated with account 0# first deposit some more cash to accountacc.deposit(1_000_000,'EUR')acc.deposit(2,'BTC')# initialize strategy object# Parameter lookback controls how many previous time steps of market data (additional to current time step) is passed to# our custom strategy function. For our strategy we need to calculate a 50-days average. Furthermore we have to calculate# this value for one time step before we start analyzing our trading strategy (i.e. line crossing is detected by comparing# avg-10 and avg-50 of the previous day and the current day). Therefore, the lookback is 50 (avg-50 calculation) + 1# (calculation of avg-50 for previous day): lookback=51.# if the user wants more performance at the cost of safety: parameter ignore_type_checking=Truestrategy=FXStrategy('my_awesome_strategy',acc,avg_strategy,lookback=51)# We pull currency data from yahoo finance. This is somehow a bit tediousbtc_eur=data.DataReader('BTC-EUR',start='2020-06-01',end='2021-05-12',data_source='yahoo')['Close'].reset_index().drop_duplicates('Date','last')eur_usd=data.DataReader('EURUSD=X',start='2020-06-01',end='2021-05-12',data_source='yahoo')['Close'].reset_index().drop_duplicates('Date','last')btc_usd=data.DataReader('BTC-USD',start='2020-06-01',end='2021-05-12',data_source='yahoo')['Close'].reset_index().drop_duplicates('Date','last')# EUR|BTC is only traded on work days as opposed to cryptocurrencies, which are traded 365 days a year# so we need to restrict our data to work days only in order to be comparablecurrency_dates=eur_usd['Date'].to_numpy()crypto_dates=btc_eur['Date'].to_numpy()common_dates=np.sort(np.array(list(set(currency_dates).intersection(set(crypto_dates)))))# filter out common daysbtc_eur=btc_eur[btc_eur['Date'].isin(common_dates)]['Close'].to_numpy()btc_usd=btc_usd[btc_usd['Date'].isin(common_dates)]['Close'].to_numpy()eur_usd=eur_usd[eur_usd['Date'].isin(common_dates)]['Close'].to_numpy()# finally we can build our input dataprice_data={('BTC','EUR'):btc_eur,('EUR','USD'):eur_usd,('BTC','USD'):btc_usd}# run backtest and evaluate our strategy in USD, even though our account is referenced and billed in EUR# conversion from BTC and EUR (which are hold in account's depot) is done automaticallystrategy.run(price_data,'USD')# results can be accessed via attributesprint(strategy.merged_total_balances)# returns total value over all accounts (in our case one account) given in USD# >>> [1196651.6894629002, 1196876.5245821476, 1203001.5259501934, ... , 1313077.8513996354]print(len(price_data['BTC','EUR']),len(strategy.merged_total_balances))# >>> 245 194   strategy runs over 194 time steps, we supplied 245 time steps of market data (of which 51 are used as#               lookback data for calculating our trading decisions)Results can be visualized with any Python library of choice (here: matplotlib). Notice, that strategy execution starts
at time step 51 (which elucidates the concept of lookback data):(BTC|EUR price and total value of trading strategy/ performance at each time step, both indexed to 1 at start time of
strategy execution at time step 51)(BTC|EUR price, avg10 and avg50 indicators at each time step)As we can see from the plots, this strategy doesn't seem to outperform a basic buy-and-hold-strategy by much for the
chosen time frame (which makes sense regarding the bullish trend within the market data).Enginefromtrapeza.engineimportFXEnginefromtrapeza.dashboardimportFXDashboard# initialize engine# if the user wants more performance at the cost of safety: parameter ignore_type_checking=Trueengine=FXEngine('backtesting',strategy,n_jobs=-1)# n_jobs: use all available cores# Run stochastic backtesting wherein the length of a randomly drawn sub-sample is at least 5 time steps and at most# 50 time steps. All possible sub-samples, which comply to this condition, will be drawn. Alternatively, the max number# of draws can be defined via max_total_runs parameter. In this case we only draw 200 sub-samples.engine.run(price_data,'USD',min_run_length=5,max_run_length=50,max_total_runs=200)# We now can analyze results. FXEngine has a set of standard metrics, but users can pass custom metrics as well.defcustom_metric(x):returnnp.var(x)engine.analyze({'my_metric':custom_metric})# Results are stored in self.standard_analysis_results for the standard metrics implemented in FXEngine, and in# self.analysis_results for custom metricsprint(engine.standard_analysis_results)# metrics are evaluated for each strategy and for each sub-sample, which is defined by the window length (here between 5# and 50) and by its start time step (index in time series data)# >>> {'my_awesome_strategy':#       {('10', '138'):#           {'total_rate_of_return': -0.3178841284640982, 'volatility': 0.05711533033257823,#            'expected_rate_of_return': -0.001678860489711134, 'expected_rate_of_log_return': -0.0016867536858783518,#            'sharpe_ratio': -7.924901653751582, 'sortino_ratio': -8.217863784511843, 'value_at_risk': -0.09134675866458732,#            'max_drawdown': 19997.846368551254#           },#        ('10', '140'):#           {'total_rate_of_return': -0.009707405842422023, ...#           },#        ...#        }#     }print(engine.analysis_results)# >>> {'my_awesome_strategy':#       {('10', '138'):#           {'my_metric': 46553148.03839398#           },#        ('10', '140'):#           {'my_metric': 30596348.629725147#           },#        ...#       }#     }# Furthermore, a simple dashboard visualization is provided by trapezadash=FXDashboard(engine)dash.run(debug=False)# dashboard can be opened in browser with local address# >>> Dash is running on http://127.0.0.1:8050/#     * Serving Flask app "script_doc_examples" (lazy loading)#     * Environment: production#       WARNING: This is a development server. Do not use it in a production deployment.#       Use a production WSGI server instead.#     * Debug mode: off# DO NOT FORGET TO CLOSE: this deletes all temporary cached files (even though this should also work without calling# self.close() explicitly - but safe is safe)engine.close()Even thoughtrapezadoes not focus on visualization, this library still provides a very basic dashboard:Nonetheless, there are certainly better libraries for visualization then the one provided bytrapeza.All the above example code located at:doc/script_readme_examples.py.More examples at:docs/examples.md(e.g. RSI-based trading strategy).Custom ImplementationsFXAccount, FXStrategy, FXEngine and FXDashboard all inherit from respective base classes (i.e. BaseAccount, BaseStrategy,
BaseEngine, BaseDashboard). Own implementations can be written as drop-in replacement.If those implementations shall be compatible in their use with existing classes, then they also have to inherit from
those base classes.The FX-implementations are all focused on (infinitely) divisible (often intangible) assets, such as currencies. Those
implementations can also be easily adapted for the use with e.g. stocks if rounding of volumes at FXAccount.buy() and
FXAccount.sell() is done appropriately to the next integral number.Quick InstallPip installInstallation is as easy as:pip install trapezaThis should work for Windows, Linux and MacOS.Tested under Windows10/ Python3.6.7 and Ubuntu-20.04.2/ Python3.8.5.Not tested under MacOS yet.Build from Sourcetrapezacan also be re-compiled and built from source, see the documentation regardinginstallation.This is useful for development purpose or if the user has complications with mpdecimal (external C-library used for
decimal math) and wants to re-compile mpdecimal separately, which also induces a re-compilation oftrapeza.Make sure topip install -e .after re-compiling.RequirementsRequirements fortrapeza(installs automatically if not already satisfied):numpy
pandas
scipy
joblib
scikit-learn
dash
plotly
matplotlib
flask
setuptools
pathlib; python_version < "3.4"DocumentationTable of ContentInstallationMain ConceptsAccountOrder ManagementStrategyEngineVisualizationMetricsArithmetics(Context Settings)ExceptionsExamplesDevelopment NotesAPI ReferenceSource CodeSource code and documentation can be found at:https://gitlab.com/LHuebser/trapezaDevelopment Roadmaptrapezais under active development and released as beta.Contribution and suggestions for improvements and future features are warmly welcome!SeeDevelopment Notes.Current Statustrapezais currently in its beta version. All provided classes, methods and functions are fully functional and extensively tested.So far, the implementationsFXAccount, FXStrategy and FXEngineare conceptually tailored for monetary exchange transactions - or
more generally speaking for continuously divisible assets.
Nonetheless, if rounding to integral numbers is applied to trade volumes, then those concepts are also applicable for stocks
(i.e. base=stock, quote=currency).Current FXDashboard implementation has only very basic and limited visualization capabilities and only makes sense to use
in conjunction with classes, that implement Monte-Carlo-alike backtesting (e.g. FXEngine). Therefore, visualization in
FXDashboard focuses on representing distributions of metrics over multiple backtesting runs with randomly sub-sampled data.FXEngine limits the types of metrics, which can be evaluated. Only metrics, that output a single value given a time series of
a strategy's total value (calculated in a given reference currency) per time step are compatible with FXEngine.Metrics provided intrapeza.metricsare not checked in a scientific manner and are not compared to the latest research or
to the up-to-date state-of-the-art.Metrics implemented in this software do not state financial advice or investment
recommendations, nor do they make any representation or warranties with respect to the accuracy, applicability, fitness,
or completeness of the contents. Therefore, if you wish to apply this metrics or ideas contained in this software, you
are taking full responsibility for your action.To-DosSometimesFXEnginedoes not use all cores, even if it is specified by method arguments. This is
due to some unknown behavior ofjoblib(possibly interacting
with BLAS ofnumpy), which is used for multi-processing intrapeza. This needs further
investigation but does not limit the general capability oftrapeza.emin and emax in libmpdec (wrapper for mpdecimal C-library) are currently left at default (this should work without
any troubles), customization would be good.extend examples, e.g. risk models (i.e. via sklearn), usage of other financial Python packages in conjunction withtrapeza, etc.Implement win-loss metric in trapeza.metrics.Add an example where price data is generated dynamically inside the strategy decision function, e.g. when modelling
option or turbo bull prices (which change depending on the underyling) such that those prices do not have to be passed
within the price_data_dict argument supplied to FXStrategy.run() but are generated dynamically inside the strategy
decision function.Add benchmarks for testing the accuracy of the implemented metrics in comparison to other Python packages.Add benchmarks regarding backtesting strategies (compute time and accuracy) in comparison to other Python packages.Full To-Do listFuture DevelopmentAdd further FX market specific mechanisms, e.g. roll-overs and margin trading (or dividends and ex-dividend prices
when handling stocks).Improve visualization in FXEngine.Implementation and integration of fixed point arithmetics as alternative to 'FLOAT', 'DECIMAL',
'LIBMPDEC_FAST' (uses ryu dtoa), 'LIBMPDEC' (uses dtoa of CPython) when setting trapeza.context.ARITHMETICSSee todo in trapeza.arithmetics for adding rounding methods when implementing fixed point arithmetics.--> Currently, there is no urgent need for this as fixed point math can be emulated via
trapeza.context.ARBITRARY_QUANTIZE_SIZE setting. Only reason for implementing fixed point math separately would be
runtime performance...Implement more order types (Order Management).Implement moremetrics: tail value at risk, bias ratio, win-loss-ratio, average absolute deviation,
calmar ratio, long short exposure, sector exposure, etc. and more probability models (e.g. chebyshev) for calculating
metrics.Implement a delay element at order management between hitting limit price and actual order execution (price), e.g.
there's a difference between stop loss and stop loss limit, which are currently the same.Improve computation time of FXEngine.analyze().Implement some kind of liquidity simulation, which makes backtesting more realistic especially for larger trading
volumes.LicenseMIT LicenseCopyright (c) 2021 Louis HuebserPermission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.Icon Attribution