-
Notifications
You must be signed in to change notification settings - Fork 1
Implement RSI Oracle #70
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
/* | ||
Copyright 2019 Set Labs Inc. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
pragma solidity 0.5.7; | ||
pragma experimental "ABIEncoderV2"; | ||
|
||
import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; | ||
import { ITimeSeriesFeed } from "./interfaces/ITimeSeriesFeed.sol"; | ||
import { RSILibrary } from "./lib/RSILibrary.sol"; | ||
|
||
|
||
/** | ||
* @title RSIOracle | ||
* @author Set Protocol | ||
* | ||
* Contract used calculate RSI of data points provided by other on-chain | ||
* price feed and return to querying contract. | ||
*/ | ||
contract RSIOracle { | ||
|
||
using SafeMath for uint256; | ||
|
||
/* ============ State Variables ============ */ | ||
string public dataDescription; | ||
ITimeSeriesFeed public timeSeriesFeedInstance; | ||
|
||
/* ============ Constructor ============ */ | ||
|
||
/* | ||
* RSIOracle constructor. | ||
* Contract used calculate RSI of data points provided by other on-chain | ||
* price feed and return to querying contract. | ||
* | ||
* @param _timeSeriesFeed TimeSeriesFeed to get list of data from | ||
* @param _dataDescription Description of data | ||
*/ | ||
constructor( | ||
ITimeSeriesFeed _timeSeriesFeed, | ||
string memory _dataDescription | ||
) | ||
public | ||
{ | ||
timeSeriesFeedInstance = _timeSeriesFeed; | ||
|
||
dataDescription = _dataDescription; | ||
} | ||
|
||
/* | ||
* Get RSI over defined amount of data points by querying price feed and | ||
* calculating using RSILibrary. Returns uint256. | ||
* | ||
* @param _rsiTimePeriod RSI lookback period | ||
* @returns RSI value for passed number of _rsiTimePeriod | ||
*/ | ||
function read( | ||
uint256 _rsiTimePeriod | ||
) | ||
external | ||
view | ||
returns (uint256) | ||
{ | ||
// Get data from price feed. This will be +1 the lookback period | ||
uint256[] memory dataArray = timeSeriesFeedInstance.read(_rsiTimePeriod.add(1)); | ||
|
||
// Return RSI calculation | ||
return RSILibrary.calculate(dataArray); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/* | ||
Copyright 2019 Set Labs Inc. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
pragma solidity 0.5.7; | ||
|
||
import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; | ||
|
||
|
||
/** | ||
* @title RSILibrary | ||
* @author Set Protocol | ||
* | ||
* Library for calculating the Relative Strength Index | ||
* | ||
*/ | ||
library RSILibrary{ | ||
|
||
using SafeMath for uint256; | ||
|
||
/* ============ Constants ============ */ | ||
|
||
uint256 constant HUNDRED = 100; | ||
|
||
/* | ||
* Calculates the new relative strength index value using | ||
* an array of prices. | ||
* | ||
* RSI = 100 − 100 / | ||
* (1 + (Average Gain / Average Loss) | ||
* | ||
* Price Difference = Price(N) - Price(N-1) where N is number of days | ||
* Average Gain = Sum(Positive Price Difference) / N | ||
* Average Loss = -1 * Sum(Negative Price Difference) / N | ||
* | ||
* | ||
* Our implementation is simplified to the following for efficiency | ||
* RSI = 100 - (100 * SUM(Loss) / ((SUM(Loss) + SUM(Gain))) | ||
* | ||
* | ||
* @param _dataArray Array of prices used to calculate the RSI | ||
* @returns The RSI value | ||
*/ | ||
function calculate( | ||
uint256[] memory _dataArray | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add validation that array is greater than zero. |
||
) | ||
internal | ||
view | ||
returns (uint256) | ||
{ | ||
uint256 positiveDataSum = 0; | ||
uint256 negativeDataSum = 0; | ||
|
||
|
||
// Check that data points must be greater than 1 | ||
require( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add comment explaining why this is necessary |
||
_dataArray.length > 1, | ||
"RSILibrary.calculate: Length of data array must be greater than 1" | ||
); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add comment saying what this |
||
// Sum negative and positive price differences | ||
for (uint256 i = 1; i < _dataArray.length; i++) { | ||
uint256 currentPrice = _dataArray[i - 1]; | ||
uint256 previousPrice = _dataArray[i]; | ||
if (currentPrice > previousPrice) { | ||
positiveDataSum = positiveDataSum.add(currentPrice).sub(previousPrice); | ||
} else { | ||
negativeDataSum = negativeDataSum.add(previousPrice).sub(currentPrice); | ||
} | ||
} | ||
|
||
// Check that there must be a positive or negative price change | ||
require( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add comment explaining logic here |
||
negativeDataSum > 0 || positiveDataSum > 0, | ||
"RSILibrary.calculate: Not valid RSI Value" | ||
); | ||
|
||
// a = 100 * SUM(Loss) | ||
uint256 a = HUNDRED.mul(negativeDataSum); | ||
// b = SUM(Gain) + SUM(Loss) | ||
uint256 b = positiveDataSum.add(negativeDataSum); | ||
// c = a / b | ||
uint256 c = a.div(b); | ||
|
||
return HUNDRED.sub(c); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/* | ||
Copyright 2019 Set Labs Inc. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
pragma solidity 0.5.7; | ||
|
||
import { RSILibrary } from "../../meta-oracles/lib/RSILibrary.sol"; | ||
|
||
/** | ||
* @title RSILibraryMock | ||
* @author Set Protocol | ||
* | ||
* Mock contract for interacting with RSILibrary | ||
*/ | ||
contract RSILibraryMock { | ||
|
||
/* ============ External Function ============ */ | ||
|
||
function calculateMock( | ||
uint256[] calldata _dataArray | ||
) | ||
external | ||
returns (uint256) | ||
{ | ||
return RSILibrary.calculate( | ||
_dataArray | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
require('module-alias/register'); | ||
|
||
import * as _ from 'lodash'; | ||
import * as chai from 'chai'; | ||
import { BigNumber } from 'bignumber.js'; | ||
|
||
import ChaiSetup from '@utils/chaiSetup'; | ||
import { BigNumberSetup } from '@utils/bigNumberSetup'; | ||
import { | ||
RSILibraryMockContract, | ||
} from '@utils/contracts'; | ||
import { Blockchain } from '@utils/blockchain'; | ||
import { ZERO } from '@utils/constants'; | ||
import { expectRevertError } from '@utils/tokenAssertions'; | ||
import { ether } from '@utils/units'; | ||
import { getWeb3 } from '@utils/web3Helper'; | ||
|
||
import { LibraryMockHelper } from '@utils/helpers/libraryMockHelper'; | ||
import { OracleHelper } from '@utils/helpers/oracleHelper'; | ||
|
||
BigNumberSetup.configure(); | ||
ChaiSetup.configure(); | ||
const web3 = getWeb3(); | ||
const { expect } = chai; | ||
const blockchain = new Blockchain(web3); | ||
|
||
contract('RSILibrary', accounts => { | ||
const [ | ||
deployerAccount, | ||
] = accounts; | ||
|
||
let rsiLibraryMock: RSILibraryMockContract; | ||
|
||
const libraryMockHelper = new LibraryMockHelper(deployerAccount); | ||
const oracleHelper = new OracleHelper(deployerAccount); | ||
|
||
beforeEach(async () => { | ||
blockchain.saveSnapshotAsync(); | ||
|
||
rsiLibraryMock = await libraryMockHelper.deployRSILibraryMockAsync(); | ||
}); | ||
|
||
afterEach(async () => { | ||
blockchain.revertAsync(); | ||
}); | ||
|
||
describe('#calculate', async () => { | ||
let subjectTimePeriod: number; | ||
let subjectSeededValues: BigNumber[]; | ||
|
||
let customTimePeriod: number; | ||
let customSeededValues: BigNumber[]; | ||
|
||
beforeEach(async () => { | ||
subjectTimePeriod = customTimePeriod || 14; | ||
subjectSeededValues = customSeededValues || | ||
Array.from({length: subjectTimePeriod}, () => ether(Math.floor(Math.random() * 100) + 100)); | ||
}); | ||
|
||
afterEach(async () => { | ||
customTimePeriod = undefined; | ||
customSeededValues = undefined; | ||
}); | ||
|
||
async function subject(): Promise<BigNumber> { | ||
return rsiLibraryMock.calculateMock.callAsync( | ||
subjectSeededValues, | ||
); | ||
} | ||
|
||
it('returns the correct RSI value', async () => { | ||
const output = await subject(); | ||
const expectedOutput = oracleHelper.calculateRSI( | ||
subjectSeededValues, | ||
); | ||
expect(output).to.be.bignumber.equal(expectedOutput); | ||
}); | ||
|
||
describe('using custom seeded values', async () => { | ||
before(async () => { | ||
customSeededValues = [ether(1.5), ether(2), ether(1.5), ether(3)]; | ||
}); | ||
|
||
it('returns the correct RSI value', async () => { | ||
const output = await subject(); | ||
expect(output).to.be.bignumber.equal(20); | ||
}); | ||
}); | ||
|
||
describe('using custom seeded values where prices keep declining', async () => { | ||
before(async () => { | ||
customSeededValues = [ether(1.143), ether(1.243), ether(1.343)]; | ||
}); | ||
|
||
it('returns the correct RSI value of 0', async () => { | ||
const output = await subject(); | ||
expect(output).to.be.bignumber.equal(ZERO); | ||
}); | ||
}); | ||
|
||
describe('using custom seeded values where prices keep rising', async () => { | ||
before(async () => { | ||
customSeededValues = [ether(1.643), ether(1.642), ether(1.641), ether(1.640), ether(1.639)]; | ||
}); | ||
|
||
it('returns the correct RSI value of 100', async () => { | ||
const output = await subject(); | ||
expect(output).to.be.bignumber.equal(100); | ||
}); | ||
}); | ||
|
||
describe('using custom seeded values where prices are the same', async () => { | ||
before(async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
customSeededValues = [ether(1.643), ether(1.643), ether(1.643), ether(1.643), ether(1.643), ether(1.643)]; | ||
}); | ||
|
||
it('should revert', async () => { | ||
await expectRevertError(subject()); | ||
}); | ||
}); | ||
|
||
describe('using only one seeded value', async () => { | ||
before(async () => { | ||
customSeededValues = [ether(1.643)]; | ||
}); | ||
|
||
it('should revert', async () => { | ||
await expectRevertError(subject()); | ||
}); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know its not on the other EMA file, but let's add documentation here what
a
,b
, andc
below map to in this formula.