diff --git a/artifacts/index.ts b/artifacts/index.ts index e0130e4..42a3358 100644 --- a/artifacts/index.ts +++ b/artifacts/index.ts @@ -9,6 +9,7 @@ export { RSITrendingTrigger } from './ts/RSITrendingTrigger'; export { SocialAllocator } from './ts/SocialAllocator'; export { SocialTradingManager } from './ts/SocialTradingManager'; export { SocialTradingManagerV2 } from './ts/SocialTradingManagerV2'; +export { TwoMovingAverageCrossoverTrigger } from './ts/TwoMovingAverageCrossoverTrigger'; // Export abi-gen contract wrappers export { @@ -24,4 +25,5 @@ export { SocialAllocatorContract, SocialTradingManagerContract, SocialTradingManagerV2Contract, + TwoMovingAverageCrossoverTriggerContract, } from "../utils/contracts"; diff --git a/contracts/managers/triggers/TwoMovingAverageCrossoverTrigger.sol b/contracts/managers/triggers/TwoMovingAverageCrossoverTrigger.sol new file mode 100644 index 0000000..677de45 --- /dev/null +++ b/contracts/managers/triggers/TwoMovingAverageCrossoverTrigger.sol @@ -0,0 +1,81 @@ +/* + Copyright 2020 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 { IMetaOracleV2 } from "set-protocol-oracles/contracts/meta-oracles/interfaces/IMetaOracleV2.sol"; + +import { ITrigger } from "./ITrigger.sol"; + + +/** + * @title TwoMovingAverageCrossoverTrigger + * @author Set Protocol + * + * Implementing the ITrigger interface, this contract is queried by a + * RebalancingSetToken Manager to determine if the market is in a bullish + * state by checking if the shorter term moving average is above the longer term + * moving average. + * Note: The MA oracles can be the same or different contracts to allow flexbility of timeframes + * and types of moving averages (EMA, SMA) + */ +contract TwoMovingAverageCrossoverTrigger is + ITrigger +{ + using SafeMath for uint256; + + /* ============ State Variables ============ */ + IMetaOracleV2 public shortTermMAOracle; + IMetaOracleV2 public longTermMAOracle; + uint256 public shortTermMATimePeriod; + uint256 public longTermMATimePeriod; + + /* + * TwoMovingAverageCrossoverTrigger constructor. + * + * @param _longTermMAOracle The instance of longer term MA oracle + * @param _shortTermMAOracle The instance of shorter term MA oracle + * @param _longTermMATimePeriod The time period in the longer term MA oracle to use in the calculation + * @param _shortTermMATimePeriod The time period in the shorter term MA oracle to use in the calculation + */ + constructor( + IMetaOracleV2 _longTermMAOracle, + IMetaOracleV2 _shortTermMAOracle, + uint256 _longTermMATimePeriod, + uint256 _shortTermMATimePeriod + ) + public + { + longTermMAOracle = _longTermMAOracle; + shortTermMAOracle = _shortTermMAOracle; + longTermMATimePeriod = _longTermMATimePeriod; + shortTermMATimePeriod = _shortTermMATimePeriod; + } + + /* ============ External ============ */ + + /* + * If shorter term MA is greater than longer term MA return true, else return false + */ + function isBullish() external view returns (bool) { + uint256 longTermMovingAverage = longTermMAOracle.read(longTermMATimePeriod); + uint256 shortTermMovingAverage = shortTermMAOracle.read(shortTermMATimePeriod); + + return shortTermMovingAverage > longTermMovingAverage; + } +} \ No newline at end of file diff --git a/package.json b/package.json index 83b6c14..d39824d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "set-protocol-strategies", - "version": "1.1.39", + "version": "1.1.40", "main": "dist/artifacts/index.js", "typings": "dist/typings/artifacts/index.d.ts", "files": [ diff --git a/test/contracts/managers/triggers/twoMovingAverageCrossoverTrigger.spec.ts b/test/contracts/managers/triggers/twoMovingAverageCrossoverTrigger.spec.ts new file mode 100644 index 0000000..1aa56df --- /dev/null +++ b/test/contracts/managers/triggers/twoMovingAverageCrossoverTrigger.spec.ts @@ -0,0 +1,255 @@ +require('module-alias/register'); + +import * as _ from 'lodash'; +import * as ABIDecoder from 'abi-decoder'; +import * as chai from 'chai'; +import * as setProtocolUtils from 'set-protocol-utils'; + +import { Address } from 'set-protocol-utils'; +import { BigNumber } from 'bignumber.js'; + +import ChaiSetup from '@utils/chaiSetup'; +import { BigNumberSetup } from '@utils/bigNumberSetup'; +import { Blockchain } from 'set-protocol-contracts'; +import { ether } from '@utils/units'; + +import { + LegacyMakerOracleAdapterContract, + LinearizedPriceDataSourceContract, + MedianContract, + MovingAverageOracleV2Contract, + OracleProxyContract, + TimeSeriesFeedContract, +} from 'set-protocol-oracles'; +import { + TwoMovingAverageCrossoverTriggerContract, +} from '@utils/contracts'; + +import { + DEFAULT_GAS, + ONE_DAY_IN_SECONDS +} from '@utils/constants'; + +import { getWeb3 } from '@utils/web3Helper'; + +import { ManagerHelper } from '@utils/helpers/managerHelper'; +import { OracleHelper } from 'set-protocol-oracles'; +import { ProtocolHelper } from '@utils/helpers/protocolHelper'; + +BigNumberSetup.configure(); +ChaiSetup.configure(); + +const TwoMovingAverageCrossoverTrigger = artifacts.require('TwoMovingAverageCrossoverTrigger'); +const web3 = getWeb3(); +const { expect } = chai; +const blockchain = new Blockchain(web3); +const { SetProtocolTestUtils: SetTestUtils } = setProtocolUtils; + +contract('TwoMovingAverageCrossoverTrigger', accounts => { + const [ + deployerAccount, + ] = accounts; + + let ethMedianizer: MedianContract; + let legacyMakerOracleAdapter: LegacyMakerOracleAdapterContract; + let oracleProxy: OracleProxyContract; + let linearizedDataSource: LinearizedPriceDataSourceContract; + let longTermTimeSeriesFeed: TimeSeriesFeedContract; + let shortTermTimeSeriesFeed: TimeSeriesFeedContract; + let longTermMAOracle: MovingAverageOracleV2Contract; + let shortTermMAOracle: MovingAverageOracleV2Contract; + + let trigger: TwoMovingAverageCrossoverTriggerContract; + + let initialEthPrice: BigNumber; + + const managerHelper = new ManagerHelper(deployerAccount); + const oracleHelper = new OracleHelper(deployerAccount); + const protocolHelper = new ProtocolHelper(deployerAccount); + + before(async () => { + ABIDecoder.addABI(TwoMovingAverageCrossoverTrigger.abi); + }); + + after(async () => { + ABIDecoder.removeABI(TwoMovingAverageCrossoverTrigger.abi); + }); + + beforeEach(async () => { + blockchain.saveSnapshotAsync(); + + ethMedianizer = await protocolHelper.getDeployedWETHMedianizerAsync(); + await oracleHelper.addPriceFeedOwnerToMedianizer(ethMedianizer, deployerAccount); + + initialEthPrice = ether(150); + await oracleHelper.updateMedianizerPriceAsync( + ethMedianizer, + initialEthPrice, + SetTestUtils.generateTimestamp(1000), + ); + + + legacyMakerOracleAdapter = await oracleHelper.deployLegacyMakerOracleAdapterAsync( + ethMedianizer.address, + ); + + oracleProxy = await oracleHelper.deployOracleProxyAsync( + legacyMakerOracleAdapter.address, + ); + + const interpolationThreshold = ONE_DAY_IN_SECONDS; + linearizedDataSource = await oracleHelper.deployLinearizedPriceDataSourceAsync( + oracleProxy.address, + interpolationThreshold, + ); + + await oracleHelper.addAuthorizedAddressesToOracleProxy( + oracleProxy, + [linearizedDataSource.address] + ); + + const seededLongTermMATimePeriod = _.map(new Array(20), function(el, i) {return ether(150 + i); }); + longTermTimeSeriesFeed = await oracleHelper.deployTimeSeriesFeedAsync( + linearizedDataSource.address, + seededLongTermMATimePeriod + ); + + const dataDescriptionLongTermMA = 'ETHDaily20MA'; + longTermMAOracle = await oracleHelper.deployMovingAverageOracleV2Async( + longTermTimeSeriesFeed.address, + dataDescriptionLongTermMA + ); + + const seededShortTermMATimePeriod = _.map(new Array(10), function(el, i) {return ether(200 + i); }); + shortTermTimeSeriesFeed = await oracleHelper.deployTimeSeriesFeedAsync( + linearizedDataSource.address, + seededShortTermMATimePeriod + ); + + const dataDescriptionShortTermMA = 'ETHHourly10MA'; + shortTermMAOracle = await oracleHelper.deployMovingAverageOracleV2Async( + shortTermTimeSeriesFeed.address, + dataDescriptionShortTermMA + ); + }); + + afterEach(async () => { + blockchain.revertAsync(); + }); + + describe('#constructor', async () => { + let subjectLongTermMAOracle: Address; + let subjectShortTermMAOracle: Address; + let subjectLongTermMATimePeriod: BigNumber; + let subjectShortTermMATimePeriod: BigNumber; + + beforeEach(async () => { + subjectLongTermMAOracle = longTermMAOracle.address; + subjectShortTermMAOracle = shortTermMAOracle.address; + subjectLongTermMATimePeriod = new BigNumber(20); + subjectShortTermMATimePeriod = new BigNumber(10); + }); + + async function subject(): Promise { + return managerHelper.deployTwoMovingAverageCrossoverTrigger( + subjectLongTermMAOracle, + subjectShortTermMAOracle, + subjectLongTermMATimePeriod, + subjectShortTermMATimePeriod, + ); + } + + it('sets the correct long term moving average oracle address', async () => { + trigger = await subject(); + + const actualMovingAveragePriceFeedAddress = await trigger.longTermMAOracle.callAsync(); + + expect(actualMovingAveragePriceFeedAddress).to.equal(subjectLongTermMAOracle); + }); + + it('sets the correct short term moving average oracle address', async () => { + trigger = await subject(); + + const actualMovingAveragePriceFeedAddress = await trigger.shortTermMAOracle.callAsync(); + + expect(actualMovingAveragePriceFeedAddress).to.equal(subjectShortTermMAOracle); + }); + + it('sets the correct long term moving average days', async () => { + trigger = await subject(); + + const actualMovingAverageTimePeriod = await trigger.longTermMATimePeriod.callAsync(); + + expect(actualMovingAverageTimePeriod).to.be.bignumber.equal(subjectLongTermMATimePeriod); + }); + + it('sets the correct short term moving average days', async () => { + trigger = await subject(); + + const actualMovingAverageTimePeriod = await trigger.shortTermMATimePeriod.callAsync(); + + expect(actualMovingAverageTimePeriod).to.be.bignumber.equal(subjectShortTermMATimePeriod); + }); + }); + + describe('#isBullish', async () => { + let subjectCaller: Address; + + let updatedLongTermTimePeriod: BigNumber[]; + + before(async () => { + updatedLongTermTimePeriod = _.map(new Array(20), function(el, i) {return ether(150 + i); }); + }); + + beforeEach(async () => { + const longTermMATimePeriod = new BigNumber(20); + const shortTermMATimePeriod = new BigNumber(10); + + trigger = await managerHelper.deployTwoMovingAverageCrossoverTrigger( + longTermMAOracle.address, + shortTermMAOracle.address, + longTermMATimePeriod, + shortTermMATimePeriod + ); + await oracleHelper.addAuthorizedAddressesToOracleProxy( + oracleProxy, + [trigger.address] + ); + + await oracleHelper.batchUpdateTimeSeriesFeedAsync( + longTermTimeSeriesFeed, + ethMedianizer, + updatedLongTermTimePeriod.length, + updatedLongTermTimePeriod + ); + + subjectCaller = deployerAccount; + }); + + async function subject(): Promise { + return trigger.isBullish.callAsync( + { from: subjectCaller, gas: DEFAULT_GAS} + ); + } + + it('returns true', async () => { + const result = await subject(); + expect(result).to.be.true; + }); + + describe('price going from bullish to bearish', async () => { + before(async () => { + updatedLongTermTimePeriod = _.map(new Array(19), function(el, i) {return ether(300 + i); }); + }); + + after(async () => { + updatedLongTermTimePeriod = _.map(new Array(19), function(el, i) {return ether(150 + i); }); + }); + + it('returns false', async () => { + const result = await subject(); + expect(result).to.be.false; + }); + }); + }); +}); \ No newline at end of file diff --git a/utils/contracts.ts b/utils/contracts.ts index 35b78a2..6676da1 100644 --- a/utils/contracts.ts +++ b/utils/contracts.ts @@ -10,6 +10,7 @@ export { MACOStrategyManagerContract } from '../types/generated/m_a_c_o_strategy export { MACOStrategyManagerV2Contract } from '../types/generated/m_a_c_o_strategy_manager_v2'; export { MovingAverageCrossoverTriggerContract } from '../types/generated/moving_average_crossover_trigger'; export { TriggerMockContract } from '../types/generated/trigger_mock'; +export { TwoMovingAverageCrossoverTriggerContract } from '../types/generated/two_moving_average_crossover_trigger'; export { RSITrendingTriggerContract } from '../types/generated/r_s_i_trending_trigger'; export { SocialTradingManagerContract } from '../types/generated/social_trading_manager'; export { SocialTradingManagerV2Contract } from '../types/generated/social_trading_manager_v2'; diff --git a/utils/helpers/managerHelper.ts b/utils/helpers/managerHelper.ts index 7b1683e..cbaf4d2 100644 --- a/utils/helpers/managerHelper.ts +++ b/utils/helpers/managerHelper.ts @@ -24,6 +24,7 @@ import { SocialTradingManagerContract, SocialTradingManagerV2Contract, TriggerMockContract, + TwoMovingAverageCrossoverTriggerContract, SocialAllocatorContract, } from '../contracts'; import { BigNumber } from 'bignumber.js'; @@ -62,7 +63,9 @@ const SocialTradingManagerV2 = importArtifactsFromSource('SocialTradingManagerV2 const TriggerMock = importArtifactsFromSource('TriggerMock'); const UintArrayUtilsLibrary = importArtifactsFromSource('UintArrayUtilsLibrary'); const SocialAllocator = importArtifactsFromSource('SocialAllocator'); - +const TwoMovingAverageCrossoverTrigger = importArtifactsFromSource( + 'TwoMovingAverageCrossoverTrigger' +); const { SetProtocolUtils: SetUtils, SetProtocolTestUtils: SetTestUtils } = setProtocolUtils; const setTestUtils = new SetTestUtils(web3); const { SET_FULL_TOKEN_UNITS } = SetUtils.CONSTANTS; @@ -341,6 +344,27 @@ export class ManagerHelper { ); } + public async deployTwoMovingAverageCrossoverTrigger( + longTermMAOracle: Address, + shortTermMAOracle: Address, + longTermMADays: BigNumber, + shortTermMADays: BigNumber, + from: Address = this._tokenOwnerAddress, + ): Promise { + const trufflePriceTrigger = await TwoMovingAverageCrossoverTrigger.new( + longTermMAOracle, + shortTermMAOracle, + longTermMADays, + shortTermMADays, + { from } + ); + + return new TwoMovingAverageCrossoverTriggerContract( + getContractInstance(trufflePriceTrigger), + { from, gas: DEFAULT_GAS }, + ); + } + public async deployRSITrendingTrigger( rsiOracleInstance: Address, lowerBound: BigNumber,