The # Your code is there for you to do yourself.
**The # Solution start and # Solution end are only for helping you.Please delete the Solution and try doing it yourself…**For completing the code, please refer to the Classiq documentation, Classiq documentation, or Classiq Library.Search for the required quantum function to find its corresponding documentation page.
In finance, a crucial aspect of asset pricing pertains to derivatives.Derivatives are contracts whose value is contingent upon another source, known as the underlying.The pricing of options, a specific derivative instrument, involves determining the fair market value (discounted payoff) of contracts affording their holders the right, though not the obligation, to buy (call) or sell (put) one or more underlying assets at a predefined strike price by a specified future expiration date (maturity date).This process relies on mathematical models, considering variables like current asset prices, time to expiration, volatility, and interest rates.In many financial models we are often interested in calculating the average of a function of a given probability distribution (E[f(x)]).The most popular method to estimate the average is Monte Carlo [2] due to its flexibility and ability to generically handle stochastic parameters.Classical Monte Carlo methods, however, generally require extensive computational resources to provide an accurate estimation.
By leveraging the laws of quantum mechanics, a quantum computer may provide novel ways to solve computationally intensive financial problems, such as risk management, portfolio optimization, and option pricing.The core quantum advantage of several of these applications is the Amplitude Estimation algorithm [3] which can estimate a parameter with a
convergence rate of Ω(1/M2), compared to Ω(1/M) in the classical case, where M is the number of Grover iterations in the quantum case and the number of the Monte Carlo samples in the classical case.This represents a theoretical quadratic speed-up of the quantum method over classical Monte Carlo methods!
An option is the possibility to buy (call) or sell (put) an item (or share) at a known price - the strike price (K), where the option has a maturity price (S).The payoff function to describe for example a call option will be:f(S)={0,S−K,if K≥Sif K<SThe maturity price is unknown. Therefore, it is expressed by a price distribution function, which may be any type of a distribution function.For example a log-normal distribution: ln(S)∼N(μ,σ),
where N(μ,σ) is the standard normal distribution with mean equal to μ and standard deviation equal to σ.In the case of a rainbow options, the payoff function is defined by the maximum of the maturity prices of multiple assets. “Best of”, The best-performing asset is chosen as a reference for payoff calculation.For call options, the payoff function is defined as follows:f(S)=max(S−K,0)Where, in this case, S=max(Sˉt).There is another type of asset called “worst of”, where the worst-performing asset is chosen as a reference for payoff calculation. We will not treat this type of option in this notebook.
To estimate the average option price using a quantum computer, we need to:
Load the distribution, that is, discretize the distribution using 2n points (n is the number of qubits) and truncate it.
Implement the affine transformation to bring the assets to the maturity date.
Implement the payoff function for rainbow options and make amplitude loading using control Ry rotations.
Evaluate the expected payoff using iterative amplitude estimation.
The algorithmic framework is called Quantum Monte-Carlo Integration.For a basic example, see QMCI.
In the link, we use a simular framework to estimate European call option, where the underlying asset distribution at the maturity data is modeled as log-normal distribution.
Encode the probability distribution of a discrete multivariate random variable W taking values in {w0,..,wN−1} describing the assets’ prices at the maturity date.The number of discretized values, denoted as N, depends on the precision of the state preparation module and is consequently connected to the number of qubits n by N=2n.i=0∑N−1p(wi)∣wi⟩
def gaussian_discretization(num_qubits, mu=0, sigma=1, stds_around_mean_to_include=3): """ Discretizes a Gaussian distribution into a set of sample points and their corresponding probabilities for using QNums more accurately. """ lower = mu - stds_around_mean_to_include * sigma upper = mu + stds_around_mean_to_include * sigma num_of_bins = 2**num_qubits sample_points = np.linspace(lower, upper, num_of_bins + 1) def single_gaussian(x: np.ndarray, _mu: float, _sigma: float) -> np.ndarray: cdf = scipy.stats.norm.cdf(x, loc=_mu, scale=_sigma) return cdf[1:] - cdf[0:-1] non_normalized_pmf = (single_gaussian(sample_points, mu, sigma),) real_probs = non_normalized_pmf / np.sum(non_normalized_pmf) return sample_points[:-1], real_probs[0].tolist()grid_points, probabilities = gaussian_discretization(NUM_QUBITS)STEP_X = grid_points[1] - grid_points[0]MIN_X = grid_points[0]a = STEP_X / SCALING_FACTOR
assert K <= max( S0 * np.exp(np.dot(CHOLESKY, [grid_points[-1]] * 2) + MU)), "If K always greater than the maximum reachable asset values.Stop the run, the payoff is 0"
Considering the time delta between the starting date (t0) and the maturity date (t), we can express the return value Ri for the i-th asset as Ri=μi+yi. Where:μi=(t−t0)μ~i, being μ~i the expected daily log-return value. It can be estimated by considering the historical time series of log returns for the i-th asset.yi is obtained through the dot product between the matrix L and the standard multivariate Gaussian sample:yi=Δx⋅k∑likdk+xmin⋅k∑likΔx is the Gaussian discretization step, xmin is the lower Gaussian truncation value and dk∈[0,2m−1] is the sample taken from the k-th standard Gaussian.
lik is the i,k entry of the matrix L, defined as L=C(t−t0), where C is the lower triangular matrix obtained by applying the Cholesky decomposition to the historical daily log-returns correlation matrix.
from functools import reducefrom classiq.qmod.symbolic import max as qmaxdef get_affine_formula(assets, i): """ Affine formula for the i-th asset in a compact way. reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates ((((1+2)+3)+4)+5) assets: list of assets (QNum), hold asset prices """ return reduce( lambda x, y: x + y, [ assets[j] * round_factor(SCALING_FACTOR * CHOLESKY[i, j]) for j in range(NUM_ASSETS) if CHOLESKY[i, j] ], )c = round_factor( a=(1 / a) * ( np.log(S0[1]) + MU[1] - (np.log(S0[0]) + MU[0]) + MIN_X * sum(CHOLESKY[1] - CHOLESKY[0]) ))
For the following function, you need to compute the maximum of two affine expressions and assign the result to res.Use the qmax function from the classiq.qmod.symbolic module to compute the maximum of two expressions, and the Out-of-place assignment operator |= to assign the result to res.The qmax need to choose the maximum between two expressions:
The affine formula of the first asset: get_affine_formula([x1, x2], 0)
The affine formula of the second asset plus the constant c: get_affine_formula([x1, x2], 1) + c.
@qpermdef affine_max(x1: Const[QNum], x2: Const[QNum], res: Output[QNum]): """ In rainbow options, we compute the maximum of all assets.Computes the maximum of two affine expressions and assigns the result to res. """ # Your code # Solution start res |= qmax( x=get_affine_formula([x1, x2], 0), y=get_affine_formula([x1, x2], 1) + c ) # Solution end
This type of amplitude loading has an exponential scale, is used for validating result from the direct method and integration method that are part of the paper [1].We use here the Classiq amplitude loading functionality using the assign_amplitude_table and lookup_table functions to load the normalized payoff function f(x) into the indicator qubit:∣x⟩∣0⟩→1−f2(x)∣x⟩∣0⟩+f(x)∣x⟩∣1⟩Using the amplitude loading of the payoff function, we can estimate the expected value of the payoff function E[f(x)] by measuring the indicator qubit and calculating the probability of measuring ∣1⟩ using the iterative quantum amplitude estimation (IQAE) algorithm [4].First, we will build the amplitude loading of the payoff function f(x).This is the brute-forced method, in the paper, there two more efficient methods, the direct method and the integration method, which are implemented in the Classiq library.
Then, we will put that in the iterative quantum amplitude estimation (IQAE) algorithm to estimate the expected value of the payoff function.
def get_payoff_expression(x, size, fraction_digits): """ Expression of the payoff for the rainbow option.Similar to Fig. [1] in the article for calculating in the price space. """ payoff = np.sqrt( max( S0[0] * np.exp( a * (2 ** (size - fraction_digits)) * x + (MU[0] + MIN_X * CHOLESKY[0].sum()) ), K, ) ) return payoff# We want the probability of measuring |1> in the indicator qubit (ind_reg), see below.# So we create a normalized version of the payoff function.def get_payoff_expression_normalized(x: QNum, size, fraction_digits): x_max = 1 - 1 / (2**size) payoff_max = get_payoff_expression(x_max, size, fraction_digits) payoff = get_payoff_expression(x, size, fraction_digits) return payoff / payoff_max
For each value of ∣x⟩, we want to load the value of f(x) into the amplitude of the indicator qubit ∣ind⟩.Therefore, we use the assign_amplitude_table function with the lookup_table function inside it.The payoff function (get_payoff_expression_normalized) is the heart of the computation, and thus it used inside the lookup table function.See example in the Classiq documentation for more details.
@qfuncdef brute_force_payoff(max_reg: Const[QNum], ind_reg: QBit): max_reg_fixed = QNum( size=max_reg.size, is_signed=UNSIGNED, fraction_digits=max_reg.size ) # change the QNum register to values between 0 and 1, # it necessary for the amplitude loading bind(source=max_reg, destination=max_reg_fixed) def my_amplitude_loading_payoff_function(x: QNum) -> float: return get_payoff_expression_normalized( x=x, size=max_reg.size, fraction_digits=max_reg.fraction_digits ) # Your code # Build the amplitude loading of the payoff function # Use assign_amplitude_table, lookup_table, my_amplitude_loading_payoff_function, max_reg_fixed, and ind_reg. # Then, use the bind function again to change back the max_reg_fixed to max_reg. # It brings back the encoding (number of qubits, the signedness, and the fraction digits) to the original max_reg, so the output of the function stays the same. # Solution start assign_amplitude_table( amplitudes=lookup_table( func=my_amplitude_loading_payoff_function, targets=max_reg_fixed, ), index=max_reg_fixed, indicator=ind_reg, ) # change back to original values bind(source=max_reg_fixed, destination=max_reg) # Solution end
**We first prepare distribution of the assets and them apply the affine transformation and bring the assets to the maturity date.Then we apply the amplitude loading using the brute_force_payoff function.**
Then, we uncompute the registers to stay with the correct state using local variable max_out inside the function. In the paper [1], it makes the following operation:A∣0⟩=1−a2∣ψ0⟩∣0⟩+a∣ψ1⟩∣1⟩where A is a unitary operator while ∣ψ1⟩ and ∣ψ1⟩ are some normalized states. Thus, a is the probability of measuring ∣1⟩ in the last qubit.The value a is:a=i=0∑N−1f~(wi)p(wi)=E[f~]Which is estimated later on by the IQAE algorithm.
The IQAE algorithm estimates a by using the A operation (that uses the rainbow_brute_force function) in the grover algorithm. It repeats the Grover operation iteratively until the desired precision is reached according to a certain criteria [4]. In each iteration, it repeats the Grover operation with a different number of iterations k.The IQAE class allows to easily use the IQAE algorithm.You are welcome to see how it is built inside.
from classiq.applications.iqae.iqae import IQAE?IQAE
Executes the IQAE algorithm iteratively, according to the IQAE algorithm.Basically, it is running the quantum program multiple times with different number of Grover iterations k until the desired precision is reached. In each iteration, it adds a different number of Grover repetitions to the quantum circuit based on the results of the previous iterations according to the protocol in the article.The complexity is similar to the regular QAE but uses less qubits and thus is more appealing for simulations.
parsed_result, conf_interval = parse_result_bruteforce(result)print( f"raw iqae results: {result.estimation} with confidence interval {result.confidence_interval}")print( f"option estimated value: {parsed_result} with confidence interval {conf_interval}")
Output:
raw iqae results: 0.5024499999999998 with confidence interval [0.492332756597988, 0.5125672434020117] option estimated value: 22.298369227161515 with confidence interval [18.02356721 26.57317125]
expected_payoff = 23.0238ALPHA_ASSERTION = 1e-5measured_confidence = conf_interval[1] - conf_interval[0]confidence_scale_by_alpha = np.sqrt( np.log(ALPHA_VALUE / ALPHA_ASSERTION)) # based on e^2=(1/2N)*log(2T/alpha) from "Iterative Quantum Amplitude Estimation" since our alpha is low, we want to check within a bigger confidence intervaldiff = np.abs(parsed_result - expected_payoff)allowed_error = 0.5 * measured_confidence * confidence_scale_by_alphaif diff <= allowed_error: print("✅ Payoff is within the confidence interval.")else: print( f"❌ Payoff result is out of the {ALPHA_ASSERTION*100}% confidence interval: |{parsed_result} - {expected_payoff}| > {0.5*measured_confidence * confidence_scale_by_alpha}" )