Repository: zhs007/trdb2py

Issue:
需要能本地计算最大回撤、回报率等
需要能本地计算最大回撤、回报率等

Discussion:

Patch:
diff --git a/VERSION b/VERSION
index eac0a14..3788c77 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-v0.2.1
\ No newline at end of file
+v0.2.6
\ No newline at end of file
diff --git a/protos/trading2.proto b/protos/trading2.proto
index ce86e52..377da35 100644
--- a/protos/trading2.proto
+++ b/protos/trading2.proto
@@ -196,6 +196,7 @@ message PNLDataValue {
     float value = 5;            // 总价值
     float volume = 6;           // 如果是单资产，这里是拥有数量
     float price = 7;            // 如果是单资产，这里是当前资产价格
+    float drawdown = 8;         // 该点的回撤
 }
 
 // PNLAssetData - asset data in PNL
diff --git a/trdb2py/__init__.py b/trdb2py/__init__.py
index 8ba215e..84534e2 100644
--- a/trdb2py/__init__.py
+++ b/trdb2py/__init__.py
@@ -2,9 +2,11 @@
 from trdb2py.config import loadConfig
 from trdb2py.trdb2 import simTradings, getAssetCandles, simTrading, getAssetCandles2
 from trdb2py.utils import str2asset, nextWeekDay, asset2str
-from trdb2py.timeutils import str2timestamp
-from trdb2py.pdutils import (buildPNLReport, getPNLLastTs, getPNLValueWithTimestamp, mergePNL,
-                             rmPNLValuesWithTimestamp, getPNLTimestampLowInMonth, getPNLTimestampHighInMonth)
+from trdb2py.timeutils import str2timestamp, getDayInYear, getYearDays
+from trdb2py.pdutils import (buildPNLReport, getPNLLastTs, getPNLValueWithTimestamp, mergePNL, mergePNLEx,
+                             rmPNLValuesWithTimestamp, getPNLTimestampLowInMonth, getPNLTimestampHighInMonth,
+                             countTradingDays4Year, calcAnnualizedVolatility, rebuildPNL, rebuildDrawdown,
+                             calcAnnualizedReturns, calcSharpe)
 from trdb2py.statistics import (calcPNLWinRateInYear, buildPNLWinRateInYears,
                                 buildPNLWinRateInMonths, buildPNLListWinRateInYears2,
                                 buildPNLListWinRateInMonths2, buildPNLListWinRate4Month,
diff --git a/trdb2py/pdutils.py b/trdb2py/pdutils.py
index ac55e7c..036b2f1 100644
--- a/trdb2py/pdutils.py
+++ b/trdb2py/pdutils.py
@@ -4,6 +4,9 @@ from trdb2py.utils import str2asset, asset2str
 from datetime import datetime
 import time
 import pandas as pd
+import numpy as np
+import math
+from trdb2py.timeutils import str2timestamp, getDayInYear, getYearDays
 
 
 def buildPNLReport(lstpnl: list) -> pd.DataFrame:
@@ -166,9 +169,21 @@ def mergePNL(lstpnl: list) -> trdb2py.trading2_pb2.PNLAssetData:
     return pnl
 
 
+def mergePNLEx(pnldest: trdb2py.trading2_pb2.PNLAssetData, pnlsrc: trdb2py.trading2_pb2.PNLAssetData, inmoney):
+    for cai in range(0, len(pnlsrc.values)):
+        di = getPNLValueWithTimestamp(pnlsrc.values[cai].ts, pnldest)
+        pnldest.values[di].value += (pnlsrc.values[cai].value - inmoney)
+
+        if pnldest.values[di].cost > 0:
+            pnldest.values[di].perValue = pnldest.values[di].value / \
+                pnldest.values[di].cost
+        else:
+            pnldest.values[di].perValue = 1
+
+
 def rmPNLValuesWithTimestamp(ts, pnl: trdb2py.trading2_pb2.PNLAssetData):
     i = getPNLValueWithTimestamp(ts, pnl)
-    pnl.values.extend(pnl.values[0:i+1])
+    del pnl.values[i+1:]
 
 
 def getPNLTimestampLowInMonth(pnl: trdb2py.trading2_pb2.PNLAssetData) -> list:
@@ -235,3 +250,89 @@ def getPNLTimestampHighInMonth(pnl: trdb2py.trading2_pb2.PNLAssetData) -> list:
                 lastPerValue = v.perValue
 
     return arr
+
+
+def countTradingDays4Year(pnl: trdb2py.trading2_pb2.PNLAssetData):
+    if len(pnl.values) > 0:
+        st = datetime.utcfromtimestamp(pnl.values[0].ts)
+        et = datetime.utcfromtimestamp(pnl.values[len(pnl.values) - 1].ts)
+
+        std = getDayInYear(st.year, st.month, st.day)
+        etd = getDayInYear(et.year, et.month, et.day)
+
+        sty = std / float(getYearDays(st.year))
+        ety = etd / float(getYearDays(et.year))
+
+        fy = et.year - st.year - 1 + 1 - sty + ety
+
+        return int(len(pnl.values) / fy)
+
+    return 0
+
+
+def calcAnnualizedVolatility(pnl: trdb2py.trading2_pb2.PNLAssetData):
+    # https://www.zhihu.com/question/19770602
+    # https://wiki.mbalib.com/wiki/%E5%8E%86%E5%8F%B2%E6%B3%A2%E5%8A%A8%E7%8E%87
+
+    if len(pnl.values) > 0:
+        arr = []
+        for i in range(1, len(pnl.values)):
+            arr.append(
+                math.log(pnl.values[i].perValue / pnl.values[i-1].perValue))
+
+        arrstd = np.std(arr)
+        pnl.annualizedVolatility = arrstd * \
+            math.sqrt(countTradingDays4Year(pnl))
+    else:
+        return 0
+
+
+def rebuildPNL(pnl: trdb2py.trading2_pb2.PNLAssetData):
+    if len(pnl.values) > 0:
+        pnl.totalReturns = pnl.values[len(pnl.values) - 1].perValue
+
+        calcAnnualizedVolatility(pnl)
+
+        rebuildDrawdown(pnl)
+
+        calcAnnualizedReturns(pnl)
+
+        calcSharpe(pnl)
+    else:
+        pnl.totalReturns = 1.0
+        pnl.annualizedVolatility = 0
+
+
+def rebuildDrawdown(pnl: trdb2py.trading2_pb2.PNLAssetData):
+    maxv = 0
+    maxdd = 0
+    startts = 0
+    endts = 0
+
+    for v in pnl.values:
+        if v.perValue > maxv:
+            maxv = v.perValue
+            v.drawdown = 0
+            startts = v.ts
+        else:
+            v.drawdown = (maxv - v.perValue) / maxv
+
+            if v.drawdown > maxdd:
+                maxdd = v.drawdown
+                endts = v.ts
+
+    pnl.maxDrawdown = maxdd
+    pnl.maxDrawdownStartTs = startts
+    pnl.maxDrawdownEndTs = endts
+
+
+def calcAnnualizedReturns(pnl: trdb2py.trading2_pb2.PNLAssetData):
+    if len(pnl.values) > 0:
+        pnl.annualizedreturns = (pnl.values[len(
+            pnl.values) - 1].perValue - 1) / len(pnl.values) * countTradingDays4Year(pnl)
+
+
+def calcSharpe(pnl: trdb2py.trading2_pb2.PNLAssetData):
+    # https://www.zhihu.com/question/27264526
+
+    pnl.sharpe = (pnl.annualizedreturns - 0.03) / pnl.annualizedvolatility
diff --git a/trdb2py/timeutils.py b/trdb2py/timeutils.py
index 7d1998b..9360863 100644
--- a/trdb2py/timeutils.py
+++ b/trdb2py/timeutils.py
@@ -5,3 +5,21 @@ from datetime import datetime
 def str2timestamp(strtime: str, strformat: str):
     dt = datetime.strptime(strtime, strformat)
     return dt.timestamp()
+
+
+def getDayInYear(year, month, day):
+    daymonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
+    if year % 400 == 0 or (year % 4 == 0 and year % 100 != 0):
+        daymonth[1] = 29
+
+    if month == 1:
+        return day
+    else:
+        return sum(daymonth[:month-1])+day
+
+
+def getYearDays(year):
+    if year % 400 == 0 or (year % 4 == 0 and year % 100 != 0):
+        return 366
+
+    return 365
diff --git a/trdb2py/trading2_pb2.py b/trdb2py/trading2_pb2.py
index 379bbb8..870463f 100644
--- a/trdb2py/trading2_pb2.py
+++ b/trdb2py/trading2_pb2.py
@@ -20,7 +20,7 @@ DESCRIPTOR = _descriptor.FileDescriptor(
   syntax='proto3',
   serialized_options=b'Z&github.com/zhs007/tradingdb2/tradingpb',
   create_key=_descriptor._internal_create_key,
-  serialized_pb=b'\n\x0etrading2.proto\x12\ttradingpb\"!\n\x10\x42\x61sicRequestData\x12\r\n\x05token\x18\x01 \x01(\t\"3\n\x05\x41sset\x12\x0e\n\x06market\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0c\n\x04tags\x18\x03 \x03(\t\"1\n\x11\x43trlNodeAssetInfo\x12\x0e\n\x06volume\x18\x01 \x01(\x01\x12\x0c\n\x04\x63ost\x18\x02 \x01(\x01\"\xed\x03\n\x08\x43trlNode\x12\n\n\x02ts\x18\x01 \x01(\x03\x12\x1d\n\x03src\x18\x02 \x01(\x0b\x32\x10.tradingpb.Asset\x12\x1d\n\x03\x64st\x18\x03 \x01(\x0b\x32\x10.tradingpb.Asset\x12\x11\n\tvolumeSrc\x18\x04 \x01(\x01\x12\x11\n\tvolumeDst\x18\x05 \x01(\x01\x12!\n\x04type\x18\x06 \x01(\x0e\x32\x13.tradingpb.CtrlType\x12\x0b\n\x03\x66\x65\x65\x18\x07 \x01(\x01\x12\x1b\n\x13\x61verageHoldingPrice\x18\x08 \x01(\x01\x12\x11\n\tsellPrice\x18\t \x01(\x01\x12\x17\n\x0f\x63trlConditionID\x18\n \x01(\x05\x12\x12\n\nstrategyID\x18\x0b \x01(\x05\x12\x12\n\ntotalMoney\x18\x0c \x01(\x01\x12\x11\n\tlastMoney\x18\r \x01(\x01\x12=\n\rmapAssetsInfo\x18\x0e \x03(\x0b\x32&.tradingpb.CtrlNode.MapAssetsInfoEntry\x12\x12\n\nmoneyParts\x18\x0f \x01(\x05\x12\x16\n\x0elastMoneyParts\x18\x10 \x01(\x05\x1aR\n\x12MapAssetsInfoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.tradingpb.CtrlNodeAssetInfo:\x02\x38\x01\"=\n\x0bWalletAsset\x12\x1f\n\x05\x61sset\x18\x01 \x01(\x0b\x32\x10.tradingpb.Asset\x12\r\n\x05value\x18\x02 \x01(\x01\"0\n\x06Wallet\x12&\n\x06\x61ssets\x18\x01 \x03(\x0b\x32\x16.tradingpb.WalletAsset\"\xde\x01\n\rCtrlCondition\x12\x15\n\tindicator\x18\x01 \x01(\tB\x02\x18\x01\x12\x0c\n\x04vals\x18\x02 \x03(\x02\x12\x11\n\toperators\x18\x03 \x03(\t\x12\x15\n\rcombCondition\x18\x04 \x01(\t\x12\x13\n\x07minvals\x18\x05 \x03(\x02\x42\x02\x18\x01\x12\x13\n\x07maxvals\x18\x06 \x03(\x02\x42\x02\x18\x01\x12\x13\n\x07offvals\x18\x07 \x03(\x02\x42\x02\x18\x01\x12\x0f\n\x07strVals\x18\x08 \x03(\t\x12\x0c\n\x04name\x18\t \x01(\t\x12\r\n\x05group\x18\n \x01(\x05\x12\x11\n\tint64Vals\x18\x0b \x03(\x02\")\n\rIndicatorData\x12\x0c\n\x04vals\x18\x01 \x03(\x02\x12\n\n\x02ts\x18\x02 \x01(\x03\"m\n\tIndicator\x12\x10\n\x08\x66ullname\x18\x01 \x01(\t\x12&\n\x04type\x18\x02 \x01(\x0e\x32\x18.tradingpb.IndicatorType\x12&\n\x04\x64\x61ta\x18\x03 \x03(\x0b\x32\x18.tradingpb.IndicatorData\"\xb3\x02\n\tBuyParams\x12\x15\n\tinitMoney\x18\x01 \x01(\x02\x42\x02\x18\x01\x12\x14\n\x08perMoney\x18\x02 \x01(\x02\x42\x02\x18\x01\x12\x0e\n\x06volume\x18\x03 \x01(\x02\x12\x10\n\x08\x61ipMoney\x18\x04 \x01(\x02\x12\x14\n\x0cperInitMoney\x18\x05 \x01(\x02\x12\x14\n\x0cperHandMoney\x18\x06 \x01(\x02\x12\x19\n\rperTotalMoney\x18\x07 \x01(\x02\x42\x02\x18\x01\x12\x11\n\tassetCode\x18\x08 \x01(\t\x12\x18\n\x10timeTypeBuyAsset\x18\t \x01(\t\x12\x10\n\x08tsOffBuy\x18\n \x01(\x03\x12\x14\n\x0ctypeBuyAsset\x18\x0b \x01(\t\x12\x12\n\nmoneyParts\x18\x0c \x01(\x05\x12\x11\n\tnextTimes\x18\r \x01(\x05\x12\x14\n\x0c\x64\x65positMoney\x18\x0e \x01(\x05\"\xbb\x01\n\nSellParams\x12\x0e\n\x06volume\x18\x01 \x01(\x02\x12\x11\n\tperVolume\x18\x02 \x01(\x02\x12\r\n\x05money\x18\x03 \x01(\x02\x12\x10\n\x08keepTime\x18\x04 \x01(\x03\x12\x11\n\tassetCode\x18\x05 \x01(\t\x12\x19\n\x11timeTypeSellAsset\x18\x06 \x01(\t\x12\x11\n\ttsOffSell\x18\x07 \x01(\x03\x12\x15\n\rtypeSellAsset\x18\x08 \x01(\t\x12\x11\n\tnextTimes\x18\t \x01(\x05\"K\n\x0eStopLossParams\x12\x14\n\x08\x64rawdown\x18\x01 \x01(\x02\x42\x02\x18\x01\x12\x10\n\x08isFinish\x18\x02 \x01(\x08\x12\x11\n\tperVolume\x18\x03 \x01(\x02\"q\n\x10TakeProfitParams\x12\x12\n\x06profit\x18\x01 \x01(\x02\x42\x02\x18\x01\x12\x10\n\x08isFinish\x18\x02 \x01(\x08\x12\x11\n\tperVolume\x18\x03 \x01(\x02\x12\x0e\n\x06giveTo\x18\x04 \x01(\t\x12\x14\n\x0cisOnlyProfit\x18\x05 \x01(\x08\"+\n\nInitParams\x12\r\n\x05money\x18\x01 \x01(\x02\x12\x0e\n\x06vomume\x18\x02 \x01(\x02\"M\n\tAIPParams\x12\r\n\x05money\x18\x01 \x01(\x02\x12$\n\x04type\x18\x02 \x01(\x0e\x32\x16.tradingpb.AIPTimeType\x12\x0b\n\x03\x64\x61y\x18\x03 \x01(\x05\"C\n\tFeeParams\x12\x12\n\npercentage\x18\x01 \x01(\x02\x12\x10\n\x08minLimit\x18\x02 \x01(\x02\x12\x10\n\x08maxLimit\x18\x03 \x01(\x02\"\xe8\x04\n\x08Strategy\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1f\n\x05\x61sset\x18\x02 \x01(\x0b\x32\x10.tradingpb.Asset\x12%\n\x03\x62uy\x18\x03 \x03(\x0b\x32\x18.tradingpb.CtrlCondition\x12&\n\x04sell\x18\x04 \x03(\x0b\x32\x18.tradingpb.CtrlCondition\x12*\n\x08stoploss\x18\x05 \x03(\x0b\x32\x18.tradingpb.CtrlCondition\x12,\n\ntakeprofit\x18\x06 \x03(\x0b\x32\x18.tradingpb.CtrlCondition\x12\'\n\tparamsBuy\x18\x07 \x01(\x0b\x32\x14.tradingpb.BuyParams\x12)\n\nparamsSell\x18\x08 \x01(\x0b\x32\x15.tradingpb.SellParams\x12\x31\n\x0eparamsStopLoss\x18\t \x01(\x0b\x32\x19.tradingpb.StopLossParams\x12\x35\n\x10paramsTakeProfit\x18\n \x01(\x0b\x32\x1b.tradingpb.TakeProfitParams\x12)\n\nparamsInit\x18\x0b \x01(\x0b\x32\x15.tradingpb.InitParams\x12\'\n\tparamsAIP\x18\x0c \x01(\x0b\x32\x14.tradingpb.AIPParams\x12\x11\n\x05title\x18\r \x01(\tB\x02\x18\x01\x12\x12\n\nindicators\x18\x0e \x03(\t\x12$\n\x06\x66\x65\x65\x42uy\x18\x0f \x01(\x0b\x32\x14.tradingpb.FeeParams\x12%\n\x07\x66\x65\x65Sell\x18\x10 \x01(\x0b\x32\x14.tradingpb.FeeParams\"v\n\x0cPNLDataValue\x12\n\n\x02ts\x18\x01 \x01(\x03\x12\x10\n\x08perValue\x18\x02 \x01(\x02\x12\x0c\n\x04tags\x18\x03 \x03(\t\x12\x0c\n\x04\x63ost\x18\x04 \x01(\x02\x12\r\n\x05value\x18\x05 \x01(\x02\x12\x0e\n\x06volume\x18\x06 \x01(\x02\x12\r\n\x05price\x18\x07 \x01(\x02\"\xdf\x06\n\x0cPNLAssetData\x12\x1f\n\x05\x61sset\x18\x01 \x01(\x0b\x32\x10.tradingpb.Asset\x12\'\n\x06values\x18\x02 \x03(\x0b\x32\x17.tradingpb.PNLDataValue\x12\x13\n\x0bmaxDrawdown\x18\x03 \x01(\x02\x12\x1a\n\x12maxDrawdownStartTs\x18\x04 \x01(\x03\x12\x18\n\x10maxDrawdownEndTs\x18\x05 \x01(\x03\x12\x11\n\tmaxDrawup\x18\x06 \x01(\x02\x12\x18\n\x10maxDrawupStartTs\x18\x07 \x01(\x03\x12\x16\n\x0emaxDrawupEndTs\x18\x08 \x01(\x03\x12\x0e\n\x06sharpe\x18\t \x01(\x02\x12\x19\n\x11\x61nnualizedReturns\x18\n \x01(\x02\x12\x1c\n\x14\x61nnualizedVolatility\x18\x0b \x01(\x02\x12\x14\n\x0ctotalReturns\x18\x0c \x01(\x02\x12\x10\n\x08variance\x18\r \x01(\x02\x12\x10\n\x08\x62uyTimes\x18\x0e \x01(\x05\x12\x11\n\tsellTimes\x18\x0f \x01(\x05\x12\x15\n\rstoplossTimes\x18\x10 \x01(\x05\x12\x12\n\nmaxUpDayTs\x18\x11 \x01(\x03\x12\x13\n\x0bmaxPerUpDay\x18\x12 \x01(\x02\x12\x14\n\x0cmaxDownDayTs\x18\x13 \x01(\x03\x12\x15\n\rmaxPerDownDay\x18\x14 \x01(\x02\x12\x13\n\x0bmaxUpWeekTs\x18\x15 \x01(\x03\x12\x14\n\x0cmaxPerUpWeek\x18\x16 \x01(\x02\x12\x15\n\rmaxDownWeekTs\x18\x17 \x01(\x03\x12\x16\n\x0emaxPerDownWeek\x18\x18 \x01(\x02\x12\x14\n\x0cmaxUpMonthTs\x18\x19 \x01(\x03\x12\x15\n\rmaxPerUpMonth\x18\x1a \x01(\x02\x12\x16\n\x0emaxDownMonthTs\x18\x1b \x01(\x03\x12\x17\n\x0fmaxPerDownMonth\x18\x1c \x01(\x02\x12\x13\n\x0bmaxUpYearTs\x18\x1d \x01(\x03\x12\x14\n\x0cmaxPerUpYear\x18\x1e \x01(\x02\x12\x15\n\rmaxDownYearTs\x18\x1f \x01(\x03\x12\x16\n\x0emaxPerDownYear\x18  \x01(\x02\x12$\n\x07lstCtrl\x18! \x03(\x0b\x32\x13.tradingpb.CtrlNode\x12(\n\nindicators\x18\" \x03(\x0b\x32\x14.tradingpb.Indicator\x12\x10\n\x08winTimes\x18# \x01(\x05\"\x93\x01\n\x07PNLData\x12\x10\n\x04name\x18\x01 \x01(\tB\x02\x18\x01\x12&\n\x05total\x18\x02 \x01(\x0b\x32\x17.tradingpb.PNLAssetData\x12+\n\x06\x61ssets\x18\x03 \x03(\x0b\x32\x17.tradingpb.PNLAssetDataB\x02\x18\x01\x12\r\n\x05title\x18\x04 \x01(\t\x12\x12\n\x06lastts\x18\x05 \x01(\x03\x42\x02\x18\x01\"\x94\x02\n\x10SimTradingParams\x12 \n\x06\x61ssets\x18\x01 \x03(\x0b\x32\x10.tradingpb.Asset\x12\'\n\tbaselines\x18\x02 \x03(\x0b\x32\x10.tradingpb.AssetB\x02\x18\x01\x12\x0f\n\x07startTs\x18\x03 \x01(\x03\x12\r\n\x05\x65ndTs\x18\x04 \x01(\x03\x12\'\n\nstrategies\x18\x05 \x03(\x0b\x32\x13.tradingpb.Strategy\x12!\n\x06wallet\x18\x06 \x01(\x0b\x32\x11.tradingpb.Wallet\x12\r\n\x05title\x18\x07 \x01(\t\x12\x16\n\x0emainAssetIndex\x18\x08 \x01(\x05\x12\x12\n\nindicators\x18\t \x03(\t\x12\x0e\n\x06offset\x18\n \x01(\x05\"5\n\x0fTradingNodeInfo\x12\x10\n\x08maxTasks\x18\x01 \x01(\x05\x12\x10\n\x08\x63urTasks\x18\x02 \x01(\x05\"~\n\x13SimTradingCacheNode\x12+\n\x06params\x18\x01 \x01(\x0b\x32\x1b.tradingpb.SimTradingParams\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\x0e\n\x06lastTs\x18\x03 \x01(\x03\x12\x10\n\x04hash\x18\x04 \x01(\tB\x02\x18\x01\x12\x0b\n\x03\x62uf\x18\x05 \x01(\x0c\"@\n\x0fSimTradingCache\x12-\n\x05nodes\x18\x01 \x03(\x0b\x32\x1e.tradingpb.SimTradingCacheNode*\x83\x01\n\x08\x43trlType\x12\r\n\tCTRL_INIT\x10\x00\x12\x0c\n\x08\x43TRL_BUY\x10\x01\x12\r\n\tCTRL_SELL\x10\x02\x12\x11\n\rCTRL_STOPLOSS\x10\x03\x12\x13\n\x0f\x43TRL_TAKEPROFIT\x10\x04\x12\x11\n\rCTRL_WITHDRAW\x10\x05\x12\x10\n\x0c\x43TRL_DEPOSIT\x10\x06*7\n\rIndicatorType\x12\x12\n\x0eIT_SINGLEVALUE\x10\x00\x12\x12\n\x0eIT_SINGLEPRICE\x10\x01*4\n\x0b\x41IPTimeType\x12\x11\n\rAIPTT_WEEKDAY\x10\x00\x12\x12\n\x0e\x41IPTT_MONTHDAY\x10\x01\x42(Z&github.com/zhs007/tradingdb2/tradingpbb\x06proto3'
+  serialized_pb=b'\n\x0etrading2.proto\x12\ttradingpb\"!\n\x10\x42\x61sicRequestData\x12\r\n\x05token\x18\x01 \x01(\t\"3\n\x05\x41sset\x12\x0e\n\x06market\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0c\n\x04tags\x18\x03 \x03(\t\"1\n\x11\x43trlNodeAssetInfo\x12\x0e\n\x06volume\x18\x01 \x01(\x01\x12\x0c\n\x04\x63ost\x18\x02 \x01(\x01\"\xed\x03\n\x08\x43trlNode\x12\n\n\x02ts\x18\x01 \x01(\x03\x12\x1d\n\x03src\x18\x02 \x01(\x0b\x32\x10.tradingpb.Asset\x12\x1d\n\x03\x64st\x18\x03 \x01(\x0b\x32\x10.tradingpb.Asset\x12\x11\n\tvolumeSrc\x18\x04 \x01(\x01\x12\x11\n\tvolumeDst\x18\x05 \x01(\x01\x12!\n\x04type\x18\x06 \x01(\x0e\x32\x13.tradingpb.CtrlType\x12\x0b\n\x03\x66\x65\x65\x18\x07 \x01(\x01\x12\x1b\n\x13\x61verageHoldingPrice\x18\x08 \x01(\x01\x12\x11\n\tsellPrice\x18\t \x01(\x01\x12\x17\n\x0f\x63trlConditionID\x18\n \x01(\x05\x12\x12\n\nstrategyID\x18\x0b \x01(\x05\x12\x12\n\ntotalMoney\x18\x0c \x01(\x01\x12\x11\n\tlastMoney\x18\r \x01(\x01\x12=\n\rmapAssetsInfo\x18\x0e \x03(\x0b\x32&.tradingpb.CtrlNode.MapAssetsInfoEntry\x12\x12\n\nmoneyParts\x18\x0f \x01(\x05\x12\x16\n\x0elastMoneyParts\x18\x10 \x01(\x05\x1aR\n\x12MapAssetsInfoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.tradingpb.CtrlNodeAssetInfo:\x02\x38\x01\"=\n\x0bWalletAsset\x12\x1f\n\x05\x61sset\x18\x01 \x01(\x0b\x32\x10.tradingpb.Asset\x12\r\n\x05value\x18\x02 \x01(\x01\"0\n\x06Wallet\x12&\n\x06\x61ssets\x18\x01 \x03(\x0b\x32\x16.tradingpb.WalletAsset\"\xde\x01\n\rCtrlCondition\x12\x15\n\tindicator\x18\x01 \x01(\tB\x02\x18\x01\x12\x0c\n\x04vals\x18\x02 \x03(\x02\x12\x11\n\toperators\x18\x03 \x03(\t\x12\x15\n\rcombCondition\x18\x04 \x01(\t\x12\x13\n\x07minvals\x18\x05 \x03(\x02\x42\x02\x18\x01\x12\x13\n\x07maxvals\x18\x06 \x03(\x02\x42\x02\x18\x01\x12\x13\n\x07offvals\x18\x07 \x03(\x02\x42\x02\x18\x01\x12\x0f\n\x07strVals\x18\x08 \x03(\t\x12\x0c\n\x04name\x18\t \x01(\t\x12\r\n\x05group\x18\n \x01(\x05\x12\x11\n\tint64Vals\x18\x0b \x03(\x02\")\n\rIndicatorData\x12\x0c\n\x04vals\x18\x01 \x03(\x02\x12\n\n\x02ts\x18\x02 \x01(\x03\"m\n\tIndicator\x12\x10\n\x08\x66ullname\x18\x01 \x01(\t\x12&\n\x04type\x18\x02 \x01(\x0e\x32\x18.tradingpb.IndicatorType\x12&\n\x04\x64\x61ta\x18\x03 \x03(\x0b\x32\x18.tradingpb.IndicatorData\"\xb3\x02\n\tBuyParams\x12\x15\n\tinitMoney\x18\x01 \x01(\x02\x42\x02\x18\x01\x12\x14\n\x08perMoney\x18\x02 \x01(\x02\x42\x02\x18\x01\x12\x0e\n\x06volume\x18\x03 \x01(\x02\x12\x10\n\x08\x61ipMoney\x18\x04 \x01(\x02\x12\x14\n\x0cperInitMoney\x18\x05 \x01(\x02\x12\x14\n\x0cperHandMoney\x18\x06 \x01(\x02\x12\x19\n\rperTotalMoney\x18\x07 \x01(\x02\x42\x02\x18\x01\x12\x11\n\tassetCode\x18\x08 \x01(\t\x12\x18\n\x10timeTypeBuyAsset\x18\t \x01(\t\x12\x10\n\x08tsOffBuy\x18\n \x01(\x03\x12\x14\n\x0ctypeBuyAsset\x18\x0b \x01(\t\x12\x12\n\nmoneyParts\x18\x0c \x01(\x05\x12\x11\n\tnextTimes\x18\r \x01(\x05\x12\x14\n\x0c\x64\x65positMoney\x18\x0e \x01(\x05\"\xbb\x01\n\nSellParams\x12\x0e\n\x06volume\x18\x01 \x01(\x02\x12\x11\n\tperVolume\x18\x02 \x01(\x02\x12\r\n\x05money\x18\x03 \x01(\x02\x12\x10\n\x08keepTime\x18\x04 \x01(\x03\x12\x11\n\tassetCode\x18\x05 \x01(\t\x12\x19\n\x11timeTypeSellAsset\x18\x06 \x01(\t\x12\x11\n\ttsOffSell\x18\x07 \x01(\x03\x12\x15\n\rtypeSellAsset\x18\x08 \x01(\t\x12\x11\n\tnextTimes\x18\t \x01(\x05\"K\n\x0eStopLossParams\x12\x14\n\x08\x64rawdown\x18\x01 \x01(\x02\x42\x02\x18\x01\x12\x10\n\x08isFinish\x18\x02 \x01(\x08\x12\x11\n\tperVolume\x18\x03 \x01(\x02\"q\n\x10TakeProfitParams\x12\x12\n\x06profit\x18\x01 \x01(\x02\x42\x02\x18\x01\x12\x10\n\x08isFinish\x18\x02 \x01(\x08\x12\x11\n\tperVolume\x18\x03 \x01(\x02\x12\x0e\n\x06giveTo\x18\x04 \x01(\t\x12\x14\n\x0cisOnlyProfit\x18\x05 \x01(\x08\"+\n\nInitParams\x12\r\n\x05money\x18\x01 \x01(\x02\x12\x0e\n\x06vomume\x18\x02 \x01(\x02\"M\n\tAIPParams\x12\r\n\x05money\x18\x01 \x01(\x02\x12$\n\x04type\x18\x02 \x01(\x0e\x32\x16.tradingpb.AIPTimeType\x12\x0b\n\x03\x64\x61y\x18\x03 \x01(\x05\"C\n\tFeeParams\x12\x12\n\npercentage\x18\x01 \x01(\x02\x12\x10\n\x08minLimit\x18\x02 \x01(\x02\x12\x10\n\x08maxLimit\x18\x03 \x01(\x02\"\xe8\x04\n\x08Strategy\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1f\n\x05\x61sset\x18\x02 \x01(\x0b\x32\x10.tradingpb.Asset\x12%\n\x03\x62uy\x18\x03 \x03(\x0b\x32\x18.tradingpb.CtrlCondition\x12&\n\x04sell\x18\x04 \x03(\x0b\x32\x18.tradingpb.CtrlCondition\x12*\n\x08stoploss\x18\x05 \x03(\x0b\x32\x18.tradingpb.CtrlCondition\x12,\n\ntakeprofit\x18\x06 \x03(\x0b\x32\x18.tradingpb.CtrlCondition\x12\'\n\tparamsBuy\x18\x07 \x01(\x0b\x32\x14.tradingpb.BuyParams\x12)\n\nparamsSell\x18\x08 \x01(\x0b\x32\x15.tradingpb.SellParams\x12\x31\n\x0eparamsStopLoss\x18\t \x01(\x0b\x32\x19.tradingpb.StopLossParams\x12\x35\n\x10paramsTakeProfit\x18\n \x01(\x0b\x32\x1b.tradingpb.TakeProfitParams\x12)\n\nparamsInit\x18\x0b \x01(\x0b\x32\x15.tradingpb.InitParams\x12\'\n\tparamsAIP\x18\x0c \x01(\x0b\x32\x14.tradingpb.AIPParams\x12\x11\n\x05title\x18\r \x01(\tB\x02\x18\x01\x12\x12\n\nindicators\x18\x0e \x03(\t\x12$\n\x06\x66\x65\x65\x42uy\x18\x0f \x01(\x0b\x32\x14.tradingpb.FeeParams\x12%\n\x07\x66\x65\x65Sell\x18\x10 \x01(\x0b\x32\x14.tradingpb.FeeParams\"\x88\x01\n\x0cPNLDataValue\x12\n\n\x02ts\x18\x01 \x01(\x03\x12\x10\n\x08perValue\x18\x02 \x01(\x02\x12\x0c\n\x04tags\x18\x03 \x03(\t\x12\x0c\n\x04\x63ost\x18\x04 \x01(\x02\x12\r\n\x05value\x18\x05 \x01(\x02\x12\x0e\n\x06volume\x18\x06 \x01(\x02\x12\r\n\x05price\x18\x07 \x01(\x02\x12\x10\n\x08\x64rawdown\x18\x08 \x01(\x02\"\xdf\x06\n\x0cPNLAssetData\x12\x1f\n\x05\x61sset\x18\x01 \x01(\x0b\x32\x10.tradingpb.Asset\x12\'\n\x06values\x18\x02 \x03(\x0b\x32\x17.tradingpb.PNLDataValue\x12\x13\n\x0bmaxDrawdown\x18\x03 \x01(\x02\x12\x1a\n\x12maxDrawdownStartTs\x18\x04 \x01(\x03\x12\x18\n\x10maxDrawdownEndTs\x18\x05 \x01(\x03\x12\x11\n\tmaxDrawup\x18\x06 \x01(\x02\x12\x18\n\x10maxDrawupStartTs\x18\x07 \x01(\x03\x12\x16\n\x0emaxDrawupEndTs\x18\x08 \x01(\x03\x12\x0e\n\x06sharpe\x18\t \x01(\x02\x12\x19\n\x11\x61nnualizedReturns\x18\n \x01(\x02\x12\x1c\n\x14\x61nnualizedVolatility\x18\x0b \x01(\x02\x12\x14\n\x0ctotalReturns\x18\x0c \x01(\x02\x12\x10\n\x08variance\x18\r \x01(\x02\x12\x10\n\x08\x62uyTimes\x18\x0e \x01(\x05\x12\x11\n\tsellTimes\x18\x0f \x01(\x05\x12\x15\n\rstoplossTimes\x18\x10 \x01(\x05\x12\x12\n\nmaxUpDayTs\x18\x11 \x01(\x03\x12\x13\n\x0bmaxPerUpDay\x18\x12 \x01(\x02\x12\x14\n\x0cmaxDownDayTs\x18\x13 \x01(\x03\x12\x15\n\rmaxPerDownDay\x18\x14 \x01(\x02\x12\x13\n\x0bmaxUpWeekTs\x18\x15 \x01(\x03\x12\x14\n\x0cmaxPerUpWeek\x18\x16 \x01(\x02\x12\x15\n\rmaxDownWeekTs\x18\x17 \x01(\x03\x12\x16\n\x0emaxPerDownWeek\x18\x18 \x01(\x02\x12\x14\n\x0cmaxUpMonthTs\x18\x19 \x01(\x03\x12\x15\n\rmaxPerUpMonth\x18\x1a \x01(\x02\x12\x16\n\x0emaxDownMonthTs\x18\x1b \x01(\x03\x12\x17\n\x0fmaxPerDownMonth\x18\x1c \x01(\x02\x12\x13\n\x0bmaxUpYearTs\x18\x1d \x01(\x03\x12\x14\n\x0cmaxPerUpYear\x18\x1e \x01(\x02\x12\x15\n\rmaxDownYearTs\x18\x1f \x01(\x03\x12\x16\n\x0emaxPerDownYear\x18  \x01(\x02\x12$\n\x07lstCtrl\x18! \x03(\x0b\x32\x13.tradingpb.CtrlNode\x12(\n\nindicators\x18\" \x03(\x0b\x32\x14.tradingpb.Indicator\x12\x10\n\x08winTimes\x18# \x01(\x05\"\x93\x01\n\x07PNLData\x12\x10\n\x04name\x18\x01 \x01(\tB\x02\x18\x01\x12&\n\x05total\x18\x02 \x01(\x0b\x32\x17.tradingpb.PNLAssetData\x12+\n\x06\x61ssets\x18\x03 \x03(\x0b\x32\x17.tradingpb.PNLAssetDataB\x02\x18\x01\x12\r\n\x05title\x18\x04 \x01(\t\x12\x12\n\x06lastts\x18\x05 \x01(\x03\x42\x02\x18\x01\"\x94\x02\n\x10SimTradingParams\x12 \n\x06\x61ssets\x18\x01 \x03(\x0b\x32\x10.tradingpb.Asset\x12\'\n\tbaselines\x18\x02 \x03(\x0b\x32\x10.tradingpb.AssetB\x02\x18\x01\x12\x0f\n\x07startTs\x18\x03 \x01(\x03\x12\r\n\x05\x65ndTs\x18\x04 \x01(\x03\x12\'\n\nstrategies\x18\x05 \x03(\x0b\x32\x13.tradingpb.Strategy\x12!\n\x06wallet\x18\x06 \x01(\x0b\x32\x11.tradingpb.Wallet\x12\r\n\x05title\x18\x07 \x01(\t\x12\x16\n\x0emainAssetIndex\x18\x08 \x01(\x05\x12\x12\n\nindicators\x18\t \x03(\t\x12\x0e\n\x06offset\x18\n \x01(\x05\"5\n\x0fTradingNodeInfo\x12\x10\n\x08maxTasks\x18\x01 \x01(\x05\x12\x10\n\x08\x63urTasks\x18\x02 \x01(\x05\"~\n\x13SimTradingCacheNode\x12+\n\x06params\x18\x01 \x01(\x0b\x32\x1b.tradingpb.SimTradingParams\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\x0e\n\x06lastTs\x18\x03 \x01(\x03\x12\x10\n\x04hash\x18\x04 \x01(\tB\x02\x18\x01\x12\x0b\n\x03\x62uf\x18\x05 \x01(\x0c\"@\n\x0fSimTradingCache\x12-\n\x05nodes\x18\x01 \x03(\x0b\x32\x1e.tradingpb.SimTradingCacheNode*\x83\x01\n\x08\x43trlType\x12\r\n\tCTRL_INIT\x10\x00\x12\x0c\n\x08\x43TRL_BUY\x10\x01\x12\r\n\tCTRL_SELL\x10\x02\x12\x11\n\rCTRL_STOPLOSS\x10\x03\x12\x13\n\x0f\x43TRL_TAKEPROFIT\x10\x04\x12\x11\n\rCTRL_WITHDRAW\x10\x05\x12\x10\n\x0c\x43TRL_DEPOSIT\x10\x06*7\n\rIndicatorType\x12\x12\n\x0eIT_SINGLEVALUE\x10\x00\x12\x12\n\x0eIT_SINGLEPRICE\x10\x01*4\n\x0b\x41IPTimeType\x12\x11\n\rAIPTT_WEEKDAY\x10\x00\x12\x12\n\x0e\x41IPTT_MONTHDAY\x10\x01\x42(Z&github.com/zhs007/tradingdb2/tradingpbb\x06proto3'
 )
 
 _CTRLTYPE = _descriptor.EnumDescriptor(
@@ -68,8 +68,8 @@ _CTRLTYPE = _descriptor.EnumDescriptor(
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=4325,
-  serialized_end=4456,
+  serialized_start=4344,
+  serialized_end=4475,
 )
 _sym_db.RegisterEnumDescriptor(_CTRLTYPE)
 
@@ -94,8 +94,8 @@ _INDICATORTYPE = _descriptor.EnumDescriptor(
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=4458,
-  serialized_end=4513,
+  serialized_start=4477,
+  serialized_end=4532,
 )
 _sym_db.RegisterEnumDescriptor(_INDICATORTYPE)
 
@@ -120,8 +120,8 @@ _AIPTIMETYPE = _descriptor.EnumDescriptor(
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=4515,
-  serialized_end=4567,
+  serialized_start=4534,
+  serialized_end=4586,
 )
 _sym_db.RegisterEnumDescriptor(_AIPTIMETYPE)
 
@@ -1332,6 +1332,13 @@ _PNLDATAVALUE = _descriptor.Descriptor(
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='drawdown', full_name='tradingpb.PNLDataValue.drawdown', index=7,
+      number=8, type=2, cpp_type=6, label=1,
+      has_default_value=False, default_value=float(0),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
   ],
   extensions=[
   ],
@@ -1344,8 +1351,8 @@ _PNLDATAVALUE = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2660,
-  serialized_end=2778,
+  serialized_start=2661,
+  serialized_end=2797,
 )
 
 
@@ -1614,8 +1621,8 @@ _PNLASSETDATA = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2781,
-  serialized_end=3644,
+  serialized_start=2800,
+  serialized_end=3663,
 )
 
 
@@ -1674,8 +1681,8 @@ _PNLDATA = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3647,
-  serialized_end=3794,
+  serialized_start=3666,
+  serialized_end=3813,
 )
 
 
@@ -1769,8 +1776,8 @@ _SIMTRADINGPARAMS = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=3797,
-  serialized_end=4073,
+  serialized_start=3816,
+  serialized_end=4092,
 )
 
 
@@ -1808,8 +1815,8 @@ _TRADINGNODEINFO = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4075,
-  serialized_end=4128,
+  serialized_start=4094,
+  serialized_end=4147,
 )
 
 
@@ -1868,8 +1875,8 @@ _SIMTRADINGCACHENODE = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4130,
-  serialized_end=4256,
+  serialized_start=4149,
+  serialized_end=4275,
 )
 
 
@@ -1900,8 +1907,8 @@ _SIMTRADINGCACHE = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=4258,
-  serialized_end=4322,
+  serialized_start=4277,
+  serialized_end=4341,
 )
 
 _CTRLNODE_MAPASSETSINFOENTRY.fields_by_name['value'].message_type = _CTRLNODEASSETINFO


Test patch:
diff --git a/trdb2py/pdutils_test.py b/trdb2py/pdutils_test.py
new file mode 100644
index 0000000..2979c22
--- /dev/null
+++ b/trdb2py/pdutils_test.py
@@ -0,0 +1,27 @@
+# -*- coding:utf-8 -*-
+import pytest
+import trdb2py.trading2_pb2
+from datetime import datetime
+from trdb2py.timeutils import str2timestamp, getDayInYear, getYearDays
+from trdb2py.pdutils import (buildPNLReport, getPNLLastTs, getPNLValueWithTimestamp, mergePNL, mergePNLEx,
+                             rmPNLValuesWithTimestamp, getPNLTimestampLowInMonth, getPNLTimestampHighInMonth,
+                             countTradingDays4Year, calcAnnualizedVolatility, rebuildPNL)
+
+
+def test_countTradingDays4Year():
+    pnl = trdb2py.trading2_pb2.PNLAssetData()
+    arr = []
+
+    ts = int(trdb2py.str2timestamp('2013-05-01', '%Y-%m-%d'))
+    for i in range(0, 480):
+        dt = datetime.utcfromtimestamp(ts)
+
+        if dt.weekday() >= 0 and dt.weekday() < 5:
+            pnl.values.append(trdb2py.trading2_pb2.PNLDataValue(ts=ts))
+
+        ts += 60 * 60 * 24
+
+    pnl.values.extend(arr)
+
+    assert len(pnl.values) == 343
+    assert countTradingDays4Year(pnl) == 261
diff --git a/trdb2py/timeutils_test.py b/trdb2py/timeutils_test.py
new file mode 100644
index 0000000..bc51df3
--- /dev/null
+++ b/trdb2py/timeutils_test.py
@@ -0,0 +1,17 @@
+# -*- coding:utf-8 -*-
+import pytest
+import trdb2py.trading2_pb2
+from datetime import datetime
+from trdb2py.timeutils import str2timestamp, getDayInYear, getYearDays
+
+
+def test_getYearDays():
+    assert getYearDays(2019) == 365
+    assert getYearDays(2020) == 366
+    assert getYearDays(2000) == 366
+    assert getYearDays(1900) == 365
+
+def test_getDayInYear():
+    assert getDayInYear(2021, 1, 1) == 1
+    assert getDayInYear(2021, 1, 31) == 31
+    assert getDayInYear(2021, 2, 28) == 31 + 28