Arbitrage (Node-Based) Example
Arbitrage exploits price differences across markets for risk-free profit. This node-based formulation finds profitable currency cycles using a position-based assignment, where currencies are placed at positions in a fixed-length cycle.
import getpass
import os
import numpy as np
from dotenv import load_dotenv
from luna_quantum.algorithms import SCIP
from luna_usecases.arbitrage_node_based import (
ArbitrageNodeBasedCollection,
ArbitrageNodeBasedData,
ArbitrageNodeBasedFormulation,
ArbitrageNodeBasedInstance,
)
load_dotenv()
if "LUNA_API_KEY" not in os.environ:
os.environ["LUNA_API_KEY"] = getpass.getpass("Enter your Luna API key: ")
Create Data
Define exchange rates between 4 currencies with a maximum arbitrage cycle length of 4.
adj = np.array(
[
[0.0, 0.85, 0.73, 110.0],
[1.18, 0.0, 0.86, 129.5],
[1.37, 1.16, 0.0, 150.7],
[0.0091, 0.0077, 0.0066, 0.0],
]
)
node_names = ["USD", "EUR", "GBP", "JPY"]
data = ArbitrageNodeBasedData(adjacency_matrix=adj, node_names=node_names, max_cycle_length=4)
print(data.to_string())
Arbitrage Node-Based Data:
Currencies: USD, EUR, GBP, JPY
Number of currencies: 4
Number of directed edges: 12
Max cycle length: 4
Plot Data
Visualize the currency exchange rate network.
Create Formulation
Find profitable arbitrage cycles within the specified maximum length.
Arbitrage Node-Based Formulation:
Currencies: 4
Cycle length: 4
Decision Variables:
x[i,p] in {0,1} for i = 0, ..., 3, p = 0, ..., 3
x[i,p] = 1 if currency i is at position p in the cycle
Total: 16 binary variables
Objective:
maximize sum_p sum_(Undefined, Undefined) log(rate[i,j]) * x[i,p] * x[j,(p+1)%K]
Constraints:
1. One node per position (4 constraints):
sum_i x[i,p] == 1 for all p = 0, ..., 3
2. Each node at most one position (4 constraints):
sum_p x[i,p] <= 1 for all i = 0, ..., 3
3. Consecutive positions connected (0 constraints):
x[i,p] + x[j,(p+1)%4] <= 1 for all non-edges (i,j), p = 0, ..., 3
Create Instance
Combine data and formulation into a solvable instance.
instance = ArbitrageNodeBasedInstance(data=data, formulation=formulation)
print(instance.to_string())
Data:Arbitrage Node-Based Data:
Currencies: USD, EUR, GBP, JPY
Number of currencies: 4
Number of directed edges: 12
Max cycle length: 4
Formulation:Arbitrage Node-Based Formulation:
Currencies: 4
Cycle length: 4
Decision Variables:
x[i,p] in {0,1} for i = 0, ..., 3, p = 0, ..., 3
x[i,p] = 1 if currency i is at position p in the cycle
Total: 16 binary variables
Objective:
maximize sum_p sum_(Undefined, Undefined) log(rate[i,j]) * x[i,p] * x[j,(p+1)%K]
Constraints:
1. One node per position (4 constraints):
sum_i x[i,p] == 1 for all p = 0, ..., 3
2. Each node at most one position (4 constraints):
sum_p x[i,p] <= 1 for all i = 0, ..., 3
3. Consecutive positions connected (0 constraints):
x[i,p] + x[j,(p+1)%4] <= 1 for all non-edges (i,j), p = 0, ..., 3
Formulate Model
Translate the instance into a mathematical optimization model.
Solve and Interpret
Solve the model with SCIP and interpret the raw result into a use-case-specific solution.
scip = SCIP()
job = scip.run(model)
sol = job.result()
uc_solution = instance.interpret(sol)
print(uc_solution.to_string())
/Users/maximilianjanetschek/PycharmProjects/luna-usecases/.venv/lib/python3.13/site-packages/rich/live.py:260:
UserWarning: install "ipywidgets" for Jupyter support
warnings.warn('install "ipywidgets" for Jupyter support')
2026-05-29 11:33:32 INFO Sleeping for 5.0 seconds. Waiting and checking a function in a loop.
2026-05-29 11:33:38 INFO Sleeping for 10.0 seconds. Waiting and checking a function in a loop.
2026-05-29 11:33:49 INFO Sleeping for 15.0 seconds. Waiting and checking a function in a loop.
2026-05-29 11:34:05 INFO Sleeping for 20.0 seconds. Waiting and checking a function in a loop.
Arbitrage Node-Based Solution:
Status: VALID
Cycle: USD -> EUR -> GBP -> JPY -> USD
Arbitrage Profit: 1.002471
Profitable: YES
Plot Solution
Visualize the optimal solution.
Collections
Generate a benchmark collection of random instances for batch processing.
collection = ArbitrageNodeBasedCollection.from_random(
min_currencies=3, max_currencies=6, max_cycle_length=4, num_instances=1, seed=42
)
model = collection.instances[0].formulate()
print(model)
Model: arbitrage_node_based<arbitrage_node_based>
Maximize
0.4054651081081644 * x_0_0 * x_1_1 - 0.1724140384186996 * x_0_0 * x_1_3
- 0.110616318142052 * x_0_0 * x_2_1 - 0.10536051565782628 * x_0_0 * x_2_3
- 0.1724140384186996 * x_0_1 * x_1_0 + 0.4054651081081644 * x_0_1 * x_1_2
- 0.10536051565782628 * x_0_1 * x_2_0 - 0.110616318142052 * x_0_1 * x_2_2
- 0.1724140384186996 * x_0_2 * x_1_1 + 0.4054651081081644 * x_0_2 * x_1_3
- 0.10536051565782628 * x_0_2 * x_2_1 - 0.110616318142052 * x_0_2 * x_2_3
+ 0.4054651081081644 * x_0_3 * x_1_0 - 0.1724140384186996 * x_0_3 * x_1_2
- 0.110616318142052 * x_0_3 * x_2_0 - 0.10536051565782628 * x_0_3 * x_2_2
- 0.10536051565782628 * x_1_0 * x_2_1 - 0.19757889468462903 * x_1_0 * x_2_3
- 0.19757889468462903 * x_1_1 * x_2_0 - 0.10536051565782628 * x_1_1 * x_2_2
- 0.19757889468462903 * x_1_2 * x_2_1 - 0.10536051565782628 * x_1_2 * x_2_3
- 0.10536051565782628 * x_1_3 * x_2_0 - 0.19757889468462903 * x_1_3 * x_2_2
Subject To
pos_one_node_0: x_0_0 + x_1_0 + x_2_0 == 1
pos_one_node_1: x_0_1 + x_1_1 + x_2_1 == 1
pos_one_node_2: x_0_2 + x_1_2 + x_2_2 == 1
pos_one_node_3: x_0_3 + x_1_3 + x_2_3 == 1
node_one_pos_0: x_0_0 + x_0_1 + x_0_2 + x_0_3 <= 1
node_one_pos_1: x_1_0 + x_1_1 + x_1_2 + x_1_3 <= 1
node_one_pos_2: x_2_0 + x_2_1 + x_2_2 + x_2_3 <= 1
Binary
x_0_0 x_0_1 x_0_2 x_0_3 x_1_0 x_1_1 x_1_2 x_1_3 x_2_0 x_2_1 x_2_2 x_2_3