Skip to content

Commit

Permalink
Merge pull request #129 from primitivefinance/feat/arb.py
Browse files Browse the repository at this point in the history
Feat/arb.py
  • Loading branch information
Alexangelj committed Jul 14, 2021
2 parents 63633fe + 62ae8d2 commit df055bc
Show file tree
Hide file tree
Showing 9 changed files with 413 additions and 25 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -11,3 +11,5 @@ typechain/
dist

.vscode/

.simulationData.json
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -61,6 +61,7 @@
"dotenv": "^10.0.0",
"ethereum-waffle": "^3.3.0",
"ethers": "^5.0.32",
"gaussian": "^1.2.0",
"hardhat": "^2.3.0",
"hardhat-contract-sizer": "^2.0.3",
"hardhat-gas-reporter": "^1.0.4",
Expand Down
10 changes: 9 additions & 1 deletion test/shared/CumulativeNormalDistribution.ts
@@ -1,3 +1,5 @@
import gaussian from 'gaussian'

function cdf(x, mean, variance) {
return 0.5 * (1 + erf((x - mean) / Math.sqrt(2 * variance)))
}
Expand Down Expand Up @@ -25,7 +27,13 @@ export function std_n_cdf(x) {
return cdf(x, 0, 1)
}

// source: https://github.com/errcw/gaussian/blob/master/lib/gaussian.js
export function inverse_std_n_cdf(x) {
return gaussian(0, 1).ppf(x)
}

// solidity implementation: https://arxiv.org/pdf/1002.0567.pdf
/* export function inverse_std_n_cdf(x) {
const q = x - 0.5
const r = Math.pow(q, 2)
const a0 = 0.151015506
Expand All @@ -38,4 +46,4 @@ export function inverse_std_n_cdf(x) {
const input = a2 + numerator / denominator
const result = q * input
return result
}
} */
121 changes: 121 additions & 0 deletions test/shared/sdk/entities/Arb.ts
@@ -0,0 +1,121 @@
import { parseWei, Wei } from 'web3-units'
import { inverse_std_n_cdf, std_n_cdf } from '../../CumulativeNormalDistribution'
import { Pool } from './Pool'
import gaussian from 'gaussian'

export const quantilePrime = (x) => {
return gaussian(0, 1).pdf(inverse_std_n_cdf(x)) ** -1
}

export const EPSILON = 1e-3

// JavaScript program for implementation
// of Bisection Method for
// solving equations

// Prints root of func(x) with error of EPSILON
function bisection(func, a, b) {
if (func(a) * func(b) >= 0) {
console.log('\n You have not assumed' + ' right a and b')
return
}

let c = a
while (b - a >= EPSILON) {
// Find middle point
c = (a + b) / 2

// Check if middle point is root
if (func(c) == 0.0) break
// Decide the side to repeat the steps
else if (func(c) * func(a) < 0) b = c
else a = c
}
//prints value of c upto 4 decimal places
console.log('\n The value of ' + 'root is : ' + c)
return c
}

// This code is contributed by susmitakundugoaldanga.

/**
* @notice Represents an agent that will look a reference price of the risky asset, denominated in the stable asset,
* then looks at the reference price in the AMM pool, and will arbitrage any difference.
*/
export class Arbitrageur {
public readonly optimalAmount: number

constructor() {
this.optimalAmount = 1e-8
}

arbitrageExactly(spot: Wei, pool: Pool) {
console.log(`\n ----- Start Arb at spot price: ${spot.float} -----`)
const gamma = 1 - pool.entity.fee
const [R1, R2, invariant, strike, sigma, tau] = [
pool.reserveRisky.float / pool.liquidity.float,
pool.reserveStable.float / pool.liquidity.float,
pool.invariant,
pool.strike,
pool.sigma,
pool.tau,
]

// Marginal price of selling epsilon risky
const sellPriceRisky = pool.getMarginalPriceSwapRiskyIn(0)

// Marginal price of buying epsilon risky
const buyPriceRisky = pool.getMarginalPriceSwapStableIn(0)

console.log(`\n Sell price of risky: ${sellPriceRisky}`)
console.log(` Buy price risky: ${buyPriceRisky}`)
console.log(` Market price: ${spot.float}`)

if (sellPriceRisky > spot.float + this.optimalAmount) {
const func = (amountIn) => {
return pool.getMarginalPriceSwapRiskyIn(amountIn) - spot.float
}

let optimalTrade
if (Math.sign(func(EPSILON)) != Math.sign(func(1 - R1 - EPSILON))) {
optimalTrade = bisection(func, EPSILON, 1 - R1 - EPSILON) // bisect
} else {
optimalTrade = 1 - R1
}
console.log(`\n Optimal trade is: ${optimalTrade}`)
optimalTrade = parseWei(Math.floor(optimalTrade * 1e18) / 1e18)
const { deltaOut } = pool.virtualSwapAmountInRisky(optimalTrade)
const profit = deltaOut.float - optimalTrade.float * spot.float

console.log(` Sell profit: ${profit}`)
if (profit > 0) {
pool.swapAmountInRisky(optimalTrade) // do the arbitrage
console.log(` Invariant after arbitrage: ${pool.invariant.parsed / Math.pow(10, 18)}`)
}
} else if (buyPriceRisky < spot.float - this.optimalAmount) {
const func = (amountIn) => {
return spot.float - pool.getMarginalPriceSwapStableIn(amountIn)
}

let optimalTrade
if (Math.sign(func(EPSILON)) != Math.sign(func(strike.float - R2 - EPSILON))) {
optimalTrade = bisection(func, 0, strike.float - R2 - EPSILON) //bisect func
} else {
optimalTrade = strike.float - R2
}
console.log(`\n Optimal trade is: ${optimalTrade}`)
optimalTrade = parseWei(Math.floor(optimalTrade * 1e18) / 1e18)

const { deltaOut } = pool.virtualSwapAmountInStable(optimalTrade)
console.log(` Got delta out of ${deltaOut.float}`)
const profit = optimalTrade.float * spot.float - deltaOut.float
console.log(` Buy profit: ${profit}`)
if (profit > 0) {
pool.swapAmountInStable(optimalTrade) // do the arbitrage
console.log(` Invariant after arbitrage: ${pool.invariant.parsed / Math.pow(10, 18)}`)
}
}

console.log(`\n ----- End Arb -----`)
}
}
7 changes: 6 additions & 1 deletion test/shared/sdk/entities/Engine.ts
@@ -1,4 +1,4 @@
import { utils } from 'ethers'
import { constants, utils } from 'ethers'
import { BytesLike } from '@ethersproject/bytes'
/// SDK Imports
import { Pool, Token } from '../entities'
Expand All @@ -13,6 +13,11 @@ export interface SwapReturn {
effectivePriceOutStable?: Wei
}

export const DefaultTokens = {
risky: new Token(1337, constants.AddressZero, 18, 'RISKY', 'RISKY'),
stable: new Token(1337, constants.AddressZero, 18, 'STABLE', 'STABLE'),
}

// ===== Engine Class =====

/**
Expand Down
135 changes: 112 additions & 23 deletions test/shared/sdk/entities/Pool.ts
Expand Up @@ -2,6 +2,24 @@ import numeric from 'numeric'
import { Engine, SwapReturn } from './Engine'
import { getInverseTradingFunction, getTradingFunction, calcInvariant } from '../../ReplicationMath'
import { Integer64x64, Percentage, Time, Wei, parseWei, parseInt64x64 } from 'web3-units'
import { inverse_std_n_cdf, std_n_cdf } from '../../CumulativeNormalDistribution'
import { quantilePrime } from './Arb'

export const nonNegative = (x: number): boolean => {
return x >= 0
}

export const clonePool = (poolToClone: Pool, newRisky: Wei, newStable: Wei): Pool => {
return new Pool(
poolToClone.entity,
newRisky,
newStable,
poolToClone.strike,
poolToClone.sigma,
poolToClone.maturity,
poolToClone.lastTimestamp
)
}

/**
* @notice Typescript representation of an individual Pool in an Engine
Expand Down Expand Up @@ -58,16 +76,28 @@ export class Pool {
* @return reserveStable Expected amount of stable token reserves
*/
getStableGivenRisky(reserveRisky: Wei): Wei {
return parseWei(
getTradingFunction(
this.invariant.float,
reserveRisky.float,
this.liquidity.float,
this.strike.float,
this.sigma.float,
this.tau.years
)
const invariant = Math.floor(this.invariant.parsed) / Math.pow(10, 18)
console.log(
Math.abs(invariant) >= 1e-8,
invariant,
reserveRisky.float,
this.liquidity.float,
this.strike.float,
this.sigma.float,
this.tau.years
)

let stable = getTradingFunction(
Math.abs(invariant) >= 1e-8 ? 0 : invariant,
reserveRisky.float,
this.liquidity.float,
this.strike.float,
this.sigma.float,
this.tau.years
)

stable = Math.floor(stable * Math.pow(10, 18)) / Math.pow(10, 18)
return parseWei(stable)
}

/**
Expand All @@ -76,16 +106,27 @@ export class Pool {
* @return reserveRisky Expected amount of risky token reserves
*/
getRiskyGivenStable(reserveStable: Wei): Wei {
return parseWei(
getInverseTradingFunction(
this.invariant.float,
reserveStable.float,
this.liquidity.float,
this.strike.float,
this.sigma.float,
this.tau.years
)
const invariant = Math.floor(this.invariant.parsed) / Math.pow(10, 18)
console.log(
Math.abs(invariant) >= 1e-8,
invariant,
reserveStable.float,
this.liquidity.float,
this.strike.float,
this.sigma.float,
this.tau.years
)
let risky = getInverseTradingFunction(
Math.abs(invariant) >= 1e-8 ? 0 : invariant,
reserveStable.float,
this.liquidity.float,
this.strike.float,
this.sigma.float,
this.tau.years
)
console.log(`\n Pool: got risky: ${risky} given stable: ${reserveStable.float}`)
risky = Math.floor(risky * Math.pow(10, 18)) / Math.pow(10, 18)
return parseWei(risky)
}

/**
Expand Down Expand Up @@ -148,11 +189,11 @@ export class Pool {
const deltaInWithFee = deltaIn.mul(gamma * Percentage.Mantissa).div(Percentage.Mantissa)
const newReserveRisky = this.reserveRisky.add(deltaInWithFee)
const newReserveStable = this.getStableGivenRisky(newReserveRisky)
const deltaOut = newReserveStable.sub(this.reserveStable)
const deltaOut = this.reserveStable.sub(newReserveStable)
const effectivePriceOutStable = deltaOut.div(deltaIn)
return {
deltaOut,
pool: this,
pool: clonePool(this, newReserveRisky, newReserveStable),
effectivePriceOutStable: effectivePriceOutStable,
}
}
Expand Down Expand Up @@ -195,17 +236,65 @@ export class Pool {
const effectivePriceOutStable = deltaIn.div(deltaOut)
return {
deltaOut,
pool: this,
pool: clonePool(this, newReserveRisky, newReserveStable),
effectivePriceOutStable: effectivePriceOutStable,
}
}

getSpotPrice(): Wei {
const fn = function (this, x: number[]) {
return calcInvariant(x[0], x[1], this.liquidity.float, this.strike.float, this.sigma.float, this.tau.years)
const liquidity = this.liquidity.float
const strike = this.strike.float
const sigma = this.sigma.float
const tau = this.tau.years
const fn = function (x: number[]) {
return calcInvariant(x[0], x[1], liquidity, strike, sigma, tau)
}
const spot = numeric.gradient(fn, [this.reserveRisky.float, this.reserveStable.float])
//console.log({ spot }, [x[0].float, x[1].float], spot[0] / spot[1])
return parseWei(spot[0] / spot[1])
}

/**
* @notice See https://arxiv.org/pdf/2012.08040.pdf
* @param amountIn Amount of risky token to add to risky reserve
* @return Marginal price after a trade with size `amountIn` with the current reserves.
*/
getMarginalPriceSwapRiskyIn(amountIn) {
if (!nonNegative(amountIn)) return 0
const gamma = 1 - this.entity.fee
const reserveRisky = this.reserveRisky.float / this.liquidity.float
const invariant = this.invariant
const strike = this.strike
const sigma = this.sigma
const tau = this.tau
const step0 = 1 - reserveRisky - gamma * amountIn
const step1 = sigma.float * Math.sqrt(tau.years)
const step2 = quantilePrime(step0)

return gamma * strike.float * step1 * step2
}

/**
* @notice See https://arxiv.org/pdf/2012.08040.pdf
* @param amountIn Amount of stable token to add to stable reserve
* @return Marginal price after a trade with size `amountIn` with the current reserves.
*/
getMarginalPriceSwapStableIn(amountIn) {
if (!nonNegative(amountIn)) return 0
const gamma = 1 - this.entity.fee
const reserveStable = this.reserveStable.float / this.liquidity.float
const invariant = this.invariant
const strike = this.strike
const sigma = this.sigma
const tau = this.tau
const step0 = (reserveStable + gamma * amountIn - invariant.parsed / Math.pow(10, 18)) / strike.float
const step1 = sigma.float * Math.sqrt(tau.years)
const step3 = inverse_std_n_cdf(step0)
const step4 = std_n_cdf(step3 + step1)
const step5 = step0 * (1 / strike.float)
const step6 = quantilePrime(step5)
const step7 = gamma * step4 * step6
//console.log({ step0, step1, step3, step4, step5, step6, step7 }, 1 / step7)
return 1 / step7
}
}

0 comments on commit df055bc

Please sign in to comment.