This repository has been archived by the owner on Feb 18, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #190 from centrifuge/linear-solver
Linear solver setup
- Loading branch information
Showing
7 changed files
with
322 additions
and
5 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import assert from 'assert' | ||
import { ITinlake } from '../types/tinlake' | ||
import { createTinlake } from '../test/utils' | ||
import testConfig from '../test/config' | ||
|
||
let tinlake: ITinlake | ||
|
||
describe('coordinator tests', async () => { | ||
before(async () => { | ||
tinlake = createTinlake(testConfig.godAccount, testConfig) | ||
}) | ||
|
||
describe('epoch solver', async () => { | ||
it('should return an optimal solution when limited by the max TIN ratio', async () => { | ||
const state = { | ||
netAssetValue: 800, | ||
reserve: 200, | ||
seniorDebt: 700, | ||
seniorBalance: 100, | ||
minTinRatio: 0.15, | ||
maxTinRatio: 0.2, | ||
maxReserve: 10000, | ||
} | ||
|
||
const orderState = { | ||
tinRedeemOrder: 100, | ||
dropRedeemOrder: 300, | ||
tinInvestOrder: 200, | ||
dropInvestOrder: 400, | ||
} | ||
|
||
const result = await tinlake.calculateOptimalSolution(state, orderState) | ||
|
||
assert.equal(result.status, 5) | ||
assert.equal(result.z > 0, true) | ||
assert.equal(result.vars.tinRedeem, 100) | ||
assert.equal(result.vars.dropRedeem, 300) | ||
assert.equal(result.vars.tinInvest, 125) | ||
assert.equal(result.vars.dropInvest, 400) | ||
}) | ||
|
||
it('should return an optimal solution when limited by the max reserve', async () => { | ||
// The gap between the maxReserve and reserve is 300, so 300 tokens can be invested. | ||
const state = { | ||
netAssetValue: 800, | ||
reserve: 200, | ||
seniorDebt: 700, | ||
seniorBalance: 100, | ||
minTinRatio: 0.0, | ||
maxTinRatio: 1.0, | ||
maxReserve: 500, | ||
} | ||
|
||
// 50 is redeemed, so 300+50=350 can be invested. | ||
const orderState = { | ||
tinRedeemOrder: 0, | ||
dropRedeemOrder: 50, | ||
tinInvestOrder: 200, | ||
dropInvestOrder: 200, | ||
} | ||
|
||
const result = await tinlake.calculateOptimalSolution(state, orderState) | ||
|
||
// The full redeem is possible, while only 350/400 of total invest orders are possible. | ||
// TIN investments have preference over DROP investments, so the full TIN invest order is fulfilled, while the tin invest order is limited by 150. | ||
assert.equal(result.status, 5) | ||
assert.equal(result.z > 0, true) | ||
assert.equal(result.vars.tinRedeem, 0) | ||
assert.equal(result.vars.dropRedeem, 50) | ||
assert.equal(result.vars.tinInvest, 200) | ||
assert.equal(result.vars.dropInvest, 150) | ||
}) | ||
|
||
it('should return no feasible solution if the input state is unhealthy', async () => { | ||
const state = { | ||
netAssetValue: 800, | ||
reserve: 200, | ||
seniorDebt: 700, | ||
seniorBalance: 100, | ||
minTinRatio: 0.01, | ||
maxTinRatio: 0.01, | ||
maxReserve: 500, | ||
} | ||
|
||
const orderState = { | ||
tinRedeemOrder: 100, | ||
dropRedeemOrder: 200, | ||
tinInvestOrder: 300, | ||
dropInvestOrder: 400, | ||
} | ||
|
||
const result = await tinlake.calculateOptimalSolution(state, orderState) | ||
assert.equal(result.status, 4) | ||
assert.equal(result.z, 0) | ||
}) | ||
|
||
it('should return a feasible solution if the input state is unhealthy, but a feasible solution can be found using the orders', async () => { | ||
const state = { | ||
netAssetValue: 800, | ||
reserve: 200, | ||
seniorDebt: 700, | ||
seniorBalance: 100, | ||
minTinRatio: 0.01, | ||
maxTinRatio: 0.01, | ||
maxReserve: 50000, | ||
} | ||
|
||
const orderState = { | ||
tinRedeemOrder: 10000, | ||
dropRedeemOrder: 10000, | ||
tinInvestOrder: 10000, | ||
dropInvestOrder: 10000, | ||
} | ||
|
||
const result = await tinlake.calculateOptimalSolution(state, orderState) | ||
|
||
assert.equal(result.status, 5) | ||
assert.equal(result.z > 0, true) | ||
assert.equal(result.vars.tinRedeem > 0, true) | ||
assert.equal(result.vars.dropRedeem > 0, true) | ||
assert.equal(result.vars.tinInvest > 0, true) | ||
assert.equal(result.vars.dropInvest > 0, true) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
import { Constructor, TinlakeParams } from '../Tinlake' | ||
|
||
export function CoordinatorActions<ActionsBase extends Constructor<TinlakeParams>>(Base: ActionsBase) { | ||
return class extends Base implements ICoordinatorActions { | ||
solveEpoch = async () => { | ||
// const tinlake = (this as any) | ||
// const reserve = (await tinlake.getJuniorReserve()).add(await tinlake.getSeniorReserve()) | ||
|
||
// const state = { | ||
// reserve, | ||
// netAssetValue: 0, | ||
// seniorDebt: await tinlake.getSeniorDebt(), | ||
// seniorBalance: 0, | ||
// minTinRatio: await tinlake.getMinJuniorRatio(), | ||
// maxTinRatio: 0, | ||
// maxReserve: 0, | ||
// } | ||
|
||
return Promise.resolve({ | ||
tinRedeem: 1, | ||
dropRedeem: 2, | ||
tinInvest: 3, | ||
dropInvest: 4 | ||
}) | ||
} | ||
|
||
calculateOptimalSolution = async (state: State, orderState: OrderState) => { | ||
return require('glpk.js').then((glpk: any) => { | ||
const lp = { | ||
name: 'LP', | ||
generals: ['dropRedeem', 'tinRedeem', 'tinInvest', 'dropInvest'], | ||
objective: { | ||
// Maximize: dropRedeem > tinRedeem > tinInvest > dropInvest | ||
direction: glpk.GLP_MAX, | ||
name: 'obj', | ||
vars: [ | ||
{ name: 'dropRedeem', coef: 10000 }, | ||
{ name: 'tinRedeem', coef: 1000 }, | ||
{ name: 'tinInvest', coef: 100 }, | ||
{ name: 'dropInvest', coef: 10 }, | ||
], | ||
}, | ||
subjectTo: [ | ||
{ | ||
name: 'currencyAvailable', | ||
vars: [ | ||
{ name: 'tinInvest', coef: 1.0 }, | ||
{ name: 'dropInvest', coef: 1.0 }, | ||
{ name: 'tinRedeem', coef: -1.0 }, | ||
{ name: 'dropRedeem', coef: -1.0 }, | ||
], | ||
bnds: { type: glpk.GLP_LO, ub: 0.0, lb: -state.reserve }, | ||
}, | ||
{ | ||
name: 'dropRedeemOrder', | ||
vars: [{ name: 'dropRedeem', coef: 1.0 }], | ||
bnds: { type: glpk.GLP_UP, ub: orderState.dropRedeemOrder, lb: 0.0 }, | ||
}, | ||
{ | ||
name: 'tinRedeemOrder', | ||
vars: [{ name: 'tinRedeem', coef: 1.0 }], | ||
bnds: { type: glpk.GLP_UP, ub: orderState.tinRedeemOrder, lb: 0.0 }, | ||
}, | ||
{ | ||
name: 'dropInvestOrder', | ||
vars: [{ name: 'dropInvest', coef: 1.0 }], | ||
bnds: { type: glpk.GLP_UP, ub: orderState.dropInvestOrder, lb: 0.0 }, | ||
}, | ||
{ | ||
name: 'tinInvestOrder', | ||
vars: [{ name: 'tinInvest', coef: 1.0 }], | ||
bnds: { type: glpk.GLP_UP, ub: orderState.tinInvestOrder, lb: 0.0 }, | ||
}, | ||
{ | ||
name: 'maxReserve', | ||
vars: [ | ||
{ name: 'tinRedeem', coef: -1.0 }, | ||
{ name: 'dropRedeem', coef: -1.0 }, | ||
{ name: 'tinInvest', coef: 1.0 }, | ||
{ name: 'dropInvest', coef: 1.0 }, | ||
], | ||
bnds: { type: glpk.GLP_UP, ub: state.maxReserve - state.reserve, lb: 0.0 }, | ||
}, | ||
/** | ||
* The next tow constraints were rewritten from the original equations in the epoch model. | ||
* For one, minTINRatio was rewritten as a lower bound, which means both sides were multiplied by -1. | ||
* Secondly, all output vars were moved to the left side, while all input vars were moved to the right side. | ||
* | ||
* E.g. for dropRedeem, in the epoch model there's both -I4*(1-B7) and +I4. | ||
* So: -I4*(1-B7) + I4 = -0.8 I4 + 1.0 I4 = 0.2 I4 = minTinRatio * dropRedeem. | ||
*/ | ||
{ | ||
name: 'minTINRatio', | ||
vars: [ | ||
{ name: 'tinRedeem', coef: -(1 - state.minTinRatio) }, | ||
{ name: 'dropRedeem', coef: state.minTinRatio }, | ||
{ name: 'tinInvest', coef: 1 - state.minTinRatio }, | ||
{ name: 'dropInvest', coef: -state.minTinRatio }, | ||
], | ||
bnds: { | ||
type: glpk.GLP_LO, | ||
ub: 0.0, | ||
lb: | ||
-(1 - state.minTinRatio) * state.netAssetValue - | ||
(1 - state.minTinRatio) * state.reserve + | ||
state.seniorBalance + | ||
state.seniorDebt, | ||
}, | ||
}, | ||
{ | ||
name: 'maxTINRatio', | ||
vars: [ | ||
{ name: 'tinInvest', coef: -(1 - state.maxTinRatio) }, | ||
{ name: 'dropInvest', coef: state.maxTinRatio }, | ||
{ name: 'tinRedeem', coef: 1 - state.maxTinRatio }, | ||
{ name: 'dropRedeem', coef: -state.maxTinRatio }, | ||
], | ||
bnds: { | ||
type: glpk.GLP_LO, | ||
ub: 0.0, | ||
lb: | ||
(1 - state.maxTinRatio) * state.netAssetValue + | ||
(1 - state.maxTinRatio) * state.reserve - | ||
state.seniorBalance - | ||
state.seniorDebt, | ||
}, | ||
}, | ||
], | ||
} | ||
|
||
const output = glpk.solve(lp, glpk.GLP_MSG_ERR) | ||
return output.result | ||
}) | ||
} | ||
} | ||
} | ||
|
||
export type ICoordinatorActions = { | ||
solveEpoch(): Promise<SolverSolution> | ||
calculateOptimalSolution(state: State, orderState: OrderState): Promise<SolverResult> | ||
} | ||
|
||
export default CoordinatorActions | ||
|
||
interface BaseState { | ||
netAssetValue: number | ||
reserve: number | ||
seniorDebt: number | ||
seniorBalance: number | ||
} | ||
|
||
export interface State extends BaseState { | ||
netAssetValue: number | ||
reserve: number | ||
seniorDebt: number | ||
seniorBalance: number | ||
minTinRatio: number | ||
maxTinRatio: number | ||
maxReserve: number | ||
} | ||
|
||
export interface OrderState { | ||
tinRedeemOrder: number | ||
dropRedeemOrder: number | ||
tinInvestOrder: number | ||
dropInvestOrder: number | ||
} | ||
|
||
export interface SolverSolution { | ||
tinRedeem: number | ||
dropRedeem: number | ||
tinInvest: number | ||
dropInvest: number | ||
} | ||
|
||
export interface SolverResult { | ||
z: number | ||
status: number | ||
vars: SolverSolution | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,3 +5,5 @@ declare module '*.abi' { | |
|
||
declare module 'ethjs' | ||
declare module 'web3-utils' | ||
|
||
declare module 'glpk.js' |