Skip to content

Arbitrage (Edge-Based) Example

Download Notebook


Arbitrage exploits price differences across markets for risk-free profit. This edge-based formulation finds profitable currency cycles in a directed graph of exchange rates, where edge weights represent conversion rates between currencies.

import getpass
import os

import numpy as np
from dotenv import load_dotenv
from luna_quantum.algorithms import SCIP

from luna_usecases.arbitrage_edge_based import (
    ArbitrageEdgeBasedCollection,
    ArbitrageEdgeBasedData,
    ArbitrageEdgeBasedFormulation,
    ArbitrageEdgeBasedInstance,
)

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 (USD, EUR, GBP, JPY) as a directed rate matrix.

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 = ArbitrageEdgeBasedData.from_adjacency_matrix(adjacency_matrix=adj, node_names=node_names)
print(data.to_string())
Arbitrage Edge-Based Data:
  Currencies: USD, EUR, GBP, JPY
  Number of currencies: 4
  Number of directed edges: 12

Plot Data

Visualize the currency exchange rate network.

data.plot()

<Axes: title={'center': 'Arbitrage (Edge) - 4 currencies'}>
png

Create Formulation

Find profitable arbitrage cycles where the product of exchange rates exceeds 1.

formulation = ArbitrageEdgeBasedFormulation()
print(formulation.to_string(data))
Arbitrage Edge-Based Formulation:
  Currencies: 4
  Directed edges: 12

Decision Variables:
  y[i,j] in {0,1} for each directed edge (i,j)
  y[i,j] = 1 if edge (i,j) is in the cycle
  Total: 12 binary variables

Objective:
  maximize sum_(Undefined, Undefined) log(rate[i,j]) * y[i,j]

Constraints:
  1. Flow conservation (4 constraints):
     sum_j y[v,j] == sum_j y[j,v]  for all nodes v
  2. At most one outgoing edge (4 constraints):
     sum_j y[v,j] <= 1  for all nodes v
  3. Non-trivial cycle (1 constraint):
     sum_{(i,j)} y[i,j] >= 1

Create Instance

Combine data and formulation into a solvable instance.

instance = ArbitrageEdgeBasedInstance(data=data, formulation=formulation)
print(instance.to_string())
Data:Arbitrage Edge-Based Data:
  Currencies: USD, EUR, GBP, JPY
  Number of currencies: 4
  Number of directed edges: 12
Formulation:Arbitrage Edge-Based Formulation:
  Currencies: 4
  Directed edges: 12

Decision Variables:
  y[i,j] in {0,1} for each directed edge (i,j)
  y[i,j] = 1 if edge (i,j) is in the cycle
  Total: 12 binary variables

Objective:
  maximize sum_(Undefined, Undefined) log(rate[i,j]) * y[i,j]

Constraints:
  1. Flow conservation (4 constraints):
     sum_j y[v,j] == sum_j y[j,v]  for all nodes v
  2. At most one outgoing edge (4 constraints):
     sum_j y[v,j] <= 1  for all nodes v
  3. Non-trivial cycle (1 constraint):
     sum_{(i,j)} y[i,j] >= 1

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:31 INFO     Sleeping for 5.0 seconds. Waiting and checking a function in a loop.


2026-05-29 11:33:37 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.




Arbitrage Edge-Based Solution:
  Status: VALID
  Cycle: USD->EUR -> EUR->USD
  Arbitrage Profit: 1.003000
  Profitable: YES

Plot Solution

Visualize the optimal solution.

uc_solution.plot(data)

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

Collections

Generate a benchmark collection of random instances for batch processing.

collection = ArbitrageEdgeBasedCollection.from_random(min_currencies=3, max_currencies=6, num_instances=1, seed=42)
model = collection.instances[0].formulate()
print(model)
Model: arbitrage_edge_based<arbitrage_edge_based>
Maximize
  0.4054651081081644 * y_0_1 - 0.110616318142052 * y_0_2
  - 0.1724140384186996 * y_1_0 - 0.10536051565782628 * y_1_2
  - 0.10536051565782628 * y_2_0 - 0.19757889468462903 * y_2_1
Subject To
  flow_conservation_0: y_0_1 + y_0_2 - y_1_0 - y_2_0 == 0
  flow_conservation_1: -y_0_1 + y_1_0 + y_1_2 - y_2_1 == 0
  flow_conservation_2: -y_0_2 - y_1_2 + y_2_0 + y_2_1 == 0
  max_one_outgoing_0: y_0_1 + y_0_2 <= 1
  max_one_outgoing_1: y_1_0 + y_1_2 <= 1
  max_one_outgoing_2: y_2_0 + y_2_1 <= 1
  non_trivial: y_0_1 + y_0_2 + y_1_0 + y_1_2 + y_2_0 + y_2_1 >= 1
Binary
  y_0_1 y_0_2 y_1_0 y_1_2 y_2_0 y_2_1