Skip to main content

Postprocessing

Code

Postprocessing directives provide a way to do additional postprocessing on the generated prices before they are returned to the user. These postprocessing directives may be stacked, meaning that the output of one can serve as the input into another. Below describes the supported postprocessing directives.

Arbitrage Removal

Detects and removes triangular arbitrage opportunities in a system of currency pairs.

Arbitrage is a financial strategy that involves buying a currency at a lower price and immediately selling it at a higher price in a different market, taking advantage of price discrepancies to make a profit.

This postprocessing directive operates by finding all currencies that can be directly exchanged from each currency, iterating over all combinations of these directly reachable currencies, and checking if there's an arbitrage opportunity. If such an opportunity is detected, the prices are modified to remove the opportunity.

remove_arbitrage_opportunity_for_single_timestep(prices)

This function takes a dictionary of price pairs as input. The keys of this dictionary are tuples (representing a pair of currencies).

The function returns an updated dictionary of prices with the first arbitrage opportunity that leads to the smallest overall percentage price change removed. If an arbitrage opportunity was detected and corrected, the function returns True; otherwise, it returns False.

Input

  • prices: A dictionary where keys can be tuples or strings, and values are floats. Each key represents a currency pair or a single currency, and each value represents the price for that pair or currency.

Output

  • prices: The updated dictionary of prices with the first arbitrage opportunity that leads to the smallest overall percentage price change removed.
  • boolean: True if an arbitrage opportunity was detected and corrected, False otherwise.

Algorithm

  1. Identify the pairs of currencies and individual currencies.
  2. For each currency, identify which other currencies it can be directly exchanged with.
  3. Iterate over all combinations of directly reachable currencies, and check if there's an arbitrage opportunity.
  4. If an arbitrage opportunity is found, calculate the ideal prices that would remove the opportunity.
  5. Keep track of the arbitrage opportunity that leads to the smallest overall percentage price change.
  6. Correct the prices for the identified arbitrage opportunity.

Complexity

The time complexity of this function is O(n^3), where n is the number of currencies. This is because we are iterating over all combinations of directly reachable currencies for each currency.

The space complexity is O(n), where n is also the number of currencies. This is because we store a dictionary of reachable currencies for each currency.

remove_arbitrage_opportunity(prices)

This function uses remove_arbitrage_opportunity_for_single_timestep() to remove arbitrage opportunities for multiple timesteps. It returns a dictionary of the corrected prices and a list of boolean flags indicating whether an arbitrage opportunity was found at each timestep.

Input

  • prices: A dictionary where keys are tuples representing currency pairs and values are lists of floats representing prices at each timestep.

Output

  • corrected_prices: A dictionary where keys are the same as the input dictionary and values are lists of corrected prices at each timestep.
  • arbitrage_flags: A list of boolean flags indicating whether an arbitrage opportunity was found at each timestep.

remove_all_arbitrage_opportunities(prices)

This function iteratively calls remove_arbitrage_opportunity() until no arbitrage opportunities are found in any timestep.

Input

  • prices: A dictionary where keys are tuples representing currency pairs and values are lists of floats representing prices at each timestep.

Output

  • prices: The same dictionary as the input, but with prices adjusted to remove all arbitrage opportunities.

Miscellaneous Design Decisions

All currency combinations are checked to find the arbitrage opportunity with the smallest percentage price change.

Caching is used to store the directly reachable currencies from each currency, reducing the time complexity.

Price Aggregation

The AggregationDirective class transforms input price data based on a set of predefined steps. The class can produce either standard price lists or Open-High-Low-Close (OHLC) data.

Class Parameters

  • steps (Dict[Union[AnyStr, SupportsInt], SupportsInt]): A dictionary defining the steps for the price aggregation.
  • is_ohlc (bool, optional): Flag to indicate if the data is OHLC data. Default to False.
  • reverse (bool, optional): Flag to indicate if the price data should be processed in reverse order. Default to False.

Class Methods

__init__(self, steps: Dict[Union[AnyStr, SupportsInt], SupportsInt], is_ohlc: bool = False, reverse: bool = False)

Initializes an instance of the AggregationDirective class.

execute(self, prices_array)

Processes an array of price lists and creates a corresponding aggregated list of prices for each list in the array based on the given steps.

is_reversed(self)

Returns True if the price data is processed in reverse order. Otherwise, it returns False.

get_ohlc(price_range, reverse=False)

Given a range of prices, it returns the open, high, low, and close prices as a list. If the reverse parameter is True, the function swaps the open and close prices.

Example Usage

import numpy as np

# Test the function when stepping forward (simulated prices)
_prices = np.array([i * 1.0 for i in range(250)])
_prices = _prices.reshape((1, 1, _prices.shape[0]))
_steps = {0: 1, 100: 10, 110: 1}
_is_ohlc = True
_directive = AggregationDirective(steps=_steps, is_ohlc=_is_ohlc, reverse=False)
_output = _directive.execute(_prices)
print(_output)

# Test the function when stepping backward (historical prices)
_directive = AggregationDirective(steps=_steps, is_ohlc=_is_ohlc, reverse=True)
_output = _directive.execute(_prices)
print(_output)

In the example above, we first create a list of ascending prices, reshape it to fit the expected input shape, define a step dictionary, and set the is_ohlc flag to True. Then, we instantiate the AggregationDirective class and execute it on the list of prices. Finally, we print the result. We do this twice, once for ascending (simulated) prices and once for descending (historical) prices.

Rescaler

The RescalerDirective class rescales the final prices while keeping the overall simulated shape. The rescaling is done such that the starting price never changes but the ending price is a targeted range based on the expected value. The expected value here is defined as the 90% confidence interval of the price after t time steps given the historical standard deviation of price returns and the provided market drift. This is done for each asset based on its configured market drift.

Class Parameters

  • market_drift_scale_factor float: A value that scales the derived mu metric for the expected value. This value is provided by the user but defaults to 1.0. The formula is mu = market_drift * standard_deviation_of_returns * market_drift_scale_factor.
  • confidence_interval float: A value that indicates the confidence interval to use for getting lower and upper bounds. This is set between 0.0 and 0.99. For example, the default value of 0.9 indicates to use the 90% confidence interval.
  • rescale_per_step bool: Defaults to False. A value indicating whether to rescale the price for every step rather than the first step. Specifically, this derives the 90% confidence interval for each step rather than the last step and assigns the price from a random selection of this interval for every step. Note that this is effectively regenerating the full price series rather than rescaling it per se.
  • index_to_token_mapping Dict[int, AnyStr]: Provides a mapping of the index back to the asset. This is an implementation detail.