first commit
This commit is contained in:
commit
aa7869eb33
386 files changed
+69375
No files matched your search
@@ -0,0 +1,99 @@
|
||||
<script>
|
||||
|
||||
import top from './components/layout/header.vue'
|
||||
import bottom from './components/layout/footer.vue'
|
||||
import modal from './components/layout/modal.vue'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {
|
||||
top,
|
||||
bottom,
|
||||
modal
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang='pug'>
|
||||
#app
|
||||
top
|
||||
.fill
|
||||
router-view.view
|
||||
bottom
|
||||
modal
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fill {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.text {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
input {
|
||||
background: none;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.params {
|
||||
min-height: 235px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
.hr {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
height: 10px;
|
||||
background-color: rgba(250,250,250,.99);
|
||||
}
|
||||
|
||||
.contain {
|
||||
width: 900px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
display: inline-block;
|
||||
margin-right: 12px;
|
||||
margin-bottom: 12px;
|
||||
height: 40px;
|
||||
padding: 0 18px;
|
||||
border-radius: 4px;
|
||||
background-color: #3498db;
|
||||
text-shadow: 0 1px 3px rgba(36,180,126,.4);
|
||||
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
line-height: 40px;
|
||||
transition: transform 250ms;
|
||||
}
|
||||
|
||||
.btn--primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 7px 14px rgba(50,50,93,.1), 0 3px 6px rgba(0,0,0,.08);
|
||||
background-color: #3498db;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn--primary:active,
|
||||
.btn--primary:focus {
|
||||
background-color: #3498db;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn--primary:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<template lang='pug'>
|
||||
div
|
||||
dataset-picker.my2(v-on:dataset='updateDataset').contain
|
||||
.hr
|
||||
strat-picker.my2(v-on:stratConfig='updateStrat').contain
|
||||
.hr
|
||||
paper-trader(v-on:settings='updatePaperTrader').contain
|
||||
.hr
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import datasetPicker from '../global/configbuilder/datasetpicker.vue'
|
||||
import stratPicker from '../global/configbuilder/stratpicker.vue'
|
||||
import paperTrader from '../global/configbuilder/papertrader.vue'
|
||||
import _ from 'lodash'
|
||||
import { get } from '../../tools/ajax'
|
||||
|
||||
export default {
|
||||
created: function() {
|
||||
get('configPart/performanceAnalyzer', (error, response) => {
|
||||
this.performanceAnalyzer = toml.parse(response.part);
|
||||
this.performanceAnalyzer.enabled = true;
|
||||
});
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
dataset: {},
|
||||
strat: {},
|
||||
paperTrader: {},
|
||||
performanceAnalyzer: {}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
stratPicker,
|
||||
datasetPicker,
|
||||
paperTrader
|
||||
},
|
||||
computed: {
|
||||
market: function() {
|
||||
if(!this.dataset.exchange)
|
||||
return {};
|
||||
|
||||
return {
|
||||
exchange: this.dataset.exchange,
|
||||
currency: this.dataset.currency,
|
||||
asset: this.dataset.asset
|
||||
}
|
||||
},
|
||||
range: function() {
|
||||
if(!this.dataset.exchange)
|
||||
return {};
|
||||
|
||||
return {
|
||||
from: this.dataset.from,
|
||||
to: this.dataset.to
|
||||
}
|
||||
},
|
||||
config: function() {
|
||||
let config = {};
|
||||
Object.assign(
|
||||
config,
|
||||
{ watch: this.market },
|
||||
{ paperTrader: this.paperTrader },
|
||||
this.strat,
|
||||
{
|
||||
backtest: {
|
||||
daterange: this.range
|
||||
},
|
||||
backtestResultExporter: {
|
||||
enabled: true,
|
||||
writeToDisk: false,
|
||||
data: {
|
||||
stratUpdates: false,
|
||||
roundtrips: true,
|
||||
stratCandles: true,
|
||||
stratCandleProps: ['open'],
|
||||
trades: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{ performanceAnalyzer: this.performanceAnalyzer },
|
||||
);
|
||||
|
||||
config.valid = this.validConfig(config);
|
||||
config.backtestResultExporter.enabled = true;
|
||||
|
||||
return config;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validConfig: function(config) {
|
||||
if(!config.backtest)
|
||||
return false;
|
||||
|
||||
if(!config.backtest.daterange)
|
||||
return false;
|
||||
|
||||
if(_.isEmpty(config.backtest.daterange))
|
||||
return false;
|
||||
|
||||
if(!config.watch)
|
||||
return false;
|
||||
|
||||
if(!config.tradingAdvisor)
|
||||
return false;
|
||||
|
||||
let strat = config.tradingAdvisor.method;
|
||||
if(_.isEmpty(config[ strat ]))
|
||||
return false;
|
||||
|
||||
if(config.tradingAdvisor) {
|
||||
if(_.isNaN(config.tradingAdvisor.candleSize))
|
||||
return false;
|
||||
else if(config.tradingAdvisor.candleSize == 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
updateDataset: function(set) {
|
||||
this.dataset = set;
|
||||
this.$emit('config', this.config);
|
||||
},
|
||||
updateStrat: function(sc) {
|
||||
this.strat = sc;
|
||||
this.$emit('config', this.config);
|
||||
},
|
||||
updatePaperTrader: function(pt) {
|
||||
this.paperTrader = pt;
|
||||
this.paperTrader.enabled = true;
|
||||
this.$emit('config', this.config);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template lang='pug'>
|
||||
div
|
||||
h2.contain Backtest
|
||||
.hr
|
||||
config-builder(v-on:config='check')
|
||||
div(v-if='backtestable')
|
||||
.txt--center
|
||||
a.w100--s.my1.btn--primary(href='#', v-if='backtestState !== "fetching"', v-on:click.prevent='run') Backtest
|
||||
div(v-if='backtestState === "fetching"').scan-btn
|
||||
p Running backtest..
|
||||
spinner
|
||||
result(v-if='backtestResult && backtestState === "fetched"', :result='backtestResult')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import configBuilder from './backtestConfigBuilder.vue'
|
||||
import result from './result/result.vue'
|
||||
import { post } from '../../tools/ajax'
|
||||
import spinner from '../global/blockSpinner.vue'
|
||||
|
||||
export default {
|
||||
data: () => {
|
||||
return {
|
||||
backtestable: false,
|
||||
backtestState: 'idle',
|
||||
backtestResult: false,
|
||||
config: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
check: function(config) {
|
||||
// console.log('CHECK', config);
|
||||
this.config = config;
|
||||
|
||||
if(!config.valid)
|
||||
return this.backtestable = false;
|
||||
|
||||
this.backtestable = true;
|
||||
},
|
||||
run: function() {
|
||||
this.backtestState = 'fetching';
|
||||
|
||||
post('backtest', this.config, (error, response) => {
|
||||
this.backtestState = 'fetched';
|
||||
this.backtestResult = response;
|
||||
});
|
||||
}
|
||||
},
|
||||
components: {
|
||||
configBuilder,
|
||||
result,
|
||||
spinner
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template lang='pug'>
|
||||
#chartWrapper(v-bind:class='{ clickable: !isClicked }')
|
||||
.shield(v-on:click.prevent='click')
|
||||
svg#chart(width='960', :height='height')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import chart from '../../../d3/chart4'
|
||||
import { draw as drawMessage, clear as clearMessage } from '../../../d3/message'
|
||||
|
||||
const MIN_CANDLES = 4;
|
||||
|
||||
export default {
|
||||
props: ['data', 'height'],
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
isClicked: false
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
data: function() { this.render() },
|
||||
},
|
||||
|
||||
created: function() { setTimeout( this.render, 100) },
|
||||
beforeDestroy: function() {
|
||||
this.remove();
|
||||
},
|
||||
|
||||
methods: {
|
||||
click: function() {
|
||||
this.isClicked = true;
|
||||
},
|
||||
render: function() {
|
||||
this.remove();
|
||||
|
||||
|
||||
if(_.size(this.data.candles) < MIN_CANDLES) {
|
||||
drawMessage('Not enough data to spawn chart');
|
||||
} else {
|
||||
chart(this.data.candles, this.data.trades, this.height);
|
||||
}
|
||||
},
|
||||
remove: function() {
|
||||
d3.select('#chart').html('');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
#chartWrapper.clickable {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#chartWrapper.clickable .shield {
|
||||
cursor: zoom-in;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: grey;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
#chart {
|
||||
background-color: #eee;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#chart circle {
|
||||
clip-path: url(#clip);
|
||||
}
|
||||
|
||||
#chart .zoom {
|
||||
cursor: move;
|
||||
fill: none;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
#chart .line {
|
||||
fill: none;
|
||||
stroke: steelblue;
|
||||
stroke-width: 1.5px;
|
||||
clip-path: url(#clip);
|
||||
}
|
||||
|
||||
/*#chart .price.line {
|
||||
stroke-width: 2.5px;
|
||||
}*/
|
||||
|
||||
#chart circle.buy {
|
||||
fill: #7FFF00;
|
||||
}
|
||||
|
||||
#chart circle.sell {
|
||||
fill: red;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template lang='pug'>
|
||||
div
|
||||
.hr.contain
|
||||
div.contain
|
||||
h3 Backtest result
|
||||
result-summary(:report='result.performanceReport')
|
||||
.hr.contain
|
||||
chart(:data='candles', height='500')
|
||||
.hr.contain
|
||||
roundtripTable(:roundtrips='result.roundtrips')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import resultSummary from './summary.vue'
|
||||
import chart from './chartWrapper.vue'
|
||||
import roundtripTable from './roundtripTable.vue'
|
||||
|
||||
export default {
|
||||
props: ['result'],
|
||||
data: () => {
|
||||
return {}
|
||||
},
|
||||
methods: {},
|
||||
components: {
|
||||
roundtripTable,
|
||||
resultSummary,
|
||||
chart
|
||||
},
|
||||
computed: {
|
||||
candles: function() {
|
||||
return {
|
||||
candles: this.result.stratCandles,
|
||||
trades: this.result.trades
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template lang='pug'>
|
||||
.contain.roundtrips
|
||||
h2 Roundtrips
|
||||
table(v-if='roundtrips.length')
|
||||
thead
|
||||
tr
|
||||
th Entry at (UTC)
|
||||
th Exit at (UTC)
|
||||
th Exposure
|
||||
th Entry balance
|
||||
th Exit balance
|
||||
th P&L
|
||||
th Profit
|
||||
tr(v-for='rt in roundtrips')
|
||||
td {{ fmt(rt.entryAt) }}
|
||||
td {{ fmt(rt.exitAt) }}
|
||||
td {{ diff(rt.duration) }}
|
||||
td {{ round(rt.entryBalance) }}
|
||||
td {{ round(rt.exitBalance) }}
|
||||
template(v-if="Math.sign(rt.pnl)===-1")
|
||||
td.loss {{ Math.sign(rt.pnl)*rt.pnl.toFixed(2) }}
|
||||
td.loss {{ rt.profit.toFixed(2) }}%
|
||||
template(v-else)
|
||||
td.profit {{ rt.pnl.toFixed(2) }}
|
||||
td.profit {{ rt.profit.toFixed(2) }}%
|
||||
div(v-if='!roundtrips.length')
|
||||
p Not enough data to display
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
props: ['roundtrips'],
|
||||
data: () => {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
diff: n => moment.duration(n).humanize(),
|
||||
humanizeDuration: (n) => window.humanizeDuration(n),
|
||||
fmt: date => {
|
||||
|
||||
// roundtrips coming out of a backtest
|
||||
// are unix timestamp, live roundtrips
|
||||
// are date strings.
|
||||
// TODO: normalize
|
||||
|
||||
let mom;
|
||||
|
||||
if(_.isNumber(date)) {
|
||||
mom = moment.unix(date);
|
||||
} else {
|
||||
mom = moment(date).utc();
|
||||
}
|
||||
|
||||
return mom.utc().format('YYYY-MM-DD HH:mm');
|
||||
},
|
||||
round: n => (+n).toFixed(3),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.roundtrips {
|
||||
margin-top: 50px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.roundtrips table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roundtrips table th,
|
||||
.roundtrips table td {
|
||||
border: 1px solid #c6cbd1;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.roundtrips table td.loss {
|
||||
color: red;
|
||||
text-align: right;
|
||||
}
|
||||
.roundtrips table td.profit {
|
||||
color: green;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.roundtrips table tr:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template lang='pug'>
|
||||
div.contain
|
||||
|
||||
.grd-row.summary
|
||||
.grd-row-col-3-6
|
||||
table.p1
|
||||
tr
|
||||
th start time
|
||||
td {{ report.startTime }}
|
||||
tr
|
||||
th end time
|
||||
td {{ report.endTime }}
|
||||
tr
|
||||
th timespan
|
||||
td {{ report.timespan }}
|
||||
tr
|
||||
th start price
|
||||
td {{ round(report.startPrice) }} {{ report.currency }}
|
||||
tr
|
||||
th end price
|
||||
td {{ round(report.endPrice) }} {{ report.currency }}
|
||||
tr
|
||||
th market
|
||||
td {{ round(report.market) }}%
|
||||
|
||||
paperTradeSummary(:report='report')
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import paperTradeSummary from '../../global/paperTradeSummary.vue'
|
||||
|
||||
export default {
|
||||
props: ['report'],
|
||||
components: {
|
||||
paperTradeSummary
|
||||
},
|
||||
methods: {
|
||||
round: n => (+n).toFixed(5)
|
||||
},
|
||||
computed: {
|
||||
profitClass: function() {
|
||||
if(this.report.relativeProfit > 0)
|
||||
return 'profit'
|
||||
else
|
||||
return 'loss'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.summary td {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template lang='pug'>
|
||||
.grd.contain
|
||||
h3 Add an API key
|
||||
p Make sure that the API key has the permissions to create and cancel orders and view balances.
|
||||
.grd-row
|
||||
.grd-row-col-3-6.mx1
|
||||
h3 Exchange
|
||||
exchange-picker(v-on:exchange='updateExchange', only-tradable='true')
|
||||
.grd-row-col-3-6.mx1
|
||||
h3 Credentials
|
||||
template(v-for='cred in requires')
|
||||
label {{ cred }}
|
||||
input(v-model='credentials[cred]')
|
||||
.txt--center
|
||||
a.w100--s.my1.btn--primary(href='#', v-on:click.prevent='upload') Add
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import exchangePicker from '../global/configbuilder/exchangepicker.vue'
|
||||
import _ from 'lodash'
|
||||
import { post } from '../../tools/ajax';
|
||||
|
||||
export default {
|
||||
data: () => {
|
||||
return {
|
||||
exchange: false,
|
||||
credentials: {}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
exchangePicker
|
||||
},
|
||||
computed: {
|
||||
apiKeySets: function() {
|
||||
return this.$store.state.apiKeys;
|
||||
},
|
||||
exchanges: function() {
|
||||
return this.$store.state.exchanges;
|
||||
},
|
||||
requires: function() {
|
||||
if(!this.exchanges)
|
||||
return [];
|
||||
|
||||
if(!this.exchange)
|
||||
return [];
|
||||
|
||||
return this.exchanges[this.exchange].requires;
|
||||
},
|
||||
config: function() {
|
||||
let config = {
|
||||
exchange: this.exchange,
|
||||
values: this.credentials
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
credentials: function() {
|
||||
this.emitConfig();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateExchange: function(exchange) {
|
||||
this.credentials = {};
|
||||
this.exchange = exchange;
|
||||
this.emitConfig();
|
||||
},
|
||||
emitConfig: function() {
|
||||
this.$emit('config', this.config);
|
||||
},
|
||||
upload: function() {
|
||||
|
||||
let exchange = this.config.exchange;
|
||||
|
||||
if(
|
||||
this.exchanges &&
|
||||
this.apiKeySets.includes(exchange) &&
|
||||
!confirm(`You already have API keys for ${exchange} defined, do you want to overwrite them?`)
|
||||
)
|
||||
return;
|
||||
|
||||
post('addApiKey', this.config, (error, response) => {
|
||||
if(error)
|
||||
return alert(error);
|
||||
|
||||
this.credentials = {};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template lang='pug'>
|
||||
div.contain
|
||||
h2 Config
|
||||
.hr
|
||||
h3 Available API keys
|
||||
p(v-if='!apiKeySets.length')
|
||||
em You don't have any API keys yet.
|
||||
ul
|
||||
li(v-for='exchange in apiKeySets') {{ exchange }} (
|
||||
a(href='#', v-on:click.prevent='removeApiKey(exchange)') remove
|
||||
| )
|
||||
a.btn--primary(href='#', v-if='!addApiToggle', v-on:click.prevent='openAddApi') Add an API key
|
||||
template(v-if='addApiToggle')
|
||||
.hr
|
||||
apiConfigBuilder
|
||||
.hr
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiConfigBuilder from './apiConfigBuilder.vue';
|
||||
import { post } from '../../tools/ajax';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
apiConfigBuilder
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
addApiToggle: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openAddApi: function() {
|
||||
this.addApiToggle = true;
|
||||
},
|
||||
removeApiKey: function(exchange) {
|
||||
if(!confirm('Are you sure you want to delete these API keys?'))
|
||||
return;
|
||||
|
||||
post('removeApiKey', {exchange}, (error, response) => {
|
||||
if(error)
|
||||
return alert(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
apiKeySets: function() {
|
||||
return this.$store.state.apiKeys
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
apiKeySets: function() {
|
||||
this.addApiToggle = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,103 @@
|
||||
<template lang='pug'>
|
||||
.contain
|
||||
.text(v-html='intro')
|
||||
.hr
|
||||
h2 Available datasets
|
||||
.txt--center.my2(v-if='datasetScanstate === "idle"')
|
||||
a.w100--s.btn--primary.scan-btn(href='#', v-on:click.prevent='scan') Scan available data
|
||||
.txt--center.my2(v-if='datasetScanstate === "scanning"')
|
||||
spinner
|
||||
.my2(v-if='datasetScanstate === "scanned"')
|
||||
.bg--orange.p1.warning.my1(v-if='unscannableMakets.length')
|
||||
p.clickable(v-if='!viewUnscannable', v-on:click.prevent='toggleUnscannable') Some markets were unscannable, click here for details.
|
||||
template(v-if='viewUnscannable')
|
||||
p Unable to find datasets in the following markets:
|
||||
.mx2(v-for='market in unscannableMakets')
|
||||
| - {{ market.exchange }}:{{ market.currency }}:{{ market.asset }}
|
||||
template(v-if='datasets.length')
|
||||
table.full.data
|
||||
thead
|
||||
tr
|
||||
th exchange
|
||||
th currency
|
||||
th asset
|
||||
th from
|
||||
th to
|
||||
th duration
|
||||
tbody
|
||||
tr(v-for='set in datasets')
|
||||
td {{ set.exchange }}
|
||||
td {{ set.currency }}
|
||||
td {{ set.asset }}
|
||||
td {{ fmt(set.from) }}
|
||||
td {{ fmt(set.to) }}
|
||||
td {{ humanizeDuration(set.to.diff(set.from)) }}
|
||||
template(v-if='!datasets.length')
|
||||
p It looks like you don't have any local data yet.
|
||||
.my2
|
||||
h2 Import more data
|
||||
p.text You can easily import more market data directly from exchanges using the importer.
|
||||
router-link.btn--primary(to='/data/importer') Go to the importer!
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import spinner from '../global/blockSpinner.vue'
|
||||
import marked from '../../tools/marked'
|
||||
import dataset from '../global/mixins/dataset'
|
||||
// global moment
|
||||
// global humanizeDuration
|
||||
|
||||
let intro = marked(`
|
||||
|
||||
## Local data
|
||||
|
||||
Gekko needs local market data in order to backtest strategies. The local
|
||||
data can also be used in a warmup period when running a strategy against a
|
||||
live market.
|
||||
|
||||
`);
|
||||
|
||||
export default {
|
||||
mixins: [ dataset ],
|
||||
components: {
|
||||
spinner
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
intro,
|
||||
viewUnscannable: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleUnscannable: function() { this.viewUnscannable = true },
|
||||
humanizeDuration: (n) => window.humanizeDuration(n),
|
||||
fmt: mom => mom.format('YYYY-MM-DD HH:mm'),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
table.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.full td {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
table.full.data th {
|
||||
text-align: left;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.warning p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template lang='pug'>
|
||||
.grd.contain
|
||||
.grd-row
|
||||
.grd-row-col-3-6.mx1
|
||||
h3 Market
|
||||
market-picker(v-on:market='updateMarketConfig', only-importable='true')
|
||||
.grd-row-col-3-6.mx1
|
||||
range-creator(v-on:range='updateRange')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import marketPicker from '../../global/configbuilder/marketpicker.vue'
|
||||
import rangeCreator from '../../global/configbuilder/rangecreator.vue'
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
data: () => {
|
||||
return {
|
||||
market: {},
|
||||
range: {}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
marketPicker,
|
||||
rangeCreator
|
||||
},
|
||||
computed: {
|
||||
config: function() {
|
||||
|
||||
let config = {};
|
||||
Object.assign(
|
||||
config,
|
||||
this.market,
|
||||
{
|
||||
importer: {
|
||||
daterange: this.range
|
||||
}
|
||||
},
|
||||
{
|
||||
candleWriter: { enabled: true }
|
||||
}
|
||||
);
|
||||
|
||||
return config;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateMarketConfig: function(mc) {
|
||||
this.market = mc;
|
||||
this.emitConfig();
|
||||
},
|
||||
updateRange: function(range) {
|
||||
this.range = range;
|
||||
this.emitConfig();
|
||||
},
|
||||
emitConfig: function() {
|
||||
this.$emit('config', this.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template lang='pug'>
|
||||
div.contain.my2
|
||||
div.text(v-html='intro')
|
||||
.hr
|
||||
h3 Currently running imports
|
||||
p(v-if='imports.length === 0') You currently don't have any imports running.
|
||||
ul(v-if='imports.length')
|
||||
li(v-for='_import in imports')
|
||||
router-link(:to='"/data/importer/import/" + _import.id') {{ _import.watch.exchange }}:{{ _import.watch.currency }}/{{ _import.watch.asset }}
|
||||
|
||||
.hr
|
||||
h3 Start a new import
|
||||
import-config-builder(v-on:config='updateConfig')
|
||||
.hr
|
||||
.txt--center
|
||||
a.w100--s.my1.btn--primary(href='#', v-on:click.prevent='run') Import
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { post } from '../../../tools/ajax'
|
||||
import spinner from '../../global/blockSpinner.vue'
|
||||
import importConfigBuilder from './importConfigBuilder.vue'
|
||||
|
||||
import marked from '../../../tools/marked'
|
||||
|
||||
let intro = marked(`
|
||||
|
||||
## Import data
|
||||
|
||||
The importer can download historical market data directly from the exchange.
|
||||
|
||||
`)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
importConfigBuilder,
|
||||
spinner
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
intro,
|
||||
config: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
imports: function() {
|
||||
return this.$store.state.imports
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
daysApart: function(range) {
|
||||
let to = moment(range.to);
|
||||
let from = moment(range.from);
|
||||
|
||||
return to.diff(from, 'days');
|
||||
},
|
||||
updateConfig: function(config) {
|
||||
this.config = config;
|
||||
},
|
||||
run: function() {
|
||||
let daysApart = this.daysApart(this.config.importer.daterange);
|
||||
|
||||
if(daysApart < 1)
|
||||
return alert('You can only import at least one day of data..')
|
||||
|
||||
let exchange = this.$store.state.exchanges[this.config.watch.exchange];
|
||||
if ("exchangeMaxHistoryAge" in exchange) {
|
||||
if (moment(this.config.importer.daterange.from) < moment().subtract(exchange.exchangeMaxHistoryAge, "days")) {
|
||||
return alert('Your date from is too old for ' + this.config.watch.exchange + '. It supports only the last ' + exchange.exchangeMaxHistoryAge + ' days..');
|
||||
}
|
||||
}
|
||||
|
||||
post('import', this.config, (error, response) => {
|
||||
if(error)
|
||||
return alert(error);
|
||||
|
||||
this.$store.commit('addImport', response);
|
||||
|
||||
this.$router.push({
|
||||
path: `/data/importer/import/${response.id}`,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template lang='pug'>
|
||||
div.contain.my2
|
||||
div(v-if='data && !data.done')
|
||||
h2 Importing data..
|
||||
.grd
|
||||
.grd-row
|
||||
.grd-row-col-2-6 <strong>Market:</strong>
|
||||
.grd-row-col-4-6 {{ data.watch.exchange }}
|
||||
.grd-row
|
||||
.grd-row-col-2-6 <strong>Currency/Asset:</strong>
|
||||
.grd-row-col-4-6 {{ data.watch.currency }}/{{ data.watch.asset }}
|
||||
|
||||
.grd
|
||||
.grd-row
|
||||
.grd-row-col-2-6 <strong>From:</strong>
|
||||
.grd-row-col-4-6 {{ fmt(from) }}
|
||||
.grd-row
|
||||
.grd-row-col-2-6 <strong>To:</strong>
|
||||
.grd-row-col-4-6 {{ fmt(to) }}
|
||||
.grd-row(v-if='initialized')
|
||||
.grd-row-col-2-6 <strong>Imported data until:</strong>
|
||||
.grd-row-col-4-6 {{ fmt(latest) }}
|
||||
.grd-row(v-if='initialized')
|
||||
.grd-row-col-2-6 <strong>Remaining:</strong>
|
||||
.grd-row-col-4-6 {{ fromEnd }}
|
||||
spinner(v-if='!initialized')
|
||||
.contain(v-if='initialized')
|
||||
progressBar(:progress='progress')
|
||||
p
|
||||
em (you don't have to wait until the import is done,
|
||||
| you can already start
|
||||
router-link(to='/backtest') backtesting
|
||||
| ).
|
||||
div(v-if='data && data.done').txt--center
|
||||
h2 Import done
|
||||
p
|
||||
| Go and
|
||||
router-link(to='/backtest') backtest
|
||||
| with your new data!
|
||||
div(v-if='!data').txt--center
|
||||
h2 ERROR: Unknown import
|
||||
p
|
||||
I don't know this import..
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import _ from 'lodash';
|
||||
import progressBar from '../../global/progressBar.vue'
|
||||
import spinner from '../../global/blockSpinner.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
progressBar,
|
||||
spinner
|
||||
},
|
||||
computed: {
|
||||
data: function() {
|
||||
return _.find(
|
||||
this.$store.state.imports,
|
||||
{ id: this.$route.params.id }
|
||||
);
|
||||
},
|
||||
initialized: function() {
|
||||
if(this.data && this.latest.isValid())
|
||||
return true
|
||||
},
|
||||
latest: function() {
|
||||
if(this.data)
|
||||
return this.mom(this.data.latest);
|
||||
},
|
||||
fromEndMs: function() {
|
||||
if(this.data)
|
||||
return this.to.diff(this.latest);
|
||||
},
|
||||
fromEnd: function() {
|
||||
if(!this.latest)
|
||||
return 'LOADING'
|
||||
|
||||
return humanizeDuration(this.fromEndMs);
|
||||
},
|
||||
from: function() {
|
||||
if(this.data)
|
||||
return this.mom(this.data.from)
|
||||
},
|
||||
to: function() {
|
||||
if(this.data)
|
||||
return this.mom(this.data.to)
|
||||
},
|
||||
timespan: function() {
|
||||
if(this.data)
|
||||
return this.to.diff(this.from)
|
||||
},
|
||||
progress: function() {
|
||||
if(!this.data)
|
||||
return;
|
||||
|
||||
const current = this.timespan - this.fromEndMs;
|
||||
return 100 * current / this.timespan;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fmt: mom => { return mom.format('YYYY-MM-DD HH:mm:ss') },
|
||||
mom: str => moment.utc(str)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<template lang='pug'>
|
||||
.grd.contain
|
||||
.grd-row
|
||||
.grd-row-col-3-6.mx1
|
||||
h3 Market
|
||||
market-picker(v-on:market='updateMarketConfig', :only-tradable='isTradebot')
|
||||
.grd-row-col-3-6.mx1
|
||||
type-picker(v-on:type='updateType')
|
||||
template(v-if='type !== "market watcher"')
|
||||
.hr
|
||||
strat-picker.contain.my2(v-on:stratConfig='updateStrat')
|
||||
.hr(v-if='type === "paper trader"')
|
||||
paper-trader(v-on:settings='updatePaperTrader', v-if='type === "paper trader"')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import marketPicker from '../global/configbuilder/marketpicker.vue'
|
||||
import typePicker from '../global/configbuilder/typepicker.vue'
|
||||
import stratPicker from '../global/configbuilder/stratpicker.vue'
|
||||
import paperTrader from '../global/configbuilder/papertrader.vue'
|
||||
import { get } from '../../tools/ajax'
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
|
||||
created: function() {
|
||||
get('configPart/candleWriter', (error, response) => {
|
||||
this.candleWriter = toml.parse(response.part);
|
||||
});
|
||||
get('configPart/performanceAnalyzer', (error, response) => {
|
||||
this.performanceAnalyzer = toml.parse(response.part);
|
||||
this.performanceAnalyzer.enabled = true;
|
||||
});
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
market: {},
|
||||
range: {},
|
||||
type: '',
|
||||
strat: {},
|
||||
paperTrader: {},
|
||||
candleWriter: {},
|
||||
performanceAnalyzer: {}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
marketPicker,
|
||||
typePicker,
|
||||
stratPicker,
|
||||
paperTrader
|
||||
},
|
||||
computed: {
|
||||
isTradebot: function() {
|
||||
return this.type === 'tradebot';
|
||||
},
|
||||
config: function() {
|
||||
let config = {};
|
||||
Object.assign(
|
||||
config,
|
||||
this.market,
|
||||
this.strat,
|
||||
{ paperTrader: this.paperTrader },
|
||||
{ candleWriter: this.candleWriter },
|
||||
{ type: this.type },
|
||||
{ performanceAnalyzer: this.performanceAnalyzer }
|
||||
);
|
||||
|
||||
if(this.isTradebot) {
|
||||
delete config.paperTrader;
|
||||
config.trader = { enabled: true }
|
||||
}
|
||||
|
||||
config.valid = this.validConfig(config);
|
||||
|
||||
return config;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validConfig: config => {
|
||||
if(config.type === 'market watcher')
|
||||
return true;
|
||||
|
||||
if(!config.tradingAdvisor)
|
||||
return false;
|
||||
if(_.isNaN(config.tradingAdvisor.candleSize))
|
||||
return false;
|
||||
else if(config.tradingAdvisor.candleSize == 0)
|
||||
return false;
|
||||
|
||||
let strat = config.tradingAdvisor.method;
|
||||
if(_.isEmpty(config[ strat ]))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
updateMarketConfig: function(mc) {
|
||||
this.market = mc;
|
||||
this.emitConfig();
|
||||
},
|
||||
updateType: function(type) {
|
||||
this.type = type;
|
||||
this.emitConfig();
|
||||
},
|
||||
updateStrat: function(strat) {
|
||||
this.strat = strat;
|
||||
this.emitConfig();
|
||||
},
|
||||
updatePaperTrader: function(pt) {
|
||||
this.paperTrader = pt;
|
||||
this.paperTrader.enabled = true;
|
||||
this.emitConfig();
|
||||
},
|
||||
|
||||
emitConfig: function() {
|
||||
this.$emit('config', this.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template lang='pug'>
|
||||
.contain.py2
|
||||
h3 Market watchers
|
||||
.text(v-if='!watchers.length')
|
||||
p You don't have any market watchers.
|
||||
table.full.clickable(v-if='watchers.length')
|
||||
thead
|
||||
tr
|
||||
th exchange
|
||||
th currency
|
||||
th asset
|
||||
th status
|
||||
th started at
|
||||
th last update
|
||||
th duration
|
||||
tbody
|
||||
tr.clickable(v-for='gekko in watchers', v-on:click='$router.push({path: `/live-gekkos/${gekko.id}`})')
|
||||
td {{ gekko.config.watch.exchange }}
|
||||
td {{ gekko.config.watch.currency }}
|
||||
td {{ gekko.config.watch.asset }}
|
||||
td {{ status(gekko) }}
|
||||
td
|
||||
template(v-if='gekko.events.initial.candle') {{ fmt(gekko.events.initial.candle.start) }}
|
||||
td
|
||||
template(v-if='gekko.events.latest.candle') {{ fmt(gekko.events.latest.candle.start) }}
|
||||
td
|
||||
template(v-if='gekko.events.initial.candle && gekko.events.latest.candle') {{ timespan(gekko.events.latest.candle.start, gekko.events.initial.candle.start) }}
|
||||
h3 Strat runners
|
||||
.text(v-if='!stratrunners.length')
|
||||
p You don't have any stratrunners.
|
||||
table.full(v-if='stratrunners.length')
|
||||
thead
|
||||
tr
|
||||
th exchange
|
||||
th currency
|
||||
th asset
|
||||
th status
|
||||
th duration
|
||||
th strategy
|
||||
th PnL
|
||||
th type
|
||||
th trades
|
||||
tbody
|
||||
tr.clickable(v-for='gekko in stratrunners', v-on:click='$router.push({path: `/live-gekkos/${gekko.id}`})')
|
||||
td {{ gekko.config.watch.exchange }}
|
||||
td {{ gekko.config.watch.currency }}
|
||||
td {{ gekko.config.watch.asset }}
|
||||
td {{ status(gekko) }}
|
||||
td
|
||||
template(v-if='gekko.events.initial.candle && gekko.events.latest.candle') {{ timespan(gekko.events.latest.candle.start, gekko.events.initial.candle.start) }}
|
||||
td {{ gekko.config.tradingAdvisor.method }}
|
||||
td
|
||||
template(v-if='!report(gekko)') 0
|
||||
template(v-if='report(gekko)') {{ round(report(gekko).profit) }} {{ report(gekko).currency }}
|
||||
td {{ gekko.logType }}
|
||||
td
|
||||
template(v-if='!gekko.events.tradeCompleted') 0
|
||||
template(v-if='gekko.events.tradeCompleted') {{ gekko.events.tradeCompleted.length }}
|
||||
.hr
|
||||
h2 Start a new live Gekko
|
||||
router-link.btn--primary(to='/live-gekkos/new') Start a new live Gekko!
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// global moment
|
||||
// global humanizeDuration
|
||||
|
||||
export default {
|
||||
created: function() {
|
||||
this.timer = setInterval(() => {
|
||||
this.now = moment();
|
||||
}, 1000)
|
||||
},
|
||||
destroyed: function() {
|
||||
clearTimeout(this.timer);
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
timer: false,
|
||||
now: moment()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
stratrunners: function() {
|
||||
return _.values(this.$store.state.gekkos)
|
||||
.concat(_.values(this.$store.state.archivedGekkos))
|
||||
.filter(g => {
|
||||
if(g.logType === 'papertrader')
|
||||
return true;
|
||||
|
||||
if(g.logType === 'tradebot')
|
||||
return true;
|
||||
|
||||
return false;
|
||||
})
|
||||
},
|
||||
watchers: function() {
|
||||
return _.values(this.$store.state.gekkos)
|
||||
.concat(_.values(this.$store.state.archivedGekkos))
|
||||
.filter(g => g.logType === 'watcher')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
humanizeDuration: (n) => window.humanizeDuration(n),
|
||||
moment: mom => moment.utc(mom),
|
||||
fmt: mom => moment.utc(mom).format('YYYY-MM-DD HH:mm'),
|
||||
round: n => (+n).toFixed(3),
|
||||
timespan: function(a, b) {
|
||||
return this.humanizeDuration(this.moment(a).diff(this.moment(b)))
|
||||
},
|
||||
status: state => {
|
||||
if(state.errored)
|
||||
return 'errored';
|
||||
if(state.stopped)
|
||||
return 'stopped';
|
||||
if(state.active)
|
||||
return 'running';
|
||||
|
||||
console.log('unknown state:', state);
|
||||
},
|
||||
report: state => {
|
||||
return _.get(state, 'events.latest.performanceReport');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
table.clickable {
|
||||
border-collapse: separate;
|
||||
}
|
||||
|
||||
tr.clickable td:nth-child(1) {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
tr.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
tr.clickable:hover {
|
||||
background: rgba(216,216,216,.99);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,200 @@
|
||||
<template lang='pug'>
|
||||
div.contain.my2
|
||||
h3 Start a new gekko
|
||||
gekko-config-builder(v-on:config='updateConfig')
|
||||
.hr
|
||||
.txt--center(v-if='config.valid')
|
||||
a.w100--s.my1.btn--primary(href='#', v-on:click.prevent='start', v-if="!pendingStratrunner") Start
|
||||
spinner(v-if='pendingStratrunner')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import _ from 'lodash'
|
||||
import Vue from 'vue'
|
||||
import { post } from '../../tools/ajax'
|
||||
import gekkoConfigBuilder from './gekkoConfigBuilder.vue'
|
||||
import spinner from '../global/blockSpinner.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
gekkoConfigBuilder,
|
||||
spinner
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
pendingStratrunner: false,
|
||||
config: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
gekkos: function() {
|
||||
return this.$store.state.gekkos;
|
||||
},
|
||||
watchConfig: function() {
|
||||
let raw = _.pick(this.config, 'watch', 'candleWriter');
|
||||
let watchConfig = Vue.util.extend({}, raw);
|
||||
watchConfig.type = 'market watcher';
|
||||
watchConfig.mode = 'realtime';
|
||||
return watchConfig;
|
||||
},
|
||||
requiredHistoricalData: function() {
|
||||
if(!this.config.tradingAdvisor || !this.config.valid)
|
||||
return;
|
||||
|
||||
let stratSettings = this.config.tradingAdvisor;
|
||||
return stratSettings.candleSize * stratSettings.historySize;
|
||||
},
|
||||
gekkoConfig: function() {
|
||||
var startAt;
|
||||
|
||||
if(!this.existingMarketWatcher)
|
||||
return;
|
||||
|
||||
if(!this.requiredHistoricalData)
|
||||
startAt = moment().utc().startOf('minute').format();
|
||||
else {
|
||||
// TODO: figure out whether we can stitch data
|
||||
// without looking at the existing watcher
|
||||
const optimal = moment().utc().startOf('minute')
|
||||
.subtract(this.requiredHistoricalData, 'minutes')
|
||||
.unix();
|
||||
|
||||
const available = moment
|
||||
.utc(this.existingMarketWatcher.events.initial.candle.start)
|
||||
.unix();
|
||||
|
||||
startAt = moment.unix(Math.max(optimal, available)).utc().format();
|
||||
}
|
||||
|
||||
const gekkoConfig = Vue.util.extend({
|
||||
market: {
|
||||
type: 'leech',
|
||||
from: startAt
|
||||
},
|
||||
mode: 'realtime'
|
||||
}, this.config);
|
||||
return gekkoConfig;
|
||||
},
|
||||
existingMarketWatcher: function() {
|
||||
const market = Vue.util.extend({}, this.watchConfig.watch);
|
||||
return _.find(this.gekkos, {config: {watch: market}});
|
||||
},
|
||||
exchange: function() {
|
||||
return this.watchConfig.watch.exchange;
|
||||
},
|
||||
existingTradebot: function() {
|
||||
return _.find(
|
||||
this.gekkos,
|
||||
g => {
|
||||
if(g.logType === 'tradebot' && g.config.watch.exchange === this.exchange) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
},
|
||||
availableApiKeys: function() {
|
||||
return this.$store.state.apiKeys;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// start the stratrunner
|
||||
existingMarketWatcher: function(val, prev) {
|
||||
if(!this.pendingStratrunner)
|
||||
return;
|
||||
|
||||
const gekko = this.existingMarketWatcher;
|
||||
|
||||
if(gekko.events.latest.candle) {
|
||||
this.pendingStratrunner = false;
|
||||
|
||||
this.startGekko((err, resp) => {
|
||||
this.$router.push({
|
||||
path: `/live-gekkos/${resp.id}`
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateConfig: function(config) {
|
||||
this.config = config;
|
||||
},
|
||||
start: function() {
|
||||
|
||||
// if the user starts a tradebot we do some
|
||||
// checks first.
|
||||
if(this.config.type === 'tradebot') {
|
||||
if(this.existingTradebot) {
|
||||
let str = 'You already have a tradebot running on this exchange';
|
||||
str += ', you can only run one tradebot per exchange.';
|
||||
return alert(str);
|
||||
}
|
||||
|
||||
if(!this.availableApiKeys.includes(this.exchange))
|
||||
return alert('Please first configure API keys for this exchange in the config page.')
|
||||
}
|
||||
|
||||
// internally a live gekko consists of two parts:
|
||||
//
|
||||
// - a market watcher
|
||||
// - a live gekko (strat runner + (paper) trader)
|
||||
//
|
||||
// however if the user selected type "market watcher"
|
||||
// the second part won't be created
|
||||
if(this.config.type === 'market watcher') {
|
||||
|
||||
// check if the specified market is already being watched
|
||||
if(this.existingMarketWatcher) {
|
||||
alert('This market is already being watched, redirecting you now...');
|
||||
this.$router.push({
|
||||
path: `/live-gekkos/${this.existingMarketWatcher.id}`
|
||||
});
|
||||
} else {
|
||||
this.startWatcher((error, resp) => {
|
||||
this.$router.push({
|
||||
path: `/live-gekkos/${resp.id}`
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if(this.existingMarketWatcher) {
|
||||
// the specified market is already being watched,
|
||||
// just start a gekko!
|
||||
this.startGekko(this.routeToGekko);
|
||||
|
||||
} else {
|
||||
// the specified market is not yet being watched,
|
||||
// we need to create a watcher
|
||||
this.startWatcher((err, resp) => {
|
||||
this.pendingStratrunner = resp.id;
|
||||
// now we just wait for the watcher to be properly initialized
|
||||
// (see the `watch.existingMarketWatcher` method)
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
routeToGekko: function(err, resp) {
|
||||
if(err || resp.error)
|
||||
return console.error(err, resp.error);
|
||||
|
||||
this.$router.push({
|
||||
path: `/live-gekkos/${resp.id}`
|
||||
});
|
||||
},
|
||||
startWatcher: function(next) {
|
||||
post('startGekko', this.watchConfig, next);
|
||||
},
|
||||
startGekko: function(next) {
|
||||
post('startGekko', this.gekkoConfig, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,361 @@
|
||||
<template lang='pug'>
|
||||
div.my2
|
||||
.contain(v-if='!data')
|
||||
h1 Unknown Gekko instance
|
||||
p Gekko doesn't know what gekko this is...
|
||||
div(v-if='data')
|
||||
h2.contain Gekko {{ type }}
|
||||
div(v-if='isArchived', class='contain brdr--mid-gray p1 bg--orange')
|
||||
| This is an archived Gekko, it is currently not running anymore.
|
||||
div(v-if='data.errorMessage', class='contain brdr--mid-gray p1 bg--orange')
|
||||
| This is Gekko crashed with the following error: {{ data.errorMessage }}
|
||||
.grd.contain
|
||||
.grd-row
|
||||
.grd-row-col-3-6
|
||||
h3 Market
|
||||
.grd-row
|
||||
.grd-row-col-3-6 Exchange
|
||||
.grd-row-col-3-6 {{ config.watch.exchange }}
|
||||
.grd-row
|
||||
.grd-row-col-3-6 Currency
|
||||
.grd-row-col-3-6 {{ config.watch.currency }}
|
||||
.grd-row
|
||||
.grd-row-col-3-6 Asset
|
||||
.grd-row-col-3-6 {{ config.watch.asset }}
|
||||
.grd-row
|
||||
.grd-row-col-3-6 Type
|
||||
.grd-row-col-3-6 {{ type }}
|
||||
.grd-row-col-3-6
|
||||
h3 Runtime
|
||||
spinner(v-if='isLoading')
|
||||
template(v-if='!isLoading')
|
||||
.grd-row(v-if='initialEvents.candle')
|
||||
.grd-row-col-2-6 Watching since
|
||||
.grd-row-col-4-6 {{ fmt(initialEvents.candle.start) }}
|
||||
.grd-row(v-if='latestEvents.candle')
|
||||
.grd-row-col-2-6 Received data until
|
||||
.grd-row-col-4-6 {{ fmt(latestEvents.candle.start) }}
|
||||
.grd-row(v-if='latestEvents.candle')
|
||||
.grd-row-col-2-6 Data spanning
|
||||
.grd-row-col-4-6 {{ humanizeDuration(moment(latestEvents.candle.start).diff(moment(initialEvents.candle.start))) }}
|
||||
template(v-if='isStratrunner')
|
||||
.grd-row
|
||||
.grd-row-col-2-6 Amount of trades
|
||||
.grd-row-col-4-6 {{ trades.length }}
|
||||
.grd-row
|
||||
.grd-row-col-2-6 Candle size
|
||||
.grd-row-col-4-6 {{ config.tradingAdvisor.candleSize }}
|
||||
.grd-row
|
||||
.grd-row-col-2-6 History size
|
||||
.grd-row-col-4-6 {{ config.tradingAdvisor.historySize }}
|
||||
div(v-if='warmupRemaining', class='contain brdr--mid-gray p1 bg--orange')
|
||||
| This stratrunner is still warming up for the next
|
||||
i {{ warmupRemaining.replace(',', ' and ') }}
|
||||
| , it will not trade until it is warmed up.
|
||||
.grd-row(v-if='isStratrunner')
|
||||
.grd-row-col-3-6
|
||||
h3 Strategy
|
||||
.grd-row
|
||||
.grd-row-col-3-6 Name
|
||||
.grd-row-col-3-6
|
||||
strong {{ stratName }}
|
||||
| Parameters
|
||||
pre {{ stratParams }}
|
||||
.grd-row-col-3-6
|
||||
h3 Profit report
|
||||
template(v-if='!report')
|
||||
p
|
||||
em(v-if='isArchived') This Gekko never executed a trade..
|
||||
em(v-if='!isArchived') Waiting for at least one trade..
|
||||
template(v-if='report')
|
||||
.grd-row
|
||||
.grd-row-col-3-6 Start balance
|
||||
.grd-row-col-3-6 {{ round(report.startBalance) }}
|
||||
.grd-row
|
||||
.grd-row-col-3-6 Current balance
|
||||
.grd-row-col-3-6 {{ round(report.balance) }}
|
||||
.grd-row
|
||||
.grd-row-col-3-6 Market
|
||||
.grd-row-col-3-6 {{round(report.market / 100 * report.startPrice)}} {{ config.watch.currency }} ({{ round(report.market) }} %)
|
||||
.grd-row
|
||||
.grd-row-col-3-6 Profit
|
||||
.grd-row-col-3-6 {{ round(report.profit) }} {{ config.watch.currency }} ({{ round(report.relativeProfit) }} %)
|
||||
.grd-row
|
||||
.grd-row-col-3-6 Alpha
|
||||
.grd-row-col-3-6 {{ round(report.alpha) }} {{ config.watch.currency }}
|
||||
p(v-if='isStratrunner && !watcher && !isArchived') WARNING: stale gekko, not attached to a watcher, please report
|
||||
a(href='https://github.com/askmike/gekko/issues') here
|
||||
| .
|
||||
p(v-if='!isArchived')
|
||||
a(v-on:click='stopGekko', class='w100--s my1 btn--red') Stop Gekko
|
||||
p(v-if='isArchived')
|
||||
a(v-on:click='deleteGekko', class='w100--s my1 btn--red') Delete Gekko
|
||||
p(v-if='isStratrunner && watcher && !isArchived')
|
||||
em This gekko gets market data from
|
||||
router-link(:to='"/live-gekkos/" + watcher.id') this market watcher
|
||||
| .
|
||||
template(v-if='!isLoading')
|
||||
h3.contain Market graph
|
||||
spinner(v-if='candleFetch === "fetching"')
|
||||
template(v-if='candleFetch === "fetched"')
|
||||
chart(:data='chartData', :height='300')
|
||||
roundtrips(v-if='isStratrunner', :roundtrips='roundtrips')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Vue from 'vue'
|
||||
import _ from 'lodash'
|
||||
|
||||
import { post } from '../../tools/ajax'
|
||||
import spinner from '../global/blockSpinner.vue'
|
||||
import chart from '../backtester/result/chartWrapper.vue'
|
||||
import roundtrips from '../backtester/result/roundtripTable.vue'
|
||||
import paperTradeSummary from '../global/paperTradeSummary.vue'
|
||||
// global moment
|
||||
|
||||
export default {
|
||||
created: function() {
|
||||
if(!this.isLoading)
|
||||
this.getCandles();
|
||||
},
|
||||
components: {
|
||||
spinner,
|
||||
chart,
|
||||
paperTradeSummary,
|
||||
roundtrips
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
candleFetch: 'idle',
|
||||
candles: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
id: function() {
|
||||
return this.$route.params.id;
|
||||
},
|
||||
gekkos: function() {
|
||||
return this.$store.state.gekkos;
|
||||
},
|
||||
archivedGekkos: function() {
|
||||
return this.$store.state.archivedGekkos;
|
||||
},
|
||||
data: function() {
|
||||
if(!this.gekkos)
|
||||
return false;
|
||||
if(_.has(this.gekkos, this.id))
|
||||
return this.gekkos[this.id];
|
||||
if(_.has(this.archivedGekkos, this.id))
|
||||
return this.archivedGekkos[this.id];
|
||||
|
||||
return false;
|
||||
},
|
||||
config: function() {
|
||||
return _.get(this, 'data.config');
|
||||
},
|
||||
latestEvents: function() {
|
||||
return _.get(this, 'data.events.latest');
|
||||
},
|
||||
initialEvents: function() {
|
||||
return _.get(this, 'data.events.initial');
|
||||
},
|
||||
trades: function() {
|
||||
return _.get(this, 'data.events.tradeCompleted') || [];
|
||||
},
|
||||
roundtrips: function() {
|
||||
return _.get(this, 'data.events.roundtrip') || [];
|
||||
},
|
||||
isLive: function() {
|
||||
return _.has(this.gekkos, this.id);
|
||||
},
|
||||
type: function() {
|
||||
return this.data.logType;
|
||||
},
|
||||
isStratrunner: function() {
|
||||
return this.type !== 'watcher';
|
||||
},
|
||||
isArchived: function() {
|
||||
return this.data.stopped;
|
||||
},
|
||||
warmupRemaining: function() {
|
||||
if(!this.isStratrunner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(this.isArchived) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(this.initialEvents.stratWarmupCompleted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!this.initialEvents.candle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const historySize = _.get(this.config, 'tradingAdvisor.historySize');
|
||||
|
||||
if(!historySize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const warmupTime = _.get(this.config, 'tradingAdvisor.candleSize') * historySize;
|
||||
|
||||
return humanizeDuration(
|
||||
moment(this.initialEvents.candle.start).add(warmupTime, 'm').diff(moment()),
|
||||
{ largest: 2 }
|
||||
);
|
||||
},
|
||||
chartData: function() {
|
||||
return {
|
||||
candles: this.candles,
|
||||
trades: this.trades
|
||||
}
|
||||
},
|
||||
report: function() {
|
||||
return _.get(this.latestEvents, 'performanceReport');
|
||||
},
|
||||
stratName: function() {
|
||||
if(this.data)
|
||||
return this.data.config.tradingAdvisor.method;
|
||||
},
|
||||
stratParams: function() {
|
||||
if(!this.data)
|
||||
return 'Loading...';
|
||||
|
||||
let stratParams = Vue.util.extend({}, this.data.config[this.stratName]);
|
||||
delete stratParams.__empty;
|
||||
|
||||
if(_.isEmpty(stratParams))
|
||||
return 'No parameters'
|
||||
|
||||
return JSON.stringify(stratParams, null, 4);
|
||||
},
|
||||
isLoading: function() {
|
||||
if(!this.data)
|
||||
return true;
|
||||
if(!_.get(this.data, 'events.initial.candle'))
|
||||
return true;
|
||||
if(!_.get(this.data, 'events.latest.candle'))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
watcher: function() {
|
||||
if(!this.isStratrunner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let watch = Vue.util.extend({}, this.data.config.watch);
|
||||
return _.find(this.gekkos, g => {
|
||||
if(g.id === this.id)
|
||||
return false;
|
||||
|
||||
return _.isEqual(watch, g.config.watch);
|
||||
});
|
||||
},
|
||||
hasLeechers: function() {
|
||||
if(this.isStratrunner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let watch = Vue.util.extend({}, this.data.config.watch);
|
||||
|
||||
return _.find(this.gekkos, g => {
|
||||
if(g.id === this.id)
|
||||
return false;
|
||||
|
||||
return _.isEqual(watch, g.config.watch);
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'data.events.latest.candle.start': function() {
|
||||
setTimeout(this.getCandles, _.random(100, 2000));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
round: n => (+n).toFixed(5),
|
||||
humanizeDuration: (n, x) => window.humanizeDuration(n, x),
|
||||
moment: mom => moment.utc(mom),
|
||||
fmt: mom => moment.utc(mom).format('YYYY-MM-DD HH:mm'),
|
||||
getCandles: function() {
|
||||
if(this.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.candleFetch === 'fetching') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.candleFetch = 'fetching';
|
||||
|
||||
let to = this.data.events.latest.candle.start;
|
||||
let from = this.data.events.initial.candle.start;
|
||||
let candleSize = 1;
|
||||
|
||||
if(this.type !== 'watcher') {
|
||||
candleSize = this.data.config.tradingAdvisor.candleSize;
|
||||
}
|
||||
|
||||
let config = {
|
||||
watch: this.data.config.watch,
|
||||
daterange: {
|
||||
to, from
|
||||
},
|
||||
candleSize
|
||||
};
|
||||
|
||||
// We timeout because of 2 reasons:
|
||||
// - In case we get a batch of candles we only fetch once
|
||||
// - This way we give the db (mostly sqlite) some time to write
|
||||
// the result before we query it.
|
||||
setTimeout(() => {
|
||||
post('getCandles', config, (err, res) => {
|
||||
this.candleFetch = 'fetched';
|
||||
if(!res || res.error || !_.isArray(res))
|
||||
return console.log(res);
|
||||
|
||||
this.candles = res.map(c => {
|
||||
c.start = moment.unix(c.start).utc().format();
|
||||
return c;
|
||||
});
|
||||
})
|
||||
}, _.random(150, 2500));
|
||||
},
|
||||
stopGekko: function() {
|
||||
if(this.hasLeechers) {
|
||||
return alert('This Gekko is fetching market data for multiple stratrunners, stop these first.');
|
||||
}
|
||||
|
||||
if(!confirm('Are you sure you want to stop this Gekko?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
post('stopGekko', { id: this.data.id }, (err, res) => {
|
||||
console.log('stopped gekko');
|
||||
});
|
||||
},
|
||||
deleteGekko: function() {
|
||||
if(!this.isArchived) {
|
||||
return alert('This Gekko is still running, stop it first!');
|
||||
}
|
||||
|
||||
if(!confirm('Are you sure you want to delete this Gekko?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
post('deleteGekko', { id: this.data.id }, (err, res) => {
|
||||
this.$router.push({
|
||||
path: `/live-gekkos/`
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,153 @@
|
||||
<template lang='pug'>
|
||||
div.my2
|
||||
.contain(v-if='!data')
|
||||
h1 Unknown Watcher
|
||||
p Gekko doesn't know what whatcher this is...
|
||||
div(v-if='data')
|
||||
h2.contain Market Watcher
|
||||
.grd.contain
|
||||
h3 Market
|
||||
.grd-row
|
||||
.grd-row-col-2-6 Exchange
|
||||
.grd-row-col-4-6 {{ data.watch.exchange }}
|
||||
.grd-row
|
||||
.grd-row-col-2-6 Currency
|
||||
.grd-row-col-4-6 {{ data.watch.currency }}
|
||||
.grd-row
|
||||
.grd-row-col-2-6 Asset
|
||||
.grd-row-col-4-6 {{ data.watch.asset }}
|
||||
h3 Statistics
|
||||
spinner(v-if='isLoading')
|
||||
template(v-if='!isLoading')
|
||||
.grd-row(v-if='data.firstCandle')
|
||||
.grd-row-col-2-6 Watching since
|
||||
.grd-row-col-4-6 {{ fmt(data.firstCandle.start) }}
|
||||
.grd-row(v-if='data.lastCandle')
|
||||
.grd-row-col-2-6 Received data until
|
||||
.grd-row-col-4-6 {{ fmt(data.lastCandle.start) }}
|
||||
.grd-row(v-if='data.lastCandle && data.firstCandle')
|
||||
.grd-row-col-2-6 Data spanning
|
||||
.grd-row-col-4-6 {{ humanizeDuration(moment(data.lastCandle.start).diff(moment(data.firstCandle.start))) }}
|
||||
template(v-if='!isLoading')
|
||||
h3.contain Market graph
|
||||
spinner(v-if='candleFetch === "fetching"')
|
||||
template(v-if='candles.length')
|
||||
chart(:data='chartData', :height='500')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { post } from '../../tools/ajax'
|
||||
import _ from 'lodash'
|
||||
import spinner from '../global/blockSpinner.vue'
|
||||
import Vue from 'vue'
|
||||
import chart from '../backtester/result/chartWrapper.vue'
|
||||
// global moment
|
||||
|
||||
export default {
|
||||
created: function() {
|
||||
if(!this.isLoading)
|
||||
this.getCandles();
|
||||
},
|
||||
components: {
|
||||
spinner,
|
||||
chart
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
candleFetch: 'idle',
|
||||
candles: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
watchers: function() {
|
||||
return this.$store.state.watchers;
|
||||
},
|
||||
data: function() {
|
||||
return _.find(this.watchers, {id: this.$route.params.id});
|
||||
},
|
||||
chartData: function() {
|
||||
return {
|
||||
candles: this.candles,
|
||||
trades: []
|
||||
}
|
||||
},
|
||||
isLoading: function() {
|
||||
if(!this.data)
|
||||
return true;
|
||||
if(!_.isObject(this.data.firstCandle))
|
||||
return true;
|
||||
if(!_.isObject(this.data.lastCandle))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'data.lastCandle.start': function() {
|
||||
this.candleFetch = 'dirty';
|
||||
},
|
||||
data: function(val, prev) {
|
||||
let complete = val && val.firstCandle && val.lastCandle;
|
||||
|
||||
if(!complete)
|
||||
return;
|
||||
|
||||
if(this.candleFetch !== 'fetched' )
|
||||
this.getCandles();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
humanizeDuration: (n) => window.humanizeDuration(n),
|
||||
moment: mom => moment.utc(mom),
|
||||
fmt: mom => moment.utc(mom).format('YYYY-MM-DD HH:mm'),
|
||||
getCandles: function() {
|
||||
|
||||
this.candleFetch = 'fetching';
|
||||
|
||||
// up unto we have data
|
||||
let to = moment.utc(
|
||||
this.data.lastCandle.start
|
||||
).unix();
|
||||
|
||||
// max 7 days of data
|
||||
let from = Math.max(
|
||||
moment.utc(this.data.firstCandle.start).unix(),
|
||||
moment.utc(to).subtract(7, 'days').unix()
|
||||
);
|
||||
|
||||
// TODO...
|
||||
const diff = to - from;
|
||||
let candleSize = 60;
|
||||
if(diff < 60 * 60 * 24) // a day
|
||||
if(diff < 60 * 60 * 12) // 3 hours
|
||||
candleSize = 1;
|
||||
else
|
||||
candleSize = 5;
|
||||
|
||||
from = moment.unix(from).utc().format();
|
||||
to = moment.unix(to).utc().format();
|
||||
|
||||
let config = {
|
||||
watch: this.data.watch,
|
||||
daterange: {
|
||||
to, from
|
||||
},
|
||||
// hourly candles
|
||||
candleSize
|
||||
};
|
||||
|
||||
post('getCandles', config, (err, res) => {
|
||||
this.candleFetch = 'fetched';
|
||||
this.candles = res.map(c => {
|
||||
c.start = moment.unix(c.start).utc().format();
|
||||
return c;
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
// http://tobiasahlin.com/spinkit/
|
||||
|
||||
<template lang='pug'>
|
||||
.spinner
|
||||
.rect1
|
||||
.rect2
|
||||
.rect3
|
||||
.rect4
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.spinner {
|
||||
margin: 20px auto 100px auto;
|
||||
width: 50px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.spinner > div {
|
||||
background-color: #333;
|
||||
height: 100%;
|
||||
width: 6px;
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
-webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out;
|
||||
animation: sk-stretchdelay 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.spinner .rect2 {
|
||||
-webkit-animation-delay: -1.1s;
|
||||
animation-delay: -1.1s;
|
||||
}
|
||||
|
||||
.spinner .rect3 {
|
||||
-webkit-animation-delay: -1.0s;
|
||||
animation-delay: -1.0s;
|
||||
}
|
||||
|
||||
.spinner .rect4 {
|
||||
-webkit-animation-delay: -0.9s;
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
|
||||
.spinner .rect5 {
|
||||
-webkit-animation-delay: -0.8s;
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-stretchdelay {
|
||||
0%, 40%, 100% { -webkit-transform: scaleY(0.4) }
|
||||
20% { -webkit-transform: scaleY(1.0) }
|
||||
}
|
||||
|
||||
@keyframes sk-stretchdelay {
|
||||
0%, 40%, 100% {
|
||||
transform: scaleY(0.4);
|
||||
-webkit-transform: scaleY(0.4);
|
||||
} 20% {
|
||||
transform: scaleY(1.0);
|
||||
-webkit-transform: scaleY(1.0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template lang='pug'>
|
||||
div
|
||||
h3 Select a dataset
|
||||
.txt--center.my2(v-if='datasetScanstate === "idle"')
|
||||
a.w100--s.btn--primary.scan-btn(href='#', v-on:click.prevent='scan') Scan available data
|
||||
.txt--center.my2(v-if='datasetScanstate === "scanning"')
|
||||
spinner
|
||||
.my2(v-if='datasetScanstate === "scanned"')
|
||||
|
||||
div(v-if='datasets.length != 0')
|
||||
table.full
|
||||
thead
|
||||
tr
|
||||
th
|
||||
th exchange
|
||||
th currency
|
||||
th asset
|
||||
th from
|
||||
th to
|
||||
th duration
|
||||
tbody
|
||||
tr(v-for='(set, i) in datasets')
|
||||
td.radio
|
||||
input(type='radio', name='dataset', :value='i', v-model='setIndex', v-bind:id='set.id')
|
||||
td
|
||||
label(v-bind:for='set.id') {{ set.exchange }}
|
||||
td
|
||||
label(v-bind:for='set.id') {{ set.currency }}
|
||||
td
|
||||
label(v-bind:for='set.id') {{ set.asset }}
|
||||
td
|
||||
label(v-bind:for='set.id') {{ fmt(set.from) }}
|
||||
td
|
||||
label(v-bind:for='set.id') {{ fmt(set.to) }}
|
||||
td
|
||||
label(v-bind:for='set.id') {{ humanizeDuration(set.to.diff(set.from)) }}
|
||||
a.btn--primary(href='#', v-on:click.prevent='openRange', v-if='!rangeVisible') Adjust range
|
||||
template(v-if='rangeVisible')
|
||||
div
|
||||
label(for='customFrom') From:
|
||||
input(v-model='customFrom')
|
||||
div
|
||||
label(for='customTo') To:
|
||||
input(v-model='customTo')
|
||||
|
||||
em(v-else) No Data found
|
||||
a(href='#/data/importer') Lets add some
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import _ from 'lodash'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { post } from '../../../tools/ajax'
|
||||
import spinner from '../../global/blockSpinner.vue'
|
||||
import dataset from '../../global/mixins/dataset'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
spinner
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
setIndex: -1,
|
||||
customTo: false,
|
||||
customFrom: false,
|
||||
rangeVisible: false,
|
||||
set: false
|
||||
};
|
||||
},
|
||||
mixins: [ dataset ],
|
||||
methods: {
|
||||
humanizeDuration: (n) => {
|
||||
return window.humanizeDuration(n, {largest: 4});
|
||||
},
|
||||
fmt: mom => mom.utc().format('YYYY-MM-DD HH:mm'),
|
||||
openRange: function() {
|
||||
if(this.setIndex === -1)
|
||||
return alert('Select a dataset to adjust range');
|
||||
|
||||
this.updateCustomRange();
|
||||
|
||||
this.rangeVisible = true;
|
||||
},
|
||||
updateCustomRange: function() {
|
||||
this.customTo = this.fmt(this.set.to);
|
||||
this.customFrom = this.fmt(this.set.from);
|
||||
},
|
||||
emitSet: function(val) {
|
||||
if(!val)
|
||||
return;
|
||||
|
||||
let set;
|
||||
|
||||
if(!this.customTo)
|
||||
set = val;
|
||||
else {
|
||||
set = Vue.util.extend({}, val);
|
||||
set.to = moment.utc(this.customTo, 'YYYY-MM-DD HH:mm').format();
|
||||
set.from = moment.utc(this.customFrom, 'YYYY-MM-DD HH:mm').format();
|
||||
}
|
||||
|
||||
this.$emit('dataset', set);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
setIndex: function() {
|
||||
this.set = this.datasets[this.setIndex];
|
||||
|
||||
this.updateCustomRange();
|
||||
|
||||
this.emitSet(this.set);
|
||||
},
|
||||
|
||||
customTo: function() { this.emitSet(this.set); },
|
||||
customFrom: function() { this.emitSet(this.set); }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
td.radio {
|
||||
width: 45px;
|
||||
}
|
||||
td label{
|
||||
display: inline;
|
||||
font-size: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template lang='pug'>
|
||||
div
|
||||
.mx1
|
||||
label(for='exchange').wrapper Exchange:
|
||||
.custom-select.button
|
||||
select(v-model='exchange')
|
||||
option(v-for='(market, e) in exchanges') {{ e }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import _ from 'lodash'
|
||||
import rangePicker from './rangepicker.vue'
|
||||
import rangeCreator from './rangecreator.vue'
|
||||
import { get } from '../../../tools/ajax'
|
||||
|
||||
export default {
|
||||
props: ['onlyTradable', 'onlyImportable'],
|
||||
data: () => {
|
||||
return {
|
||||
exchange: 'poloniex',
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
this.emitExchange();
|
||||
},
|
||||
computed: {
|
||||
exchanges: function() {
|
||||
|
||||
let exchanges = Object.assign({}, this.$store.state.exchanges);
|
||||
|
||||
if(_.isEmpty(exchanges))
|
||||
return false;
|
||||
|
||||
if(this.onlyTradable) {
|
||||
_.each(exchanges, (e, name) => {
|
||||
if(!e.tradable)
|
||||
delete exchanges[name];
|
||||
});
|
||||
}
|
||||
|
||||
if(this.onlyImportable) {
|
||||
_.each(exchanges, (e, name) => {
|
||||
if(!e.importable)
|
||||
delete exchanges[name];
|
||||
});
|
||||
}
|
||||
|
||||
return exchanges;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
exchanges: function() { this.emitExchange() },
|
||||
exchange: function() { this.emitExchange() }
|
||||
},
|
||||
|
||||
methods: {
|
||||
emitExchange: function() {
|
||||
this.$emit('exchange', this.exchange);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template lang='pug'>
|
||||
div
|
||||
.mx1
|
||||
label(for='exchange').wrapper Exchange:
|
||||
.custom-select.button
|
||||
select(v-model='exchange')
|
||||
option(v-for='(market, e) in exchanges') {{ e }}
|
||||
.grd-row
|
||||
.grd-row-col-3-6.mx1
|
||||
label(for='currency') Currency:
|
||||
.custom-select.button
|
||||
select(v-model='currency')
|
||||
option(v-for='cur in currencies') {{ cur }}
|
||||
.grd-row-col-3-6.mx1
|
||||
label(for='asset') Asset:
|
||||
.custom-select.button
|
||||
select(v-model='asset')
|
||||
option(v-for='asst in assets') {{ asst }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import _ from 'lodash'
|
||||
import rangePicker from './rangepicker.vue'
|
||||
import rangeCreator from './rangecreator.vue'
|
||||
import { get } from '../../../tools/ajax'
|
||||
|
||||
export default {
|
||||
props: ['onlyTradable', 'onlyImportable'],
|
||||
data: () => {
|
||||
return {
|
||||
// defaults:
|
||||
exchange: 'poloniex',
|
||||
currency: 'USDT',
|
||||
asset: 'BTC',
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
this.emitConfig();
|
||||
},
|
||||
computed: {
|
||||
exchanges: function() {
|
||||
|
||||
let exchanges = Object.assign({}, this.$store.state.exchanges);
|
||||
|
||||
if(_.isEmpty(exchanges))
|
||||
return false;
|
||||
|
||||
if(this.onlyTradable) {
|
||||
_.each(exchanges, (e, name) => {
|
||||
if(!e.tradable)
|
||||
delete exchanges[name];
|
||||
});
|
||||
}
|
||||
|
||||
if(this.onlyImportable) {
|
||||
_.each(exchanges, (e, name) => {
|
||||
if(!e.importable)
|
||||
delete exchanges[name];
|
||||
});
|
||||
}
|
||||
|
||||
return exchanges;
|
||||
},
|
||||
markets: function() {
|
||||
return this.exchanges ? this.exchanges[ this.exchange ] : null;
|
||||
},
|
||||
|
||||
assets: function() {
|
||||
return this.exchanges ? this.exchanges[this.exchange].markets[this.currency] : null;
|
||||
},
|
||||
|
||||
currencies: function() {
|
||||
return this.exchanges ? _.keys( this.exchanges[this.exchange].markets ) : null;
|
||||
},
|
||||
watchConfig: function() {
|
||||
return {
|
||||
watch: {
|
||||
exchange: this.exchange,
|
||||
currency: this.currency,
|
||||
asset: this.asset,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
currency: function() { this.emitConfig() },
|
||||
asset: function() { this.emitConfig() },
|
||||
market: function() { this.emitConfig() },
|
||||
exchanges: function() { this.emitConfig() },
|
||||
exchange: function() { this.emitConfig() }
|
||||
},
|
||||
|
||||
methods: {
|
||||
emitConfig: function() {
|
||||
this.$emit('market', this.watchConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template lang='pug'>
|
||||
.grd
|
||||
.px1
|
||||
h3 Paper trader
|
||||
a.btn--primary(href='#', v-on:click.prevent='switchToggle', v-if='toggle === "closed"') Change paper trader settings
|
||||
template(v-if='toggle === "open"')
|
||||
p Settings:
|
||||
textarea.params(v-model='rawPaperTraderParams')
|
||||
p.bg--red.p1(v-if='rawPaperTraderParamsError') {{ rawPaperTraderParamsError.message }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import _ from 'lodash'
|
||||
import { get } from '../../../tools/ajax'
|
||||
|
||||
export default {
|
||||
created: function() {
|
||||
get('configPart/paperTrader', (error, response) => {
|
||||
this.rawPaperTraderParams = response.part;
|
||||
});
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
rawPaperTraderParams: '',
|
||||
rawPaperTraderParamsError: false,
|
||||
paperTraderParams: {},
|
||||
toggle: 'closed'
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
rawPaperTraderParams: function() { this.emitConfig() }
|
||||
},
|
||||
methods: {
|
||||
switchToggle: function() {
|
||||
if(this.toggle === 'open')
|
||||
this.toggle = 'closed';
|
||||
else
|
||||
this.toggle = 'open';
|
||||
},
|
||||
emitConfig: function() {
|
||||
this.parseParams();
|
||||
this.$emit('settings', this.paperTraderParams);
|
||||
},
|
||||
parseParams: function() {
|
||||
try {
|
||||
this.paperTraderParams = toml.parse(this.rawPaperTraderParams);
|
||||
this.paperTraderParams.reportRoundtrips = true;
|
||||
this.rawPaperTraderParamsError = false;
|
||||
} catch(e) {
|
||||
this.rawPaperTraderParamsError = e;
|
||||
this.paperTraderParams = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.align .custom-select select {
|
||||
padding: 0.4em 1.2em .3em .8em;
|
||||
}
|
||||
|
||||
.label-like {
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.align {
|
||||
padding-left: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template lang='pug'>
|
||||
div
|
||||
h3 Daterange
|
||||
div
|
||||
label(for='from') From
|
||||
input(v-model='from')
|
||||
div
|
||||
label(for='to') To
|
||||
input(v-model='to')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import _ from 'lodash'
|
||||
import { post } from '../../../tools/ajax'
|
||||
// global moment
|
||||
|
||||
export default {
|
||||
data: function() {
|
||||
return {
|
||||
from: '',
|
||||
to: ''
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
let now = moment().startOf('minute');
|
||||
let then = now.clone().subtract(3, 'months');
|
||||
|
||||
this.to = this.fmt(now);
|
||||
this.from = this.fmt(then);
|
||||
this.emitRange();
|
||||
},
|
||||
methods: {
|
||||
fmtTs: (mom) => moment.unix(mom).utc(),
|
||||
fmt: (mom) => mom.utc().format('YYYY-MM-DD HH:mm'),
|
||||
emitRange: function() {
|
||||
this.$emit('range', {
|
||||
from: this.fmtTs(this.from),
|
||||
to: this.fmtTs(this.to)
|
||||
});
|
||||
},
|
||||
emitManualEntry: function() {
|
||||
if(this.from.length < '4' || this.from.length < '4')
|
||||
return this.$emit('range', {})
|
||||
|
||||
let from = moment.utc(this.from);
|
||||
let to = moment.utc(this.to);
|
||||
|
||||
if(from.isValid() && to.isValid()) {
|
||||
this.$emit('range', {
|
||||
from: this.fmt(from),
|
||||
to: this.fmt(to)
|
||||
})
|
||||
} else {
|
||||
this.$emit('range', {})
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
from: function() {
|
||||
this.emitManualEntry();
|
||||
},
|
||||
to: function() {
|
||||
this.emitManualEntry();
|
||||
},
|
||||
config: function() {
|
||||
this.scanned = false;
|
||||
},
|
||||
tab: function() {
|
||||
this.scanned = false;
|
||||
this.$emit('range', {})
|
||||
},
|
||||
selectedRangeIndex: function() {
|
||||
let selectedRange = this.ranges[this.selectedRangeIndex];
|
||||
if(selectedRange)
|
||||
this.emitRange(selectedRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.scan-btn {
|
||||
margin-top: 80px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.radio label {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<template lang='pug'>
|
||||
div
|
||||
h3 Daterange
|
||||
template(v-if='tab === "scan"')
|
||||
.txt--center(v-if='!scanned')
|
||||
a.w100--s.btn--primary.scan-btn(href='#', v-on:click.prevent='scan') Scan available data
|
||||
.txt--center(v-if='scanned == "fetching"')
|
||||
p.scan-btn Scanning..
|
||||
template(v-if='scanned == true')
|
||||
template(v-if='ranges.length === 0')
|
||||
p
|
||||
strong Unable to find any local data, do you have local data available for
|
||||
| "{{ config.watch.exchange }}:{{ config.watch.currency }}/{{ config.watch.asset }}"?
|
||||
template(v-else)
|
||||
label(for='exchange').wrapper Run simulation over:
|
||||
form.radio.grd
|
||||
div.grd-row(v-for='(range, i) in ranges').m1
|
||||
input.grd-row-col-1-6(type='radio', :value='i', v-model='selectedRangeIndex')
|
||||
label.grd-row-col-5-6(:for='i') {{ printRange(range) }}
|
||||
p
|
||||
em
|
||||
a(href='#', v-on:click.prevent='scan') rescan
|
||||
p.txt--center
|
||||
em
|
||||
a(href='#', v-on:click.prevent='tab = "manual"') Or manually set a daterange
|
||||
template(v-if='tab === "manual"')
|
||||
div
|
||||
label(for='from') From:
|
||||
input(v-model='from')
|
||||
div
|
||||
label(for='to') To:
|
||||
input(v-model='to')
|
||||
p.txt--center
|
||||
em
|
||||
a(href='#', v-on:click.prevent='tab = "scan"') Or scan for a daterange
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import _ from 'lodash'
|
||||
import { post } from '../../../tools/ajax'
|
||||
// global moment
|
||||
|
||||
export default {
|
||||
props: ['config'],
|
||||
data: () => {
|
||||
return {
|
||||
scanned: false, // 'fetching', true
|
||||
ranges: [],
|
||||
selectedRangeIndex: -1,
|
||||
tab: 'scan',
|
||||
|
||||
from: '',
|
||||
to: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scan: function() {
|
||||
this.scanned = 'fetching';
|
||||
this.selectedRangeIndex = -1;
|
||||
|
||||
post('scan', this.config, (err, response) => {
|
||||
this.scanned = true;
|
||||
this.ranges = response;
|
||||
this.selectedRangeIndex = 0;
|
||||
});
|
||||
},
|
||||
printRange: function(range) {
|
||||
let fmt = mom => mom.format('YYYY-MM-DD HH:mm')
|
||||
let from = moment.unix(range.from);
|
||||
let to = moment.unix(range.to);
|
||||
let diff = moment.duration(to.diff(from)).humanize();
|
||||
return `${fmt(from)} to ${fmt(to)} (${diff})`;
|
||||
},
|
||||
fmtTs: (mom) => moment.unix(mom).utc(),
|
||||
fmt: (mom) => mom.utc().format(),
|
||||
emitRange: function(range) {
|
||||
this.$emit('range', {
|
||||
from: this.fmtTs(range.from),
|
||||
to: this.fmtTs(range.to)
|
||||
});
|
||||
},
|
||||
emitManualEntry: function() {
|
||||
if(this.from.length < '4' || this.from.length < '4')
|
||||
// this cannot possibly be a valid date
|
||||
return this.$emit('range', {})
|
||||
|
||||
let from = moment.utc(this.from);
|
||||
let to = moment.utc(this.to);
|
||||
|
||||
if(from.isValid() && to.isValid()) {
|
||||
this.$emit('range', {
|
||||
from: this.fmt(from),
|
||||
to: this.fmt(to)
|
||||
})
|
||||
} else {
|
||||
this.$emit('range', {});
|
||||
}
|
||||
},
|
||||
reset: function() {
|
||||
this.scanned = false;
|
||||
this.$emit('range', {})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
from: function() {
|
||||
this.emitManualEntry();
|
||||
},
|
||||
to: function() {
|
||||
this.emitManualEntry();
|
||||
},
|
||||
config: function() {
|
||||
this.reset();
|
||||
},
|
||||
tab: function() {
|
||||
this.reset();
|
||||
},
|
||||
selectedRangeIndex: function() {
|
||||
let selectedRange = this.ranges[this.selectedRangeIndex];
|
||||
if(selectedRange)
|
||||
this.emitRange(selectedRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.scan-btn {
|
||||
margin-top: 80px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.radio label {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,145 @@
|
||||
<template lang='pug'>
|
||||
.grd
|
||||
.grd-row
|
||||
.grd-row-col-3-6.px1
|
||||
h3 Strategy
|
||||
div
|
||||
label(for='strat').wrapper Strategy:
|
||||
.custom-select.button
|
||||
select(v-model='strategy')
|
||||
option(v-for='strat in strategies') {{ strat.name }}
|
||||
div
|
||||
label(for='candleSize') Candle Size
|
||||
.grd-row
|
||||
.grd-row-col-3-6
|
||||
input(v-model='rawCandleSize')
|
||||
.grd-row-col-3-6.align
|
||||
.custom-select.button
|
||||
select(v-model='candleSizeUnit')
|
||||
option minutes
|
||||
option hours
|
||||
option days
|
||||
div
|
||||
label(for='historySize') Warmup period (in {{ rawCandleSize }} {{ singularCandleSizeUnit }} candles):
|
||||
input(v-model='historySize')
|
||||
em.label-like (will use {{ humanizeDuration(candleSize * historySize * 1000 * 60) }} of data as history)
|
||||
.grd-row-col-3-6.px1
|
||||
div
|
||||
h3 Parameters
|
||||
p {{ strategy }} Parameters:
|
||||
textarea.params(v-model='rawStratParams')
|
||||
p.bg--red.p1(v-if='rawStratParamsError') {{ rawStratParamsError.message }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import _ from 'lodash'
|
||||
import { get } from '../../../tools/ajax'
|
||||
|
||||
export default {
|
||||
data: () => {
|
||||
return {
|
||||
strategies: [],
|
||||
|
||||
candleSizeUnit: 'hours',
|
||||
rawCandleSize: 1,
|
||||
|
||||
strategy: 'MACD',
|
||||
historySize: 10,
|
||||
|
||||
rawStratParams: '',
|
||||
rawStratParamsError: false,
|
||||
|
||||
emptyStrat: false,
|
||||
stratParams: {}
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
get('strategies', (err, data) => {
|
||||
this.strategies = data;
|
||||
|
||||
_.each(this.strategies, function(s) {
|
||||
s.empty = s.params === '';
|
||||
});
|
||||
|
||||
this.rawStratParams = _.find(this.strategies, { name: this.strategy }).params;
|
||||
this.emptyStrat = _.find(this.strategies, { name: this.strategy }).empty;
|
||||
this.emitConfig();
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
strategy: function(strat) {
|
||||
strat = _.find(this.strategies, { name: strat });
|
||||
this.rawStratParams = strat.params;
|
||||
this.emptyStrat = strat.empty;
|
||||
|
||||
this.emitConfig();
|
||||
},
|
||||
candleSize: function() { this.emitConfig() },
|
||||
historySize: function() { this.emitConfig() },
|
||||
rawStratParams: function() { this.emitConfig() }
|
||||
},
|
||||
computed: {
|
||||
candleSize: function() {
|
||||
if(this.candleSizeUnit === 'minutes')
|
||||
return this.rawCandleSize;
|
||||
else if(this.candleSizeUnit === 'hours')
|
||||
return this.rawCandleSize * 60;
|
||||
else if(this.candleSizeUnit === 'days')
|
||||
return this.rawCandleSize * 60 * 24;
|
||||
},
|
||||
singularCandleSizeUnit: function() {
|
||||
// hours -> hour
|
||||
return this.candleSizeUnit.slice(0, -1);
|
||||
},
|
||||
config: function() {
|
||||
let config = {
|
||||
tradingAdvisor: {
|
||||
enabled: true,
|
||||
method: this.strategy,
|
||||
candleSize: +this.candleSize,
|
||||
historySize: +this.historySize
|
||||
}
|
||||
}
|
||||
|
||||
if(this.emptyStrat)
|
||||
config[this.strategy] = {__empty: true}
|
||||
else
|
||||
config[this.strategy] = this.stratParams;
|
||||
|
||||
return config;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
humanizeDuration: (n) => window.humanizeDuration(n),
|
||||
emitConfig: function() {
|
||||
this.parseParams();
|
||||
this.$emit('stratConfig', this.config);
|
||||
},
|
||||
parseParams: function() {
|
||||
try {
|
||||
this.stratParams = toml.parse(this.rawStratParams);
|
||||
this.rawStratParamsError = false;
|
||||
} catch(e) {
|
||||
this.rawStratParamsError = e;
|
||||
this.stratParams = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.align .custom-select select {
|
||||
padding: 0.4em 1.2em .3em .8em;
|
||||
}
|
||||
|
||||
.label-like {
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.align {
|
||||
padding-left: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template lang='pug'>
|
||||
div
|
||||
h3 Type
|
||||
template
|
||||
label(for='type').wrapper What do you want to do with gekko?
|
||||
form.radio.grd
|
||||
div.grd-row(v-for='(type, i) in types').m1
|
||||
input.grd-row-col-1-6(type='radio', :value='i', v-model='selectedTypeIndex')
|
||||
label.grd-row-col-5-6(:for='i') {{ type }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
created: function() {
|
||||
this.emitType();
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
types: ['paper trader', 'market watcher', 'tradebot'],
|
||||
selectedTypeIndex: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emitType: function() {
|
||||
this.$emit('type', this.type);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
type: function() {
|
||||
this.emitType();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
type: function() {
|
||||
return this.types[ this.selectedTypeIndex ];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.radio label {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
import { post } from '../../../tools/ajax'
|
||||
|
||||
var mixin = {
|
||||
data: () => {
|
||||
return {
|
||||
datasets: [],
|
||||
datasetScanstate: 'idle',
|
||||
unscannableMakets: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scan: function() {
|
||||
this.datasetScanstate = 'scanning';
|
||||
|
||||
post('scansets', {}, (error, response) => {
|
||||
this.datasetScanstate = 'scanned';
|
||||
|
||||
this.unscannableMakets = response.errors;
|
||||
|
||||
let sets = [];
|
||||
|
||||
response.datasets.forEach(market => {
|
||||
market.ranges.forEach((range, i) => {
|
||||
sets.push({
|
||||
exchange: market.exchange,
|
||||
currency: market.currency,
|
||||
asset: market.asset,
|
||||
from: moment.unix(range.from).utc(),
|
||||
to: moment.unix(range.to).utc(),
|
||||
id: market.exchange + market.asset + market.currency + i
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// for now, filter out sets smaller than 3 hours..
|
||||
sets = sets.filter(set => {
|
||||
if(set.to.diff(set.from, 'hours') > 2)
|
||||
return true;
|
||||
});
|
||||
|
||||
sets = sets.sort((a, b) => {
|
||||
let adiff = a.to.diff(a.from);
|
||||
let bdiff = b.to.diff(b.from);
|
||||
|
||||
if(adiff < bdiff)
|
||||
return -1;
|
||||
|
||||
if(adiff > bdiff)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}).reverse();
|
||||
|
||||
this.datasets = sets;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default mixin;
|
||||
@@ -0,0 +1,64 @@
|
||||
<template lang='pug'>
|
||||
.grd-row-col-3-6
|
||||
table.p1
|
||||
tr
|
||||
th amount of trades
|
||||
td {{ report.trades }}
|
||||
tr
|
||||
th sharpe ratio
|
||||
td {{ round2(report.sharpe) }}
|
||||
tr
|
||||
th start balance
|
||||
td {{ round(report.startBalance) }} {{ report.currency }}
|
||||
tr
|
||||
th final balance
|
||||
td {{ round(report.balance) }} {{ report.currency }}
|
||||
tr
|
||||
th simulated profit
|
||||
|
||||
.big.txt--right.price(:class='profitClass') {{ round(report.relativeProfit) }}%
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['report'],
|
||||
methods: {
|
||||
round2: n => (+n).toFixed(2),
|
||||
round: n => (+n).toFixed(5)
|
||||
},
|
||||
computed: {
|
||||
profitClass: function() {
|
||||
if(this.report.relativeProfit > 0)
|
||||
return 'profit'
|
||||
else
|
||||
return 'loss'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.summary td {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.big {
|
||||
font-size: 1.3em;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.summary table {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.price.profit {
|
||||
color: #7FFF00;
|
||||
}
|
||||
|
||||
.price.loss {
|
||||
color: red;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
// http://stackoverflow.com/questions/7190898/progress-bar-with-html-and-css
|
||||
|
||||
<template lang='pug'>
|
||||
.progressbarWrapper(v-if='progress')
|
||||
p <strong>{{ round(progress) }}%</strong>
|
||||
.progressbar
|
||||
div(:style='{width: progress + "%"}')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['progress'],
|
||||
methods: {
|
||||
round: n => (+n).toFixed(2)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.progressbarWrapper p {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.progressbar {
|
||||
background-color: rgba(216,216,216,.99);
|
||||
border-radius: 13px; /* (height of inner div) / 2 + padding */
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
|
||||
@keyframes shimmer{
|
||||
0%{
|
||||
background-position: 0px 0
|
||||
}
|
||||
100%{
|
||||
background-position: 960px 0
|
||||
}
|
||||
}
|
||||
|
||||
.progressbar > div {
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
/* Progress bar animation */
|
||||
background-color: #FFA500;
|
||||
animation-duration: 1500ms;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: shimmer;
|
||||
animation-timing-function: linear;
|
||||
background: #f6f7f8;
|
||||
background: linear-gradient(to right, #FFA500 10%, #ffce77 25%, #FFA500 40%);
|
||||
background-size: 960px 50px;
|
||||
background-position: left;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
import _ from 'lodash'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { wsPath } from '../../tools/api'
|
||||
import initializeState from '../../store/init'
|
||||
|
||||
var socket = null;
|
||||
|
||||
export const bus = new Vue();
|
||||
|
||||
bus.$on('gekko_update', data => console.log(data))
|
||||
|
||||
bus.$on('import_update', data => console.log(data))
|
||||
bus.$on('import_error', data => {
|
||||
alert('IMPORT ERROR: ' + data.error);
|
||||
});
|
||||
|
||||
const info = {
|
||||
connected: false
|
||||
}
|
||||
|
||||
export const connect = () => {
|
||||
socket = new ReconnectingWebSocket(wsPath, null, { maxReconnectInterval: 4000 });
|
||||
|
||||
setTimeout(() => {
|
||||
// in case we cannot connect
|
||||
if(!info.connected) {
|
||||
initializeState();
|
||||
bus.$emit('WS_STATUS_CHANGE', info);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
socket.onopen = () => {
|
||||
if(info.connected)
|
||||
return;
|
||||
|
||||
info.connected = true;
|
||||
bus.$emit('WS_STATUS_CHANGE', info);
|
||||
initializeState();
|
||||
}
|
||||
socket.onclose = () => {
|
||||
if(!info.connected)
|
||||
return;
|
||||
|
||||
info.connected = false;
|
||||
bus.$emit('WS_STATUS_CHANGE', info);
|
||||
}
|
||||
socket.onerror = () => {
|
||||
if(!info.connected)
|
||||
return;
|
||||
|
||||
info.connected = false;
|
||||
bus.$emit('WS_STATUS_CHANGE', info);
|
||||
}
|
||||
socket.onmessage = function(message) {
|
||||
const payload = JSON.parse(message.data);
|
||||
// console.log('ws message:', payload);
|
||||
bus.$emit(payload.type, payload);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<template lang='pug'>
|
||||
footer.p2.bg--off-white
|
||||
.contain
|
||||
p
|
||||
em Use Gekko at your own risk.
|
||||
p Using Gekko v{{ version.gekko }} and Gekko UI v{{ version.ui }}.
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const gekkoPackage = require('../../../../../package.json');
|
||||
const uiPackage = require('../../../package.json');
|
||||
|
||||
export default {
|
||||
data: () => {
|
||||
return {
|
||||
version: {
|
||||
gekko: gekkoPackage.version,
|
||||
ui: uiPackage.version
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template lang='pug'>
|
||||
div
|
||||
#top
|
||||
header.bg--off-white.grd
|
||||
.contain.grd-row
|
||||
h3.py1.px2.col-2 Gekko UI
|
||||
nav.bg--light-gray
|
||||
.menu.contain
|
||||
router-link(to='/home').py1 Home
|
||||
router-link(to='/live-gekkos').py1 Live Gekkos
|
||||
router-link(to='/backtest').py1 Backtest
|
||||
router-link(to='/data').py1 Local data
|
||||
router-link(to='/config').py1 Config
|
||||
a(href='https://gekko.wizb.it/docs/introduction/about_gekko.html', target='_blank').py1 Documentation
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.menu a {
|
||||
flex: 1 1 100%;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.menu .router-link-active {
|
||||
background-color: rgba(250,250,250,.99);
|
||||
}
|
||||
|
||||
.menu a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template lang='pug'>
|
||||
section.contain.grd-row
|
||||
.grd-row-col-3-6(v-html='left')
|
||||
.grd-row-col-3-6.txt--center
|
||||
img(src='static/gekko.jpg')
|
||||
p
|
||||
em The most valuable commodity I know of is information.
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import marked from '../../tools/marked';
|
||||
|
||||
const left = marked(`
|
||||
|
||||
## Gekko
|
||||
|
||||
Gekko is a Bitcoin trading bot and backtesting platform that
|
||||
connects to popular Bitcoin exchanges. It is written in javascript
|
||||
and runs on nodejs.
|
||||
|
||||
[Find out more](https://gekko.wizb.it/).
|
||||
|
||||
*Gekko is 100% free (open source), if you paid for this you have been scammed.*
|
||||
|
||||
`);
|
||||
|
||||
export default {
|
||||
data: () => {
|
||||
return {
|
||||
left
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template lang='pug'>
|
||||
div(v-if='active')
|
||||
#modal-background
|
||||
#modal.modal
|
||||
.modal-guts(v-html='content')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import marked from '../../tools/marked';
|
||||
|
||||
const messages = {
|
||||
disconnected: marked(`
|
||||
|
||||
## Disconnected
|
||||
|
||||
Something happened to either Gekko or the connection.
|
||||
Please check the terminal where Gekko is running or
|
||||
your network connection.
|
||||
|
||||
*This message is shown when the UI is unable to open a websocket connection with the Gekko Server.*
|
||||
|
||||
`)
|
||||
}
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
active: function() {
|
||||
return !this.$store.state.warnings.connected;
|
||||
},
|
||||
content: function() {
|
||||
if(!this.$store.state.warnings.connected)
|
||||
return messages.disconnected;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#modal-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
background-color: black;
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 600px;
|
||||
min-height: 300px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.modal-guts {
|
||||
|
||||
/* other stuff we already covered */
|
||||
|
||||
/* cover the modal */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
/* spacing as needed */
|
||||
padding: 20px 50px 20px 20px;
|
||||
|
||||
/* let it scroll */
|
||||
overflow: auto;
|
||||
|
||||
}
|
||||
</style>
|
||||
Vendored
+149
@@ -0,0 +1,149 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
// techanjs based cancle chart, unused at the moment
|
||||
|
||||
export default function(_data, _trades) {
|
||||
let MAX_WIDTH = window.innerWidth - 20;
|
||||
|
||||
var margin = {top: 20, right: 20, bottom: 30, left: 50},
|
||||
width = MAX_WIDTH - margin.left - margin.right,
|
||||
height = 500 - margin.top - margin.bottom;
|
||||
|
||||
var x = techan.scale.financetime()
|
||||
.range([0, width]);
|
||||
|
||||
var y = d3.scaleLinear()
|
||||
.range([height, 0]);
|
||||
|
||||
var zoom = d3.zoom()
|
||||
.on("zoom", zoomed);
|
||||
|
||||
var zoomableInit;
|
||||
|
||||
var candlestick = techan.plot.candlestick()
|
||||
.xScale(x)
|
||||
.yScale(y);
|
||||
|
||||
var tradearrow = techan.plot.tradearrow()
|
||||
.xScale(x)
|
||||
.yScale(y)
|
||||
.orient(function(d) { return d.type.startsWith("buy") ? "up" : "down"; })
|
||||
.on("mouseenter", enter)
|
||||
.on("mouseout", out);
|
||||
|
||||
var xAxis = d3.axisBottom(x);
|
||||
|
||||
var yAxis = d3.axisLeft(y);
|
||||
|
||||
var svg = d3.select("#chart").append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
svg.append("clipPath")
|
||||
.attr("id", "clip")
|
||||
.append("rect")
|
||||
.attr("x", 0)
|
||||
.attr("y", y(1))
|
||||
.attr("width", width)
|
||||
.attr("height", y(0) - y(1));
|
||||
|
||||
svg.append("g")
|
||||
.attr("class", "candlestick")
|
||||
.attr("clip-path", "url(#clip)");
|
||||
|
||||
svg.append("g")
|
||||
.attr("class", "tradearrow")
|
||||
.attr("clip-path", "url(#clip)");
|
||||
|
||||
svg.append("g")
|
||||
.attr("class", "x axis")
|
||||
.attr("transform", "translate(0," + height + ")");
|
||||
|
||||
svg.append("g")
|
||||
.attr("class", "y axis")
|
||||
.append("text")
|
||||
.attr("transform", "rotate(-90)")
|
||||
.attr("y", 6)
|
||||
.attr("dy", ".71em")
|
||||
.style("text-anchor", "end")
|
||||
.text("Price ($)");
|
||||
|
||||
svg.append("rect")
|
||||
.attr("class", "pane")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.call(zoom);
|
||||
|
||||
|
||||
var accessor = candlestick.accessor();
|
||||
|
||||
const data = _data.map(function(d) {
|
||||
return {
|
||||
date: new Date(d.start),
|
||||
open: +d.open,
|
||||
high: +d.high,
|
||||
low: +d.low,
|
||||
close: +d.close,
|
||||
volume: +d.volume
|
||||
};
|
||||
}).sort(function(a, b) { return d3.ascending(accessor.d(a), accessor.d(b)); });
|
||||
|
||||
const trades = _trades.map(function(t) {
|
||||
let trade = _.pick(t, ['price']);
|
||||
trade.quantity = 1;
|
||||
trade.type = t.action;
|
||||
trade.date = new Date(t.date);
|
||||
return trade;
|
||||
});
|
||||
|
||||
console.log(trades);
|
||||
|
||||
x.domain(data.map(accessor.d));
|
||||
y.domain(techan.scale.plot.ohlc(data, accessor).domain());
|
||||
|
||||
svg.select("g.tradearrow").datum(trades);
|
||||
svg.select("g.candlestick").datum(data);
|
||||
draw();
|
||||
|
||||
// Associate the zoom with the scale after a domain has been applied
|
||||
// Stash initial settings to store as baseline for zooming
|
||||
zoomableInit = x.zoomable().clamp(false).copy();
|
||||
|
||||
function zoomed() {
|
||||
var rescaledY = d3.event.transform.rescaleY(y);
|
||||
yAxis.scale(rescaledY);
|
||||
candlestick.yScale(rescaledY);
|
||||
tradearrow.yScale(rescaledY);
|
||||
|
||||
// Emulates D3 behaviour, required for financetime due to secondary zoomable scale
|
||||
x.zoomable().domain(d3.event.transform.rescaleX(zoomableInit).domain());
|
||||
|
||||
draw();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
svg.select("g.tradearrow").call(tradearrow);
|
||||
svg.select("g.candlestick").call(candlestick);
|
||||
// using refresh method is more efficient as it does not perform any data joins
|
||||
// Use this if underlying data is not changing
|
||||
// svg.select("g.candlestick").call(candlestick.refresh);
|
||||
svg.select("g.x.axis").call(xAxis);
|
||||
svg.select("g.y.axis").call(yAxis)
|
||||
}
|
||||
|
||||
function enter(d) {
|
||||
valueText.style("display", "inline");
|
||||
refreshText(d);
|
||||
}
|
||||
|
||||
function out() {
|
||||
valueText.style("display", "none");
|
||||
}
|
||||
|
||||
function refreshText(d) {
|
||||
valueText.text("Trade: " + dateFormat(d.date) + ", " + d.type + ", " + valueFormat(d.price));
|
||||
}
|
||||
|
||||
};
|
||||
Vendored
+204
@@ -0,0 +1,204 @@
|
||||
import _ from 'lodash';
|
||||
// global moment
|
||||
|
||||
export default function(_data, _trades, _height) {
|
||||
|
||||
const toDate = i => {
|
||||
if(_.isNumber(i)) {
|
||||
return moment.unix(i).utc().toDate();
|
||||
} else {
|
||||
return moment.utc(i).toDate();
|
||||
}
|
||||
}
|
||||
|
||||
const trades = _trades.map(t => {
|
||||
return {
|
||||
price: t.price,
|
||||
date: toDate(t.date),
|
||||
action: t.action
|
||||
}
|
||||
});
|
||||
|
||||
const data = _data.map(c => {
|
||||
return {
|
||||
price: c.open,
|
||||
date: toDate(c.start)
|
||||
}
|
||||
});
|
||||
|
||||
var dates = data.map(c => +c.date);
|
||||
var prices = data.map(c => +c.price)
|
||||
|
||||
var svg = d3.select("#chart");
|
||||
|
||||
svg.attr("width", window.innerWidth - 20);
|
||||
|
||||
var margin = {top: 20, right: 20, bottom: 110, left: 40};
|
||||
var height = _height - margin.top - margin.bottom;
|
||||
var margin2 = {top: _height - 70, right: 20, bottom: 30, left: 40};
|
||||
var width = +svg.attr("width") - margin.left - margin.right;
|
||||
var height2 = _height - margin2.top - margin2.bottom;
|
||||
|
||||
var x = d3.scaleUtc().range([0, width]),
|
||||
x2 = d3.scaleUtc().range([0, width]),
|
||||
y = d3.scaleLinear().range([height, 0]),
|
||||
y2 = d3.scaleLinear().range([height2, 0]);
|
||||
|
||||
var xAxis = d3.axisBottom(x),
|
||||
xAxis2 = d3.axisBottom(x2),
|
||||
yAxis = d3.axisLeft(y).ticks(_height / 50);
|
||||
|
||||
var brush = d3.brushX()
|
||||
.extent([[0, 0], [width, height2]])
|
||||
.on("brush end", brushed);
|
||||
|
||||
var zoom = d3.zoom()
|
||||
.scaleExtent([1, 100])
|
||||
.translateExtent([[0, 0], [width, height]])
|
||||
.extent([[0, 0], [width, height]])
|
||||
.on("zoom", zoomed);
|
||||
|
||||
var line = d3.line()
|
||||
.x(function(d) { return x(d.date); })
|
||||
.y(function(d) { return y(d.price); });
|
||||
|
||||
var line2 = d3.line()
|
||||
.x(function(d) { return x2(d.date); })
|
||||
.y(function(d) { return y2(d.price); });
|
||||
|
||||
svg.append("defs").append("clipPath")
|
||||
.attr("id", "clip")
|
||||
.append("rect")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
var focus = svg.append("g")
|
||||
.attr("class", "focus")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
var context = svg.append("g")
|
||||
.attr("class", "context")
|
||||
.attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
|
||||
|
||||
x.domain(d3.extent(data, function(d) { return d.date; }));
|
||||
y.domain([
|
||||
d3.min(prices) * 0.99,
|
||||
d3.max(prices) * 1.01
|
||||
]);
|
||||
x2.domain(x.domain());
|
||||
y2.domain(y.domain());
|
||||
|
||||
focus.append("path")
|
||||
.datum(data)
|
||||
.attr("class", "line price")
|
||||
.attr("d", line);
|
||||
|
||||
focus.append("g")
|
||||
.attr("class", "axis axis--x")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(xAxis);
|
||||
|
||||
focus.append("g")
|
||||
.attr("class", "axis axis--y")
|
||||
.call(yAxis);
|
||||
|
||||
context.append("path")
|
||||
.datum(data)
|
||||
.attr("class", "line")
|
||||
.attr("d", line2);
|
||||
|
||||
context.append("g")
|
||||
.attr("class", "axis axis--x")
|
||||
.attr("transform", "translate(0," + height2 + ")")
|
||||
.call(xAxis2);
|
||||
|
||||
var circles = svg
|
||||
.append('g')
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
|
||||
.selectAll("circle")
|
||||
.data(trades)
|
||||
.enter().append("circle")
|
||||
.attr('class', function(d) { return d.action })
|
||||
.attr("cx", function(d) { return x(d.date); })
|
||||
.attr("cy", function(d) { return y(d.price); })
|
||||
.attr('r', 5);
|
||||
|
||||
var brushCircles = context
|
||||
.append('g')
|
||||
// .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
|
||||
.selectAll("circle")
|
||||
.data(trades)
|
||||
.enter().append("circle")
|
||||
.attr('class', function(d) { return d.action })
|
||||
.attr("cx", function(d) { return x2(d.date); })
|
||||
.attr("cy", function(d) { return y2(d.price); })
|
||||
.attr('r', 3);
|
||||
|
||||
|
||||
context.append("g")
|
||||
.attr("class", "brush")
|
||||
.call(brush)
|
||||
.call(brush.move, x.range());
|
||||
|
||||
svg.append("rect")
|
||||
.attr("class", "zoom")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
|
||||
.call(zoom);
|
||||
|
||||
function brushed() {
|
||||
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
|
||||
var s = d3.event.selection || x2.range();
|
||||
x.domain(s.map(x2.invert, x2));
|
||||
|
||||
scaleY(x.domain());
|
||||
|
||||
svg.select(".axis--y")
|
||||
.call(yAxis);
|
||||
|
||||
circles
|
||||
.attr("cx", function(d) { return x(d.date); })
|
||||
.attr("cy", function(d) { return y(d.price); })
|
||||
|
||||
focus.select(".line").attr("d", line);
|
||||
focus.select(".axis--x").call(xAxis);
|
||||
svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
|
||||
.scale(width / (s[1] - s[0]))
|
||||
.translate(-s[0], 0));
|
||||
}
|
||||
|
||||
function scaleY(domain) {
|
||||
let [min, max] = domain;
|
||||
|
||||
let minIndex = _.sortedIndex(dates, min);
|
||||
let maxIndex = _.sortedIndex(dates, max);
|
||||
|
||||
let set = prices.slice(minIndex, maxIndex);
|
||||
y.domain([
|
||||
d3.min(set) * 0.9995,
|
||||
d3.max(set) * 1.0005
|
||||
]);
|
||||
}
|
||||
|
||||
function zoomed() {
|
||||
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
|
||||
var t = d3.event.transform;
|
||||
|
||||
scaleY(t.rescaleX(x2).domain());
|
||||
|
||||
svg.select(".axis--y")
|
||||
.call(yAxis);
|
||||
|
||||
x.domain(t.rescaleX(x2).domain());
|
||||
focus.select(".line").attr("d", line);
|
||||
|
||||
circles
|
||||
.attr("cx", function(d) { return x(d.date); })
|
||||
.attr("cy", function(d) { return y(d.price); })
|
||||
|
||||
|
||||
focus.select(".axis--x").call(xAxis);
|
||||
context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
|
||||
}
|
||||
}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
export const draw = function(message) {
|
||||
d3.select("#chart").append("text")
|
||||
.attr('class', 'message')
|
||||
.attr('x', 150)
|
||||
.attr('y', 150)
|
||||
.text(message);
|
||||
}
|
||||
|
||||
export const clear = function() {
|
||||
d3.select("#chart").find('text').remove();
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
import VueRouter from 'vue-router'
|
||||
Vue.use(VueRouter);
|
||||
|
||||
import store from './store'
|
||||
|
||||
import backtester from './components/backtester/backtester.vue'
|
||||
import home from './components/layout/home.vue'
|
||||
|
||||
import data from './components/data/data.vue'
|
||||
import importer from './components/data/import/importer.vue'
|
||||
import singleImport from './components/data/import/single.vue'
|
||||
import config from './components/config/config.vue'
|
||||
|
||||
import gekkoList from './components/gekko/list.vue'
|
||||
import newGekko from './components/gekko/new.vue'
|
||||
import singleGekko from './components/gekko/singleGekko.vue'
|
||||
import { connect as connectWS } from './components/global/ws'
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'hash',
|
||||
base: __dirname,
|
||||
routes: [
|
||||
{ path: '/', redirect: '/home' },
|
||||
{ path: '/home', component: home },
|
||||
{ path: '/backtest', component: backtester },
|
||||
{ path: '/config', component: config },
|
||||
{ path: '/data', component: data },
|
||||
{ path: '/data/importer', component: importer },
|
||||
{ path: '/data/importer/import/:id', component: singleImport },
|
||||
{ path: '/live-gekkos', component: gekkoList },
|
||||
{ path: '/live-gekkos/new', component: newGekko },
|
||||
{ path: '/live-gekkos/:id', component: singleGekko },
|
||||
]
|
||||
});
|
||||
|
||||
// setup some stuff
|
||||
connectWS();
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
el: '#app',
|
||||
render: h => h(App)
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import _ from 'lodash'
|
||||
|
||||
import * as importMutations from './modules/imports/mutations'
|
||||
import * as gekkoMutations from './modules/gekkos/mutations'
|
||||
import * as notificationMutations from './modules/notifications/mutations'
|
||||
import * as configMutations from './modules/config/mutations'
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production'
|
||||
|
||||
let mutations = {};
|
||||
|
||||
_.merge(mutations, importMutations);
|
||||
_.merge(mutations, gekkoMutations);
|
||||
_.merge(mutations, notificationMutations);
|
||||
_.merge(mutations, configMutations);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
warnings: {
|
||||
connected: true, // assume we will connect
|
||||
},
|
||||
imports: [],
|
||||
gekkos: {},
|
||||
archivedGekkos: {},
|
||||
connection: {
|
||||
disconnected: false,
|
||||
reconnected: false
|
||||
},
|
||||
apiKeys: [],
|
||||
exchanges: {}
|
||||
},
|
||||
mutations,
|
||||
strict: debug
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import syncImports from './modules/imports/sync'
|
||||
import syncGekkos from './modules/gekkos/sync'
|
||||
import syncNotifications from './modules/notifications/sync'
|
||||
import syncConfig from './modules/config/sync'
|
||||
|
||||
export default function() {
|
||||
syncImports();
|
||||
syncGekkos();
|
||||
syncNotifications();
|
||||
syncConfig();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
export const syncApiKeys = (state, apiKeys) => {
|
||||
Vue.set(state, 'apiKeys', apiKeys);
|
||||
return state;
|
||||
}
|
||||
|
||||
export const syncExchanges = (state, exchanges) => {
|
||||
Vue.set(state, 'exchanges', exchanges);
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { get } from '../../../tools/ajax'
|
||||
import store from '../../'
|
||||
import { bus } from '../../../components/global/ws'
|
||||
|
||||
const transformMarkets = backendData => {
|
||||
if(!backendData) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var exchangesRaw = backendData;
|
||||
var exchangesTemp = {};
|
||||
|
||||
exchangesRaw.forEach(e => {
|
||||
exchangesTemp[e.slug] = exchangesTemp[e.slug] || {markets: {}};
|
||||
|
||||
e.markets.forEach( pair => {
|
||||
let [ currency, asset ] = pair['pair'];
|
||||
exchangesTemp[e.slug].markets[currency] = exchangesTemp[e.slug].markets[currency] || [];
|
||||
exchangesTemp[e.slug].markets[currency].push( asset );
|
||||
});
|
||||
|
||||
if ("exchangeMaxHistoryAge" in e) {
|
||||
exchangesTemp[e.slug].exchangeMaxHistoryAge = e.exchangeMaxHistoryAge;
|
||||
}
|
||||
|
||||
exchangesTemp[e.slug].importable = e.providesFullHistory ? true : false;
|
||||
exchangesTemp[e.slug].tradable = e.tradable ? true : false;
|
||||
exchangesTemp[e.slug].requires = e.requires;
|
||||
});
|
||||
|
||||
return exchangesTemp;
|
||||
}
|
||||
|
||||
|
||||
const init = () => {
|
||||
get('apiKeys', (err, resp) => {
|
||||
store.commit('syncApiKeys', resp);
|
||||
});
|
||||
|
||||
get('exchanges', (err, resp) => {
|
||||
store.commit('syncExchanges', transformMarkets(resp));
|
||||
})
|
||||
}
|
||||
|
||||
const sync = () => {
|
||||
bus.$on('apiKeys', data => {
|
||||
store.commit('syncApiKeys', data.exchanges);
|
||||
});
|
||||
}
|
||||
|
||||
export default function() {
|
||||
init();
|
||||
sync();
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import Vue from 'vue'
|
||||
import _ from 'lodash';
|
||||
const reduceState = require('../../../../../state/reduceState');
|
||||
|
||||
export const syncGekkos = (state, data) => {
|
||||
if(!data) {
|
||||
return state;
|
||||
}
|
||||
|
||||
state.gekkos = data.live;
|
||||
state.archivedGekkos = data.archive;
|
||||
return state;
|
||||
}
|
||||
|
||||
export const addGekko = (state, gekko) => {
|
||||
state.gekkos = {
|
||||
...state.gekkos,
|
||||
[gekko.id]: gekko
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export const updateGekko = (state, update) => {
|
||||
if(!update.id || !_.has(state.gekkos, update.id)) {
|
||||
return console.error('cannot update unknown gekko..');;
|
||||
}
|
||||
|
||||
state.gekkos = {
|
||||
...state.gekkos,
|
||||
[update.id]: reduceState(state.gekkos[update.id], update.event)
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export const archiveGekko = (state, id) => {
|
||||
if(!_.has(state.gekkos, id)) {
|
||||
return console.error('cannot archive unknown gekko..');
|
||||
}
|
||||
|
||||
state.archivedGekkos = {
|
||||
...state.archivedGekkos,
|
||||
[id]: {
|
||||
...state.gekkos[id],
|
||||
stopped: true,
|
||||
active: false
|
||||
}
|
||||
}
|
||||
|
||||
state.gekkos = _.omit(state.gekkos, id);
|
||||
return state;
|
||||
}
|
||||
|
||||
export const errorGekko = (state, data) => {
|
||||
if(!_.has(state.gekkos, data.id)) {
|
||||
return console.error('cannot error unknown gekko..');
|
||||
}
|
||||
|
||||
state.gekkos = {
|
||||
...state.gekkos,
|
||||
[data.id]: {
|
||||
...state.gekkos[data.id],
|
||||
errored: true,
|
||||
errorMessage: data.error
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export const deleteGekko = (state, id) => {
|
||||
if(!_.has(state.archivedGekkos, id)) {
|
||||
return console.error('cannot delete unknown gekko..');
|
||||
}
|
||||
|
||||
state.archivedGekkos = _.omit(state.archivedGekkos, id);
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { get } from '../../../tools/ajax'
|
||||
import store from '../../'
|
||||
import { bus } from '../../../components/global/ws'
|
||||
import _ from 'lodash'
|
||||
|
||||
const init = () => {
|
||||
get('gekkos', (err, resp) => {
|
||||
const gekkos = resp;
|
||||
store.commit('syncGekkos', gekkos);
|
||||
});
|
||||
}
|
||||
|
||||
const sync = () => {
|
||||
bus.$on('gekko_new', data => store.commit('addGekko', data.state));
|
||||
bus.$on('gekko_event', data => store.commit('updateGekko', data));
|
||||
bus.$on('gekko_archived', data => store.commit('archiveGekko', data.id));
|
||||
bus.$on('gekko_error', data => store.commit('errorGekko', data));
|
||||
bus.$on('gekko_deleted', data => store.commit('deleteGekko', data.id));
|
||||
|
||||
// unused:
|
||||
// bus.$on('gekko_stopped', data => store.commit('x', data.id));
|
||||
// bus.$on('gekko_deleted', data => store.commit('x', data.id));
|
||||
}
|
||||
|
||||
export default function() {
|
||||
init();
|
||||
sync();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
export const addImport = (state, imp) => {
|
||||
state.imports.push(imp);
|
||||
return state;
|
||||
}
|
||||
|
||||
export const syncImports = (state, imports) => {
|
||||
state.imports = imports;
|
||||
return state;
|
||||
}
|
||||
|
||||
export const updateImport = (state, update) => {
|
||||
let index = state.imports.findIndex(i => i.id === update.import_id);
|
||||
let item = state.imports[index];
|
||||
if(!item)
|
||||
return state;
|
||||
|
||||
let updated = Vue.util.extend(item, update.updates);
|
||||
Vue.set(state.imports, index, updated);
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { get } from '../../../tools/ajax'
|
||||
import store from '../../'
|
||||
import { bus } from '../../../components/global/ws'
|
||||
|
||||
const init = () => {
|
||||
get('imports', (err, resp) => {
|
||||
store.commit('syncImports', resp);
|
||||
});
|
||||
}
|
||||
|
||||
const sync = () => {
|
||||
bus.$on('import_update', data => {
|
||||
store.commit('updateImport', data);
|
||||
});
|
||||
}
|
||||
|
||||
export default function() {
|
||||
init();
|
||||
sync();
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,7 @@
|
||||
import Vue from 'vue'
|
||||
import _ from 'lodash'
|
||||
|
||||
export const setGlobalWarning = (state, warning) => {
|
||||
state.warnings[warning.key] = warning.value;
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import store from '../../'
|
||||
import { bus } from '../../../components/global/ws'
|
||||
|
||||
const init = () => {}
|
||||
|
||||
const sync = () => {
|
||||
bus.$on('WS_STATUS_CHANGE', ws => {
|
||||
return store.commit('setGlobalWarning', {key: 'connected', value: ws.connected});
|
||||
});
|
||||
}
|
||||
|
||||
export default function() {
|
||||
init();
|
||||
sync();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import superagent from 'superagent'
|
||||
import noCache from 'superagent-no-cache'
|
||||
import { restPath } from './api.js'
|
||||
|
||||
const processResponse = next => (err, res) => {
|
||||
if(err)
|
||||
return next(err);
|
||||
|
||||
if(!res.text)
|
||||
return next('no data');
|
||||
|
||||
let data = JSON.parse(res.text);
|
||||
|
||||
next(false, data);
|
||||
}
|
||||
|
||||
export const post = (to, data, next) => {
|
||||
superagent
|
||||
.post(restPath + to)
|
||||
.use(noCache)
|
||||
.send(data)
|
||||
.end(processResponse(next));
|
||||
}
|
||||
|
||||
export const get = (to, next) => {
|
||||
superagent
|
||||
.get(restPath + to)
|
||||
.use(noCache)
|
||||
.end(processResponse(next));
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// global window.CONFIG
|
||||
|
||||
const config = window.CONFIG.ui;
|
||||
const endpoint = `${config.host}${config.port === 80 ? '' : `:${config.port}`}${config.path}`;
|
||||
|
||||
let basePath, restPath, wsPath;
|
||||
|
||||
// rest API path
|
||||
if(config.ssl) {
|
||||
basePath = `https://${endpoint}`;
|
||||
} else {
|
||||
basePath = `http://${endpoint}`;
|
||||
}
|
||||
|
||||
restPath = basePath + 'api/';
|
||||
|
||||
// ws API path
|
||||
if(config.ssl) {
|
||||
wsPath = `wss://${endpoint}api`;
|
||||
} else {
|
||||
wsPath = `ws://${endpoint}api`;
|
||||
}
|
||||
|
||||
export {
|
||||
wsPath,
|
||||
restPath,
|
||||
basePath
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
const marked = require('marked');
|
||||
|
||||
// add `target='_blank'` to outgoing links
|
||||
|
||||
// https://github.com/chjj/marked/pull/451#issuecomment-49976076
|
||||
|
||||
var myRenderer = new marked.Renderer();
|
||||
myRenderer.link = function(href, title, text) {
|
||||
var external, newWindow, out;
|
||||
external = /^https?:\/\/.+$/.test(href);
|
||||
newWindow = external || title === 'newWindow';
|
||||
out = "<a href=\"" + href + "\"";
|
||||
if (newWindow) {
|
||||
out += ' target="_blank"';
|
||||
}
|
||||
if (title && title !== 'newWindow') {
|
||||
out += " title=\"" + title + "\"";
|
||||
}
|
||||
return out += ">" + text + "</a>";
|
||||
};
|
||||
|
||||
marked.setOptions({renderer: myRenderer});
|
||||
|
||||
export default marked;
|
||||
Reference in new issue
Block a user