Skip to content

Arbitrage (Node-Based) Example

Download Notebook


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.

data.plot()

<Axes: title={'center': 'Arbitrage (Node) - 4 currencies, max cycle 4'}>
png

Create Formulation

Find profitable arbitrage cycles within the specified maximum length.

formulation = ArbitrageNodeBasedFormulation()
print(formulation.to_string(data))
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.

model = instance.formulate()

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.

uc_solution.plot(data)

<Axes: title={'center': 'Arbitrage (Node) — Cycle Solution, valid=True'}>
png

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