Skip to content

Commit

Permalink
+backtest #30 add excel export
Browse files Browse the repository at this point in the history
  • Loading branch information
eryk committed Apr 6, 2017
1 parent 2355891 commit 4cb81f9
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 129 deletions.
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ libraryDependencies ++= Seq(
"org.apache.hbase" % "hbase-common" % "1.3.0",
"org.apache.hadoop" % "hadoop-common" % "2.7.3" excludeAll ExclusionRule(organization = "javax.servlet"),
"org.scalatest" % "scalatest_2.11" % "3.0.1" % "test",
"org.apache.poi" % "poi" % "3.15"
"org.apache.poi" % "poi" % "3.15",
"info.folone" %% "poi-scala" % "0.18"
)

assemblyMergeStrategy in assembly := {
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/com/squant/cheetah/event/BackTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class BackTest(strategy: Strategy) extends Actor with ActorLogging {
strategy.getContext.clock.update()
self ! TimeEvent
} else {
strategy.report()
ExcelUtils.export(strategy.portfolio,"/home/eryk/test.xls")
self ! Finished
}
}
Expand Down
13 changes: 0 additions & 13 deletions src/main/scala/com/squant/cheetah/strategy/Strategy.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,4 @@ abstract class Strategy(context: Context) extends LazyLogging {
def handle()

def getContext = context

// def processes() = {
// if (isTradingTime(context.clock.now())) {
// symbols.foreach(process)
// }
// context.clock.update()
// }

// def now(): LocalDateTime = context.clock.now()

def report(): Unit ={
portfolio.report()
}
}
49 changes: 49 additions & 0 deletions src/main/scala/com/squant/cheetah/trade/Metric.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.squant.cheetah.trade

import com.squant.cheetah.domain.{LONG, Order, SHORT}

/**
* Created by eryk on 17-4-5.
*/
class Metric() {
//交易盈亏
var pnl: Double = 0
//收益率=交易盈亏/起始资产
var pnlRate: Double = 0
//年化收益率= 交易盈亏 / (平均持股天数/交易总天数)
var annualReturn: Double = 0
//平均每只股票持仓天数
var avgPositionDays: Int = 0
//最大单笔盈利
var maxEarnPerOp: Double = 0
//最大单笔亏损
var maxLossPerOp: Double = 0
//平均每笔盈利
var meanEarnPerOp: Double = 0
//连续盈利次数
var continuousEarnOp: Int = 0
//连续亏损次数
var continuousLossOp: Int = 0
//基准收益额,同期股价涨跌额,单位:元
var benchmarkBenfit: Double = 0
//基准收益百分比
var benchmarkBenfitPercent: Double = 0
//最大资产
var max: Double = 0
//最小资产
var min: Double = Double.MaxValue
//最大回撤=最大资产-最小资产
var drawdown: Double = 0
//总交易次数
var totalOperate: Int = 0
//总盈利次数
var earnOperate: Int = 0
//总亏损交易次数
var lossOperate: Int = 0
//操作正确率=总盈利次数/总交易次数
var accuracy: Double = 0
//夏普率
var sharpe: Double = 0
//所提诺比率
var sortino: Double = 0
}
179 changes: 65 additions & 114 deletions src/main/scala/com/squant/cheetah/trade/Portfolio.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,154 +18,86 @@ class Portfolio(context: Context) extends LazyLogging {
//可用资金, 可用来购买证券的资金
var availableCash: Double = startingCash

private val records = mutable.Buffer[Record]() //记录各个时间点账户状态
val records = mutable.Buffer[Record]() //记录各个时间点账户状态

var ts: LocalDateTime = null //最后更新record的时间点

//key是股票代码code
var positions: mutable.Map[String, Position] = mutable.LinkedHashMap[String, Position]()

//记录账户当前持仓情况

private class Metric() {
//交易盈亏
var pnl: Double = 0
//收益率=交易盈亏/起始资产
var pnlRate: Double = 0
//年化收益率= 交易盈亏 / (平均持股天数/交易总天数)
var annualReturn: Double = 0
//平均每只股票持仓天数
var avgPositionDays: Int = 0
//最大单笔盈利
var maxEarnPerOp: Double = 0
//最大单笔亏损
var maxLossPerOp: Double = 0
//平均每笔盈利
var meanEarnPerOp: Double = 0
//连续盈利次数
var continuousEarnOp: Int = 0
//连续亏损次数
var continuousLossOp: Int = 0
//基准收益额,同期股价涨跌额,单位:元
var benchmarkBenfit: Double = 0
//基准收益百分比
var benchmarkBenfitPercent: Double = 0
//最大资产
var max: Double = startingCash
//最小资产
var min: Double = Double.MaxValue
//最大回撤=最大资产-最小资产
var drawdown: Double = 0
//总交易次数
var totalOperate: Int = 0
//总盈利次数
var earnOperate: Int = 0
//总亏损交易次数
var lossOperate: Int = 0
//操作正确率=总盈利次数/总交易次数
var accuracy: Double = 0
//夏普率
var sharpe: Double = 0
//所提诺比率
var sortino: Double = 0

def update(order: Order): Unit = {
ts = context.clock.now()
order.direction match {
case LONG => {
//是否已经持有此股票
if (positions.get(order.code).isEmpty) {
positions.put(order.code, Position.mk(order))
} else {
val position = positions.get(order.code).get
positions.put(order.code, position.add(Position.mk(order)))
}
//扣除交易金额
availableCash -= order.volume

}
case SHORT => {
var position = positions.get(order.code).get

position = position.sub(Position.mk(order))
if (position.totalAmount == 0) {
positions.remove(order.code)
} else {
positions.put(order.code, position)
}
availableCash += order.volume

max = Math.max(max, endingCash)
min = Math.min(min, endingCash)
val metric = new Metric

def update(order: Order): Unit = {
ts = context.clock.now()
order.direction match {
case LONG => {
//是否已经持有此股票
if (positions.get(order.code).isEmpty) {
positions.put(order.code, Position.mk(order))
} else {
val position = positions.get(order.code).get
positions.put(order.code, position.add(Position.mk(order)))
}
case _ => new UnknownError("unkown order direction")
}
totalOperate += 1

//计算税费
availableCash -= context.cost.cost(order)
//扣除交易金额
availableCash -= order.volume

endingCash = positions.foldLeft[Double](availableCash) {
(a, b) => a + b._2.totalAmount * b._2.avgCost
}
case SHORT => {
var position = positions.get(order.code).get

position = position.sub(Position.mk(order))
if (position.totalAmount == 0) {
positions.remove(order.code)
} else {
positions.put(order.code, position)
}
availableCash += order.volume

endingCash -= context.cost.cost(order)
metric.max = Math.max(metric.max, endingCash)
metric.min = Math.min(metric.min, endingCash)
}
case _ => new UnknownError("unkown order direction")
}
metric.totalOperate += 1

pnl = endingCash - startingCash
pnlRate = (endingCash - startingCash) / startingCash * 100
//计算税费
availableCash -= context.cost.cost(order)

records.append(new Record(order.code, order.direction ,order.amount, order.price, order.volume, context.cost.cost(order), ts))
endingCash = positions.foldLeft[Double](availableCash) {
(a, b) => a + b._2.totalAmount * b._2.avgCost
}

override def toString: String = {
val buffer = mutable.StringBuilder.newBuilder
for (item <- records) {
buffer.append(s"\t\t${item}\n")
}
endingCash -= context.cost.cost(order)

val posStr = mutable.StringBuilder.newBuilder
positions.foreach { item =>
posStr.append(s"\t\t${item._1} ${item._2}\n")
}
metric.pnl = endingCash - startingCash
metric.pnlRate = (endingCash - startingCash) / startingCash * 100

s"账户详情信息:\n" +
s"\t起始资金:$startingCash\n" +
f"\t期末资金:$endingCash%2.2f\n" +
f"\t可用资金:$availableCash%2.2f\n" +
f"\t交易盈亏:$pnl%2.2f($pnlRate%2.2f" + "%)\n" +
s"\t交易次数:$totalOperate\n" +
f"\t最大资产:$max%2.2f\n" +
f"\t最小资产:$min%2.2f\n" +
f"\t持仓情况:\n${posStr.toString}" +
s"\t交易记录:\n${buffer.toString}"
}
records.append(new Record(order.code, order.direction, order.amount, order.price, order.volume, context.cost.cost(order), ts))
}

private val metric = new Metric

def buy(code: String, amount: Int, orderStyle: OrderStyle): OrderState = {
val order = context.slippage.compute(Order(code, amount, orderStyle, LONG, context.clock.now()))
metric update order
update(order)
logger.debug(s"${context.clock.now()} buy ${code} $amount at price ${order.price}")
SUCCESS
}

def buyAll(code: String, orderStyle: OrderStyle): OrderState = {

val amount = (availableCash / context.slippage.compute(Order(code,0,orderStyle,LONG,context.clock.now())).price / 100).toInt * 100
val amount = (availableCash / context.slippage.compute(Order(code, 0, orderStyle, LONG, context.clock.now())).price / 100).toInt * 100
if (amount == 0) {
return FAILED
}
val order = context.slippage.compute(Order(code, amount, orderStyle, LONG, context.clock.now()))

metric update order
update(order)
logger.debug(s"${context.clock.now()} buy ${code} $amount at price ${order.price}")
SUCCESS
}

def sell(code: String, amount: Int, orderStyle: OrderStyle): OrderState = {
val order = context.slippage.compute(Order(code, amount, orderStyle, SHORT, context.clock.now()))
metric update order
update(order)
logger.debug(s"${context.clock.now()} sell ${code} $amount at price ${order.price}")
SUCCESS
}
Expand All @@ -174,15 +106,34 @@ class Portfolio(context: Context) extends LazyLogging {
positions.get(code) match {
case Some(position) => {
val order = context.slippage.compute(Order(code, position.totalAmount, orderStyle, SHORT, context.clock.now()))
metric update order //最后更新metric,metric里会记录position
update(order) //最后更新metric,metric里会记录position
logger.debug(s"${context.clock.now()} sell ${code} ${order.amount} at price ${order.price}")
SUCCESS
}
case None => FAILED
}
}

def report(): Unit = {
logger.info(metric.toString)
override def toString: String = {
val buffer = mutable.StringBuilder.newBuilder
for (item <- records) {
buffer.append(s"\t\t${item}\n")
}

val posStr = mutable.StringBuilder.newBuilder
positions.foreach { item =>
posStr.append(s"\t\t${item._1} ${item._2}\n")
}

s"账户详情信息:\n" +
s"\t起始资金:$startingCash\n" +
f"\t期末资金:$endingCash%2.2f\n" +
f"\t可用资金:$availableCash%2.2f\n" +
f"\t交易盈亏:${metric.pnl}%2.2f(${metric.pnlRate}%2.2f" + "%)\n" +
s"\t交易次数:${metric.totalOperate}\n" +
f"\t最大资产:${metric.max}%2.2f\n" +
f"\t最小资产:${metric.min}%2.2f\n" +
f"\t持仓情况:\n${posStr.toString}" +
s"\t交易记录:\n${buffer.toString}"
}
}
Loading

0 comments on commit 4cb81f9

Please sign in to comment.