<file_sep>from datetime import datetime, timedelta
from enum import Enum

import requests
import json
import re
import time
import pandas as pd
import json
import numpy as np

class ProfitLossType(Enum):
    ACCOMPLISHED = 'accomplished'
    UNACCOMPLISHED = 'unaccomplished'
    
    
class VirtualStockAccount():

    def __init__(self, email, password, wait_time=1):

        """
        輸入帳號(email)密碼(password)來登入，並設定每個request的延遲時間是wait_time

        """

        self.ses = requests.Session()
        res = self.ses.get('https://www.cmoney.tw/member/login/')
        self.wait_time = wait_time

        # get view state
        viewstate = re.search( 'VIEWSTATE"\s+value=.*=', res.text )
        viewstate = viewstate.group()

        # get eventvalidation
        eventvalidation = re.search( 'EVENTVALIDATION"\s+value=.*\w', res.text )
        eventvalidation = eventvalidation.group( )

        time.sleep(self.wait_time)

        res = self.ses.post('https://www.cmoney.tw/member/login/', {
                '__VIEWSTATE': viewstate[18:],
            '__EVENTVALIDATION': eventvalidation[24:],
            'ctl00$ContentPlaceHolder1$account': email,
            'ctl00$ContentPlaceHolder1$pw': password,
            'ctl00$ContentPlaceHolder1$loginBtn': '登入',
            'ctl00$ContentPlaceHolder1$check': 'on',
        })

        time.sleep(self.wait_time)

        res = self.ses.get('https://www.cmoney.tw/vt/main-page.aspx')

        aids = re.findall('aid=[\d]*', res.text )
        print(aids)
        aids = [a.split('=')[1] for a in aids]
        aids = [a for a in aids if a.isdigit()]
        self.aids = aids
        if self.aids:
            self.aid = aids[0]
        else:
            raise Exception('Cannot open account due to incorrect account name or password')
        print('accounts', aids)
        print('current account', self.aid)

    def get_price(self, sid):

        """
          輸入sid='1101'，會拿到以下的資料：
           {'StockInfo': {'Commkey': '1101',
          'Name': '台泥',
          'MarketType': 2,
          'RefPrice': 42.9,
          'CeilPrice': 47.15,
          'FloorPrice': 38.65,
          'SalePrice': 43.1,
          'TotalQty': 8665,
          'DecimalPoint': 1,
          'LowPr': 42.6,
          'BuyPr1': 43.05,
          'SellPr1': 43.1,
          'BuyPr2': 43.0,
          'SellPr2': 43.15,
          'BuyPr3': 42.95,
          'SellPr3': 43.2,
          'BuyPr4': 42.9,
          'SellPr4': 43.25,
          'BuyPr5': 42.85,
          'SellPr5': 43.3,
          'BuyQty1': 73.0,
          'SellQty1': 43.0,
          'BuyQty2': 459.0,
          'SellQty2': 42.0,
          'BuyQty3': 111.0,
          'SellQty3': 178.0,
          'BuyQty4': 169.0,
          'SellQty4': 120.0,
          'BuyQty5': 46.0,
          'SellQty5': 230.0,
          'DealTime': '2018-08-22T11:40:53',
          'OrderTime': '2018-08-22T11:40:53'},
         'SalePrice': '43.1',
         'IsWarrant': False}
        """
        res = self.ses.get('https://www.cmoney.tw/vt/ashx/HandlerGetStockPrice.ashx', params={
            'q': sid,
            'accountType': 1,
            'isSDAndSell': 'false',
            })
        return json.loads(res.text)

    def entrust(self, sid, price, quantity, tradekind, type_):
        res = self.ses.get('https://www.cmoney.tw/vt/ashx/userset.ashx', params={
            'act': 'NewEntrust',
            'aid': self.aid,
            'stock': sid,
            'price': price,
            'ordqty': quantity,
            'tradekind': tradekind,
            'type': type_,
            'hasWarrant': 'true',
        })

    def buy(self, sid, quantity, price='漲停'):

        """
        購買兩張台泥股票:
        例如sid='1101', quantity=2
        假如沒有輸入price就是以當前價格買入
        假如輸入了price就是限價買入
        """

        self.entrust(sid, price, quantity, tradekind='c', type_='b')


    def sell(self, sid, quantity, price='跌停'):

        """
        賣出兩張台泥股票:
        例如sid='1101', quantity=2
        假如沒有輸入price就是以當前價格買入
        假如輸入了price就是限價買入
        """

        self.entrust(sid, price, quantity, 'c', 's')

    def sellshort(self, sid, quantity, price='跌停'):
        self.entrust(sid, price, quantity, 'sd', 's')

    def buytocover(self, sid, quantity, price='漲停'):
        self.entrust(sid, price, quantity, 'sd', 'b')

    def status(self):

        """
        查看目前account的狀態：
        [{'Id': '1101',
          'Name': '台泥',
          'TkT': '現股',
          'NowPr': '43.05',
          'DeAvgPr': '43.20',
          'IQty': '1',
          'IncomeLoss': '-402',
          'Ratio': '-0.93',
          'Cost': '43,200.00',
          'ShowCost': '43,200.00',
          'TodayQty': '0',
          'BoardLostSize': '1000',
          'IsWarrant': '0'}]
        """

        res = self.ses.get('https://www.cmoney.tw/vt/ashx/accountdata.ashx', params={
            'act': 'InventoryDetail',
            'aid': self.aid,
        })

        return json.loads(res.text)

    def listEntrust(self, f, s):

        for sid, q in s.items():
            if q == 0:
                continue
            print('new entrust', sid, q)

            f(sid.split()[0], abs(int(q)))
            time.sleep(self.wait_time)

    def get_orders(self):

        """拿到當下的委託"""

        res = self.ses.get('https://www.cmoney.tw/vt/ashx/accountdata.ashx', params={
            'act': 'EntrustQuery',
            'aid': self.aid,
        })

        return json.loads(res.text)

    def cancel_all_orders(self):

        """清空當下的委託"""

        orders = self.get_orders()
        time.sleep(self.wait_time)
        for o in orders:
            if o['CanDel'] == '1':
                print('Cancel order', o['Id'])
                res = self.ses.get('https://www.cmoney.tw/vt/ashx/accountdata.ashx', params={
                    'act': 'DeleteEntrust',
                    'aid': self.aid,
                    'GroupId': 0,
                    'OrdNo': o['CNo'],
                })
                time.sleep(self.wait_time)

    def rebalance(self, newlist):

        """
        將持股更新成newlist，newlist的結構可以參考buy_list的input，是一樣的。
        """

        newlist = pd.Series(newlist).astype(int)
        newlist.index = pd.Series(newlist.index).astype(str)

        status = self.status()
        if len(status):
            df = (pd.DataFrame(status)[['Id', 'IQty', 'NowPr', 'ShowCost', 'Ratio', 'TkT']]).sort_values('Id')
            df['Id'] = df['Id'].astype(str)
        else:
            df = pd.DataFrame(columns=['Id', 'IQty', 'NowPr', 'ShowCost', 'Ratio', 'TkT'])

        ## 現股
        def rebalance_type(tkt, newlist, long_entrust, short_entrust):

            df_c = df[df['TkT'] == tkt]
            oldlist_c = pd.Series(df_c['IQty'].values, index=df_c['Id'].values).astype(int)

            def calc_diff(new, old):
                new2 = new.reindex(old.index | new.index).fillna(0)
                old2 = old.reindex(old.index | new.index).fillna(0)
                return new2 - old2

            print("------- TEST S ---------")
            print(newlist)
            print("------- TEST E ---------")
            print(oldlist_c)
            print("----------------")

            diff_c = calc_diff(newlist, oldlist_c)
            """
            print("check diff -------------------------")
            print(newlist)
            print(oldlist_c)
            print(diff_c)
            print("check diff -------------------------")
            """
            print('------------------------------------------')
            print('operaetions', tkt)
            print("enter")
            print(diff_c[diff_c > 0])
            print("exit")
            print(diff_c[diff_c < 0])
            print('------------------------------------------')
            #input()
            self.listEntrust(long_entrust, diff_c[diff_c > 0])
            self.listEntrust(short_entrust, diff_c[diff_c < 0])

        rebalance_type('現股', newlist[newlist >= 0], self.buy, self.sell)
        rebalance_type('融券', -newlist[newlist <= 0], self.sellshort, self.buytocover)



    def info(self):

        res = self.ses.get('https://www.cmoney.tw/vt/ashx/accountdata.ashx?act=AccountInfo&aid=%s&_=1572343162391' % self.aid)


        account_info = json.loads(res.text)
        return account_info

    def calculate_weight(self, position, lowest_fee=20, discount=1, add_cost=10, short=False):

        # get total money
        account_info = self.info()
        money = int(account_info['AllAssets'].replace(',', ''))

        print('total money', money)

        # get price of the stock
        import time
        infos = {}
        if isinstance(position, pd.DataFrame):
            slist = position.iloc[-1][(position.iloc[-1] != 0)].index.tolist()
        elif isinstance(position, pd.Series):
            slist = position[(position != 0)].index.tolist()
        elif isinstance(position, list):
            slist = position
        else:
            raise Exception("type of position should be pd.Series or list")

        for sid in slist:
            print(sid)
            infos[sid] = self.get_price(sid) # RefPrice
            time.sleep(2)

        # format dataframe and series
        price = pd.Series({sid:info['StockInfo']['RefPrice'] for sid, info in infos.items() if info is not None})
        none_stock_ids = [sid for sid, info in infos.items() if info is None]
        if none_stock_ids:
            print('**WARRN: there are stock cannot find info', none_stock_ids)

        stock_list = price.dropna().transpose()

        # rebalance stocks
        while (money / len(stock_list)) < (lowest_fee - add_cost) * 1000 / 1.425 / discount:
            stock_list = stock_list.loc[stock_list != stock_list.max()]

        while True:
            invest_amount = (money / len(stock_list))
            ret = np.floor(invest_amount / stock_list / 1000)

            if (ret == 0).any():
                stock_list = stock_list.loc[stock_list != stock_list.max()]
            else:
                break

        if isinstance(short, bool) and short == True:
            ret = -ret
        elif isinstance(short, list) and short:
            for s in short:
                ret[s] = -ret[s]


        ret.to_csv('position' + str(self.aid) + '.csv')
        slist = ret.to_dict()
        return slist

    def sync(self, *arg1, short=False, **arg2):
        slist = self.calculate_weight(*arg1, short=short, **arg2)
        self.rebalance(slist)

    
    def profit_loss(self, profitLossType: ProfitLossType,
                    startTime: str = (datetime.now() - timedelta(days=180)).strftime("%Y-%m-%d"),
                    endTime: str = datetime.now().strftime("%Y-%m-%d")):

        """
       輸入損益類型(profitLossType)
       profitLossType => ProfitLossType.ACCOMPLISHED: "已實現損益"
       profitLossType => ProfitLossType.UNACCOMPLISHED: "未實現損益"
       """
        params = {
            'act': 'ProfitLoss',
            'aid': self.aid,
            'profitLossType': profitLossType.value
        }
        if profitLossType == ProfitLossType.ACCOMPLISHED:
            params["startTime"] = startTime
            params["endTime"] = endTime

        res = self.ses.get('https://www.cmoney.tw/vt/ashx/accountdata.ashx', params=params)
        return json.loads(res.text)
<file_sep>from .stock import VirtualStockAccount

__all__ = [
	"VirtualStockAccount"
]<file_sep># CMoney Stock Paper Trading API
### 為什麼要用 Paper Trading
Paper Trading 可以幫助我們更理解市場上投資的真實狀況，
顯示我們「假如」持有某種資產組合的報酬率，
我們可以自行設定投資金額，例如100萬，並且測試看看，自己精心研發的策略有沒有用

### 為什麼不用回測就好了？
簡單講，大部分寫Python的人，回測框架要達到非常嚴謹，是非常困難的，
所以通常我們都「大概測一測」，比較少考慮的部分有：
1. 胃納量（有些股票根本沒成交量，怎麼可能買很多張？）
2. 不小心使用到未來數據（財報、月營收）
3. 摩擦成本（手續費、證交稅）
4. 忘記把已下市股票給納入回測中
5. 太多了...以後再整理給大家

除了更精密的模擬外，Paper Trading還能夠

### 幫助投資人檢視策略的「微觀效果」
Paper Trading 可以讓你對於投資更有感覺，回測只是看長年下來的獲利績效，
雖然你可能覺得長期績效很好，但是這是個宏觀角度（好幾年），也就是最後得到的總報酬率，
然而我們真正投資的時候，是深處於微觀的世界（每天），會覺得每天的起伏都很劇烈！
有時候會讓你疑神疑鬼，想要調整策略，覺得這個策略可能失效了，
變得說常常在優化策略，但是都是無謂的優化
Paper Trading 另一個關鍵是，你要把它當成真的$（雖然很難），
這樣操作的時候，除了模擬獲利，也模擬自己面對報酬率的心態，

接下來我們就進行三個步驟來做 Paper Trading 吧！

### 1. 安裝

這個程式沒什麼特別的安裝方法，可以打開anaconda prompt 輸入以下指令
```
git clone https://github.com/koreal6803/CmoneyVirtualAccount.git

# mac 使用的指令
mv CmoneyVirtualAccount/cmoneyVirtualAccount ./cmoney

# windows 使用的指令
move CmoneyVirtualAccount/cmoneyVirtualAccount ./cmoney
```

### 2. 申請帳號

我們要到 Cmoney 的平台來申請帳號密碼：
https://www.cmoney.tw/vt/
這邊值得注意的是，假如你有用FB登入，請還是要設定一個密碼，這樣等等才可以使用 Python 操控 Cmoney 的模擬平台喔！(右上角使用者名稱->基本資料)

### 3. 用程式操控
然後就可以在此資料夾中，使用 Python 來做 Paper trading 囉！
```
from cmoney.stock import VirtualStockAccmount, ProfitLossType

# 登入
vs = VirtualStockAccount('your_account', 'your_password')

# 切換子帳號
vs.aid = '123456'

# ------ #
# 查看帳戶
# ------ # 

# 拿到某檔股票的資料：
vs.get_price('2330')

# 查看目前帳戶持有的股票
vs.status()

# 查看現有資金
vs.info()


# 查看目前未實現損益
vs.profit_loss(profitLossType=ProfitLossType.UNACCOMPLISHED)

# 查看目前已實現損益
# startTime default = 180 days ago
# endTime default = today
vs.profit_loss(profitLossType=ProfitLossType.ACCOMPLISHED, startTime= "2020-01-01", endTime="2020-06-31")

# ------ #
# 操作帳戶
# ------ # 

# 買一張台積電（強迫現價或開盤價買入）
vs.buy('2330', 1)


# 賣出一張台積電（強迫現價或開盤賣出）
vs.sell('2330', 1)

# ------ #
# 批次操作
# ------ # 

# 買一張台積電、一張聯發科
vs.buy({'2330': 1, '2454': 1})

# 出清所有股票
vs.sell_all()

# 將所有帳戶持股設定成：「一張台積電」、「一張聯發科」
# （會先賣出股票，並且買「一張台積電」、「一張聯發科」)
vs.rebalance({'2330': 1, '2454': 1})

# 自動分配持股（平均分散總資產）（通常我會用這個）
vs.sync(['1101', '2330'])

# ------ #
# 委託操作
# ------ # 

# 查看目前的委託單
vs.get_orders()

# 刪除所有委託單
vs.cancel_all_orders()

```

### 課程同學 Bonus!


假如有上「小資族選股策略」的同學，想要paper trade你的策略，配合最後的優等生策略，可以直接使用：

```
vs.sync(strategy(data))
```

來 paper trade 任何選股策略喔！

這支程式放在倉庫裡很久了，最近想要用又把它拿出來，想說修理一下分享給大家！
也歡迎大家來 push 新功能喔～（缺 unit test 呀XD）
