Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Crypto volatility index adapter (by COTI) #138

Merged
9 changes: 8 additions & 1 deletion .github/strategy/adapters.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,14 @@
"cmd": "make zip adapter=composite/__ADAPTER__ name=__ADAPTER__",
"asset_path": "./composite/__ADAPTER__/dist/__ADAPTER__-adapter.zip",
"asset_name": "__ADAPTER__-adapter.zip",
"adapter": ["proof-of-reserves", "market-closure", "defi-pulse", "dns-record-check", "outlier-detection"]
"adapter": [
"proof-of-reserves",
"market-closure",
"defi-pulse",
"dns-record-check",
"outlier-detection",
"crypto-volatility-index"
]
},
"synth-index": {
"docker": "make docker-synth-index adapter=__ADAPTER__",
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- `finage` to get Financial data from finage.co.uk
- `coincodex` to get crypto prices from CoinCodex
- `coinranking` to get crypto prices from Coinranking
- `crypto-volatility-index` to calculate the CVI (Crypto volatility index)
- `btc.com` to get on-chain balances from BTC.com
- `sochain` to get on-chain balances from SoChain
- `dns-query` to query DNS over HTTPS
Expand Down
3 changes: 3 additions & 0 deletions composite/crypto-volatility-index/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
...require('../../.eslintrc.ts.js'),
}
49 changes: 49 additions & 0 deletions composite/crypto-volatility-index/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Chainlink composite adapter for the Crypto Volatility Index (CVI)

## Overview

CVI is a decentralized volatility index created by COTI (https://coti.io) for the crypto markets

The CVI's calculation is based on the classic approach of the Black-Scholes option pricing model and is adapted to the current crypto-market conditions.

## Configuration

The CVI calculation requires the following environment variables:

- `RPC_URL`: Blockchain RPC endpoint to get the needed on-chain data
- `DOMINANCE_PROVIDER`: Data provider to use. Some of them require an `API_KEY`(K). Options available:
- `coingecko`
- `coinmarketcap`(K)
- `API_KEY`: For those data providers who need an api key

## Input Params

- `contractAddress` or `contract`: The address of the on-chain crypto volatility index aggregator contract
- `heartBeat` (Optional): The time length of the aggregator heart beat in minutes (Default: 180)
- `multiply`: (Optional) Multiply amount for the on-chain value, which also determines the result precision (default: 1000000)
- `isAdaptive`: (Optional) Indicates whether the calculation result should be adaptively smoothed with its latest on-chain value (default: true)

## Build and run

To build:
```bash
make docker adapter=crypto-volatility-index
```

To run:
```bash
docker run -p 8080:8080 -e RPC_URL=<rpc url, for example https://mainnet.infura.io/v3/infura_key> -e DOMINANCE_PROVIDER=coingecko -e LOG_LEVEL=debug crypto-volatility-index-adapter:latest
```


## Output

```json
{
"jobRunID": "278c97ffadb54a5bbb93cfec5f7b5503",
"data": {
"result": 67.4
},
"statusCode": 200
}
```
48 changes: 48 additions & 0 deletions composite/crypto-volatility-index/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@chainlink/crypto-volatility-index-adapter",
"version": "0.0.1",
"description": "The Crypto volatility index (CVI)",
"keywords": [
"Chainlink",
"LINK",
"COTI",
"CVI",
"blockchain",
"oracle"
],
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"prepublishOnly": "yarn build && yarn test:unit",
"setup": "yarn build",
"build": "tsc -b",
"lint": "eslint --ignore-path ../../.eslintignore . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint --ignore-path ../../.eslintignore . --ext .js,.jsx,.ts,.tsx --fix",
"test": "mocha --timeout 0 --exit -r ts-node/register 'test/**/*.test.ts'",
"test:unit": "mocha --exit --grep @integration --invert -r ts-node/register 'test/**/*.test.ts'",
"test:integration": "mocha --exit --grep @integration -r ts-node/register 'test/**/*.test.ts'",
"server": "node -e 'require(\"./index.js\").server()'",
"server:dist": "node -e 'require(\"./dist/index.js\").server()'",
"start": "yarn server:dist"
},
"devDependencies": {
"@types/big.js": "^6.0.0",
"@types/chai": "^4.2.11",
"@types/express": "^4.17.6",
"@types/mocha": "^7.0.2",
"@types/node": "^14.0.13",
"@typescript-eslint/eslint-plugin": "^3.9.0",
"@typescript-eslint/parser": "^3.9.0",
"ts-node": "^8.10.2",
"typescript": "^3.9.7"
},
"dependencies": {
"big.js": "^6.0.2",
"moment": "^2.29.1",
"@chainlink/reference-data-reader": "^0.0.2"
}
}
29 changes: 29 additions & 0 deletions composite/crypto-volatility-index/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Execute } from '@chainlink/types'
import { Requester, Validator } from '@chainlink/external-adapter'
import { calculate } from './cryptoVolatilityIndex'

const customParams = {
contract: ['contractAddress', 'contract'],
multiply: false,
heartbeatMinutes: false,
isAdaptive: false,
boxhock marked this conversation as resolved.
Show resolved Hide resolved
}
export const execute: Execute = async (input) => {
const validator = new Validator(input, customParams)
if (validator.error) throw validator.error

const jobRunID = validator.validated.id
const oracleAddress = validator.validated.data.contract
const multiply = validator.validated.data.multiply || 1000000
const heartbeatMinutes = validator.validated.data.heartbeatMinutes || 60
const isAdaptive = validator.validated.data.isAdaptive !== false

const result = await calculate(oracleAddress, multiply, heartbeatMinutes, isAdaptive)
return Requester.success(jobRunID, {
data: { result },
result,
status: 200,
})
}

export default execute
117 changes: 117 additions & 0 deletions composite/crypto-volatility-index/src/cryptoVolatilityIndex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { util } from '@chainlink/ea-bootstrap'
import { logger } from '@chainlink/external-adapter'
import { getRpcLatestRound } from '@chainlink/reference-data-reader'
import { getDerivativesData, CurrencyDerivativesData } from './derivativesDataProvider'
import { getDominanceAdapter, dominanceByCurrency } from './dominance-data-providers'
import { SigmaCalculator } from './sigmaCalculator'
import { Big } from 'big.js'
import moment from 'moment'
const cryptoCurrencies = ['BTC', 'ETH']

export const calculate = async (
oracleAddress: string,
multiply: number,
heartbeatMinutes: number,
isAdaptive: boolean,
): Promise<number> => {
// Get all of the required derivatives data for the calculations, for all the relevant currencies
const derivativesData = await getDerivativesData(cryptoCurrencies)
// Calculate vix values for all currencies
const volatilityIndexData = await calculateVixValues(derivativesData)
// Apply weights to calculate the Crypto Vix
const weightedCVI = await calculateWeighted(volatilityIndexData)
// Smooth CVI with previous on-chain value if exists
const cvi = !isAdaptive
? toOnChainValue(weightedCVI, multiply)
: await applySmoothing(weightedCVI, oracleAddress, multiply, heartbeatMinutes)

logger.info(`CVI: ${cvi}`)
validateIndex(cvi)
return cvi
}

const calculateVixValues = async (derivativesData: Record<string, CurrencyDerivativesData>) => {
const now = moment().utc()
const sigmaCalculator = new SigmaCalculator()
const vixValues = cryptoCurrencies.map((currency) => {
sigmaCalculator.sortByStrikePrice(derivativesData[currency])
const { e1, e2, exchangeRate, callsE1, putsE1, callsE2, putsE2 } = derivativesData[currency]
const weightedSigma: Big = sigmaCalculator.weightedSigma({
e1,
e2,
sigma1: sigmaCalculator.oneSigma(e1, exchangeRate, callsE1, putsE1, now),
sigma2: sigmaCalculator.oneSigma(e2, exchangeRate, callsE2, putsE2, now),
now,
})
return weightedSigma.sqrt().times(100)
})

return vixValues
}

const calculateWeighted = async (vixData: Array<Big>) => {
const dominanceByCurrency = await getDominanceByCurrency()
const weightedVix = cryptoCurrencies.reduce((vix, currency, idx) => {
const dominance = dominanceByCurrency[currency]
if (!dominance) throw new Error(`No dominance found for currency ${currency}`)
const currencyVix = new Big(vixData[idx])
// Weight by dominance
vix = vix.plus(currencyVix.times(new Big(dominance)))
return vix
}, new Big(0))

const weighted = Number(weightedVix.toFixed())
logger.debug(`Weighted volatility index:${weighted}`)
return weighted
}

const getDominanceByCurrency = async () => {
const dominanceProvider = util.getRequiredEnv('DOMINANCE_PROVIDER')
const dominanceAdapter = await getDominanceAdapter(dominanceProvider)
const dominanceData = await dominanceAdapter.getDominance(cryptoCurrencies)
return dominanceByCurrency(dominanceData)
}

const applySmoothing = async (
weightedCVI: number,
oracleAddress: string,
multiply: number,
heartBeatMinutes: number,
): Promise<number> => {
const roundData = await getRpcLatestRound(oracleAddress)
const latestIndex = new Big(roundData.answer.toString()).div(multiply)
const updatedAt = roundData.updatedAt.mul(1000).toNumber()

if (latestIndex.lte(0)) {
logger.warn('No on-chain index value found - Is first run of adapter?')
return weightedCVI
}

const now = moment().utc()
const dtSeconds = moment.duration(now.diff(updatedAt)).asSeconds()
if (dtSeconds < 0) {
throw new Error('invalid time, please check the node clock')
}
const l = lambda(dtSeconds, heartBeatMinutes)
const smoothed = latestIndex.mul(new Big(1 - l)).add(new Big(weightedCVI).mul(l))
logger.debug(`Previous value:${latestIndex}, updatedAt:${updatedAt}, dtSeconds:${dtSeconds}`)
return smoothed.toNumber()
}

const LAMBDA_MIN = 0.01
const LAMBDA_K = 0.1
const lambda = function (t: number, heartBeatMinutes: number) {
const T = moment.duration(heartBeatMinutes, 'minutes').asSeconds()
return LAMBDA_MIN + (LAMBDA_K * Math.min(t, T)) / T
}

const MAX_INDEX = 200
const validateIndex = function (cvi: number) {
if (cvi <= 0 || cvi > MAX_INDEX) {
throw new Error('Invalid calculated index value')
}
}

const toOnChainValue = function (cvi: number, multiply: number) {
return Number(cvi.toFixed(multiply.toString().length - 1)) // Keep decimal precision in same magnitude as multiply
}
Loading