Skip to content
This repository was archived by the owner on Jan 18, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions contracts/meta-oracles/RSIOracle.sol
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);
}
}
99 changes: 99 additions & 0 deletions contracts/meta-oracles/lib/RSILibrary.sol
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)))
*
Copy link
Contributor

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, and c below map to in this formula.

*
* @param _dataArray Array of prices used to calculate the RSI
* @returns The RSI value
*/
function calculate(
uint256[] memory _dataArray
Copy link
Contributor

Choose a reason for hiding this comment

The 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(
Copy link
Contributor

Choose a reason for hiding this comment

The 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"
);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment saying what this for block is doing

// 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(
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
}
}
41 changes: 41 additions & 0 deletions contracts/mocks/oracles/RSILibraryMock.sol
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
);
}
}
132 changes: 132 additions & 0 deletions test/contracts/oracles/lib/rsiLibrary.spec.ts
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 () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The before and after should be consistent - otherwise you'll have weird issues with any tests that run after this. What I mean is, it needs to be beforeAll and afterAll or beforeEach and afterEach - and not a mix

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());
});
});
});
});
Loading