Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue13 candlestick trading strategy #18

Merged
merged 16 commits into from
Nov 26, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.gradle
/build/
/out/
/data/
!gradle/wrapper/gradle-wrapper.jar

### STS ###
Expand All @@ -23,4 +24,4 @@ build/
nbbuild/
dist/
nbdist/
.nb-gradle/
.nb-gradle/
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: java -Dserver.port=$PORT -Dspring.data.mongodb.uri=$MONGODB_URI $JAVA_OPTS -jar build/libs/*.jar
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
buildscript {
ext {
kotlinVersion = '1.1.3-2'
kotlinVersion = '1.1.4'
springBootVersion = '2.0.0.BUILD-SNAPSHOT'
}
repositories {
Expand Down Expand Up @@ -42,6 +42,7 @@ dependencies {
compile("com.fasterxml.jackson.core:jackson-core:2.8.8")
compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.8.7")
compile("commons-codec:commons-codec:1.10")
compile("org.knowm.xchart:xchart:3.5.0")
compile('org.springframework.boot:spring-boot-starter-actuator')
// compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-data-mongodb')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,88 @@
package com.preslavrachev.cryptotrader

import com.preslavrachev.cryptotrader.extension.minusSecondPeriods
import com.preslavrachev.cryptotrader.extension.toCandlestick
import com.preslavrachev.cryptotrader.extension.toUnixTimestamp
import com.preslavrachev.cryptotrader.mvc.model.OrderTypeEnum
import com.preslavrachev.cryptotrader.mvc.model.TradeRecord
import com.preslavrachev.cryptotrader.persistence.repository.TradeRecordRepository
import com.preslavrachev.cryptotrader.session.AppSession
import com.preslavrachev.cryptotrader.trading.instrument.timeline.TimelineNode
import com.preslavrachev.cryptotrader.trading.strategy.TradingStrategyDecisionEnum
import com.preslavrachev.cryptotrader.trading.strategy.impl.CandlestickPatternTradingStrategy
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.scheduling.annotation.Scheduled
import remote.poloniex.model.ChartDataEntry
import remote.poloniex.service.PoloniexApiService
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.logging.Logger
import javax.inject.Inject

fun ChartDataEntry.calculateLocalDateTime(): LocalDateTime {
return LocalDateTime.ofInstant(Instant.ofEpochSecond(date), ZoneId.systemDefault())
}

@SpringBootApplication
class CryptotraderApplication
class CryptotraderApplication {

companion object {
val LOGGER = Logger.getLogger(CryptotraderApplication::class.simpleName)
}

@Inject
lateinit var session: AppSession

@Inject
lateinit var tradeRecordRepository: TradeRecordRepository

@Inject
lateinit var poloniexApiService: PoloniexApiService

@Scheduled(fixedRate = 5 * 60 * 1000)
fun testStrategy() {
val end = LocalDateTime.now().toUnixTimestamp()
val start = end.minusSecondPeriods(5, 300)
val chartData = poloniexApiService.getChartData(start = start, end = end)
val leftSide = chartData.dropLast(1)
val rightSide = chartData.drop(1)
val lastPair = leftSide.zip(rightSide)
.map { (current, prev) ->
listOf(
TimelineNode(prev.calculateLocalDateTime(), prev.toCandlestick()),
TimelineNode(current.calculateLocalDateTime(), current.toCandlestick())
)
}
.first()

val strategy = CandlestickPatternTradingStrategy()
val decision = strategy.decide(lastPair)

LOGGER.info("Candlestick Pattern Strategy Decision: $decision -- time period $start/$end")

if (decision == TradingStrategyDecisionEnum.BUY) {
val trade = TradeRecord(
amount = 0.00001f,
quoteCurrency = "BTC",
baseCurrency = "USDT",
baseCurrencyPrice = lastPair[0].content.estimateBuyingPrice(),
orderType = OrderTypeEnum.BUY
)
tradeRecordRepository.save(trade)
} else if (decision == TradingStrategyDecisionEnum.SELL) {
val trade = TradeRecord(
amount = 0.00001f,
quoteCurrency = "BTC",
baseCurrency = "USDT",
baseCurrencyPrice = lastPair[0].content.estimateSellingPrice(),
orderType = OrderTypeEnum.SELL
)
tradeRecordRepository.save(trade)
}
}
}

fun main(args: Array<String>) {
SpringApplication.run(CryptotraderApplication::class.java, *args)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.preslavrachev.cryptotrader.extension

import java.time.LocalDateTime

fun LocalDateTime.toUnixTimestamp(): Long {
return this.toEpochSecond(java.time.ZoneOffset.ofHours(2))
}

fun Long.minusSecondPeriods(periods: Long, secondsInPeriod: Long): Long {
val timePeriod = periods * secondsInPeriod
assert(timePeriod <= this) { "The time period cannot exceed the current time!" }

return this - timePeriod
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,43 @@
package com.preslavrachev.cryptotrader.mvc.controller
import com.preslavrachev.cryptotrader.extension.minusSecondPeriods
import com.preslavrachev.cryptotrader.extension.toUnixTimestamp
import com.preslavrachev.cryptotrader.mvc.model.Order
import com.preslavrachev.cryptotrader.session.AppSession
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.ResponseBody
import remote.poloniex.service.PoloniexApiService
import java.time.LocalDateTime
import javax.inject.Inject

@Controller
@RequestMapping("/")
class DashboardController {

@Inject
lateinit var session:AppSession

@Inject
lateinit var poloniexApiService: PoloniexApiService

@RequestMapping("hello", method = arrayOf(RequestMethod.GET))
@ResponseBody
fun hello(): PoloniexApiService.ChartDataEntryList {
return poloniexApiService.getChartData()
val end = LocalDateTime.now().toUnixTimestamp()
val start = end.minusSecondPeriods(100, 300)
return poloniexApiService.getChartData(start = start, end = end)
}

@RequestMapping("balances", method = arrayOf(RequestMethod.GET))
@ResponseBody
fun returnBalances(): Any {
return poloniexApiService.returnBalances()
}

@RequestMapping("orders", method = arrayOf(RequestMethod.GET))
@ResponseBody
fun orders(): List<Order> {
return session.orders
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ data class TradeRecord(
val amount: Float, /* always in relation to the quote currency */
val quoteCurrency: String, /* e.g. BTC in BTC/USDT */
val baseCurrency: String, /* e.g. USDT in BTC/USDT */
val baseCurrencyPrice: Float,
val executionDateTime: LocalDateTime = LocalDateTime.now(),
val orderType: OrderTypeEnum,
val orderScope: OrderScopeEnum = OrderScopeEnum.DEMO
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.preslavrachev.cryptotrader.session

class PackageMarker
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,12 @@ data class Candlestick(
fun isBodyTop(): Boolean {
return bodyMidpoint() >= (MAX_HEIGHT_RATIO * shadowHeight())
}

fun estimateBuyingPrice(): Float {
return Math.min(open, close)
}

fun estimateSellingPrice(): Float {
return Math.max(open, close)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@ package com.preslavrachev.cryptotrader.trading.instrument.candlestick
enum class CandlestickPatternEnum(
val evaluator: (prev: Candlestick, current:Candlestick) -> Boolean
) {
HAMMER ({prev, current -> isHammerShape(prev) && prev.isBodyTop() && current.isGreen() && !current.isDoji()}),
INVERTED_HAMMER ({prev, current -> isHammerShape(prev) && prev.isBodyBottom() && current.isGreen() && !current.isDoji()}),
SHOOTING_STAR ({prev, current -> isHammerShape(prev) && prev.isBodyBottom() && current.isRed() && !current.isDoji()}),
HANGING_MAN ({prev, current -> isHammerShape(prev) && prev.isBodyTop() && current.isRed() && !current.isDoji()}),
// @formatter:off
HAMMER ({prev, current -> isHammerShape(current) && current.isBodyTop() && prev.isRed() && !isHammerShape(prev) }),
INVERTED_HAMMER ({prev, current -> isHammerShape(current) && current.isBodyBottom() && prev.isRed() && !isHammerShape(prev)}),
SHOOTING_STAR ({prev, current -> isHammerShape(current) && current.isBodyBottom() && prev.isGreen() && !isHammerShape(prev)}),
HANGING_MAN ({prev, current -> isHammerShape(current) && current.isBodyTop() && prev.isGreen() && !isHammerShape(prev)}),
;
// @formatter:on

companion object {
private val BODY_SHADOW_MAX_HAMMER_RATIO = 0.6

fun evaluate(prev: Candlestick, current: Candlestick): List<CandlestickPatternEnum> {
return CandlestickPatternEnum.values()
.filter { it.evaluator(prev, current) }
}

fun isHammerShape(candlestick: Candlestick): Boolean {
return candlestick.bodyHeight() <= Candlestick.ONE_HALF * candlestick.shadowHeight()
return candlestick.bodyHeight() <= BODY_SHADOW_MAX_HAMMER_RATIO * candlestick.shadowHeight()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.preslavrachev.cryptotrader.trading.instrument.timeline

import java.time.LocalDateTime

data class TimelineNode<out T>(
val time: LocalDateTime,
val content: T
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.preslavrachev.cryptotrader.trading.strategy

import com.preslavrachev.cryptotrader.trading.instrument.timeline.TimelineNode

interface TradingStrategy<in T> {
fun decide(input: List<TimelineNode<T>>): TradingStrategyDecisionEnum
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.preslavrachev.cryptotrader.trading.strategy

enum class TradingStrategyDecisionEnum {
BUY,
SELL,
NO_DECISION
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.preslavrachev.cryptotrader.trading.strategy.impl

import com.preslavrachev.cryptotrader.trading.instrument.candlestick.Candlestick
import com.preslavrachev.cryptotrader.trading.instrument.candlestick.CandlestickPatternEnum
import com.preslavrachev.cryptotrader.trading.instrument.timeline.TimelineNode
import com.preslavrachev.cryptotrader.trading.strategy.TradingStrategy
import com.preslavrachev.cryptotrader.trading.strategy.TradingStrategyDecisionEnum
import java.util.*
import java.util.logging.Logger

class CandlestickPatternTradingStrategy : TradingStrategy<Candlestick> {

companion object {
val LOGGER = Logger.getLogger(CandlestickPatternTradingStrategy::class.simpleName)

// TODO: move this knowledge over to the candlestick patterns enum
val BOOLISH_PATTERNS = EnumSet.of(CandlestickPatternEnum.HAMMER, CandlestickPatternEnum.INVERTED_HAMMER)
val BEARISH_PATTERNS = EnumSet.of(CandlestickPatternEnum.SHOOTING_STAR, CandlestickPatternEnum.HANGING_MAN)
}

override fun decide(input: List<TimelineNode<Candlestick>>): TradingStrategyDecisionEnum {
assert(input.size >= 2) { LOGGER.warning("The input must have at least two data points!") }

val prev = input[input.lastIndex - 1]
val current = input[input.lastIndex]

val pattern = CandlestickPatternEnum.evaluate(prev.content, current.content)

if (pattern.any { BOOLISH_PATTERNS.contains(it) }) {
return TradingStrategyDecisionEnum.BUY
} else if (pattern.any { BEARISH_PATTERNS.contains(it) }) {
return TradingStrategyDecisionEnum.SELL
} else {
return TradingStrategyDecisionEnum.NO_DECISION
}
}
}
13 changes: 8 additions & 5 deletions src/main/kotlin/remote/poloniex/service/PoloniexApiService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ class PoloniexApiService {
class ChartDataEntryList: ArrayList<ChartDataEntry>()

enum class CurrencyPairEnum(val label: String) {
USDT_BTC ("USDT_BTC")
USDT_BTC ("USDT_BTC"),
BTC_ETH ("BTC_ETH"),
BTC_XRP ("BTC_XRP"),
BTC_DASH ("BTC_DASH"),
}

enum class CommandEnum(val label: String) {
Expand All @@ -49,13 +52,13 @@ class PoloniexApiService {
@Inject
lateinit var restTemplate: RestTemplate

fun getChartData(): ChartDataEntryList {
fun getChartData(start: Long, end: Long): ChartDataEntryList {
val url = PUBLIC_URL_BUILDER
.queryParam(COMMAND_PARAM, CommandEnum.RETURN_CHART_DATA.label)
.queryParam(CURRENCY_PAIR_PARAM, CurrencyPairEnum.USDT_BTC)
.queryParam(PERIOD_PARAM, 300)
.queryParam(START_PARAM, 1500000000)
.queryParam(END_PARAM, 1500030000)
.queryParam(START_PARAM, start)
.queryParam(END_PARAM, end)
.build()

val response = restTemplate.getForEntity(url, ChartDataEntryList::class.java)
Expand Down Expand Up @@ -91,4 +94,4 @@ class PoloniexApiService {
.joinToString(separator = "&") { entry -> "${entry.key}=${entry.value}" }
.encrypt512(apiSecret)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.preslavrachev.cryptotrader.extension

fun <T> List<T>.toPairs(): List<Pair<T,T>> {
val leftSide = this.dropLast(1)
val rightSide = this.drop(1)
return leftSide.zip(rightSide)
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.preslavrachev.cryptotrader.instrument

import com.preslavrachev.cryptotrader.config.MainAppConfig
import com.preslavrachev.cryptotrader.extension.minusSecondPeriods
import com.preslavrachev.cryptotrader.extension.toCandlestick
import com.preslavrachev.cryptotrader.extension.toUnixTimestamp
import com.preslavrachev.cryptotrader.trading.instrument.candlestick.CandlestickPatternEnum
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit4.SpringRunner
import remote.poloniex.service.PoloniexApiService
import java.time.LocalDateTime
import javax.inject.Inject


Expand All @@ -22,9 +25,11 @@ class CandlestickITest {
@Ignore("Use only manually for now")
@Test()
fun testCandlestickPatterns(): Unit {

val leftSide = poloniexApiService.getChartData().dropLast(1)
val rightSide = poloniexApiService.getChartData().drop(1)
val end = LocalDateTime.now().toUnixTimestamp()
val start = end.minusSecondPeriods(30, 300)
val chartData = poloniexApiService.getChartData(start = start, end = end)
val leftSide = chartData.dropLast(1)
val rightSide = chartData.drop(1)
val data = leftSide.zip(rightSide)
.map { (prev, current) -> Pair(prev.toCandlestick(), current.toCandlestick()) }
.map { (prev, current) -> CandlestickPatternEnum.evaluate(prev, current) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class OrderListProcessorTest {
fun setUp() {
orderListProcessor = OrderListProcessor()
orderListProcessor.tradingApi = DemoTradingService()
//orderListProcessor.postConstruct()
orderListProcessor.postConstruct()
}

@Test
Expand Down
Loading