(原創)基金定投的一種實現

絕大多數的理財投資app都會提供一個定投的功能,定投就是定期定額投資指定標的。如此推薦當然是因為他的優點很多,同時受理方也相對會獲得更多的存量資金,算是雙贏,只是周期較長,對于不懂理財的人來說可能就跟普通人去跑一萬米一樣長。
以基金為例,一般基金的交易平臺除了申購/贖回的接口,也會提供定投的接口,只是調用第三方接口的話,一方面提高了耦合性,另一方面為了優雅地向用戶展示定投相關信息,就需要付出額外的工作,因此我們需要DIY,然后按時調用買入的接口完成投資即可。

一、定投的數據模型

  • 首先,從定投的定義上來考慮:定期定額買入指定的投資標的
    1)我們需要知道是誰(user_id)
    2)定期,又分為按日、按周、按月(cycle_unit,jyrq)
    3)定額(apply_num)
    4 ) 投資標的,要考慮擴展性,可買的不僅僅是單個基金(invest_flag,invest_code)
    5)一般理財app都支持綁定多張卡,需要用戶指定從哪張卡里扣款(trade_acoo)

  • 其次,要從定投執行過程方面來設計
    6)考慮到用戶會出現資金緊張,可以允許順延,但有最大天數限制,超出則判定失敗,如果連續多次失敗則認定用戶已放棄定投,自動停止(delay_day,delay_count,fail_count)
    7)用戶可能想知道下一次扣款會在哪天,同時考慮到順延等狀況,需要更新當前的扣款日期以便執行(next_kkdate,cur_kkdate)
    8 )累積的投資金額和成功次數(total_sum,count)
    9 )標記定投的狀態,激活的or被終止了(is_active,soft_del)

  • 最后,還要考慮定投結果的展示
    9)定投結果要關聯定投、標記結果狀態--成功/失?。╝ip_id, state)
    10)如果成功還需要關聯交易訂單、交易金額、交易日期(order_id,apply_sum,trade_date)

綜上,在models.py中定義如下

class Aip(Document):
    '''
    自動投資計劃:Automatic investment plan
    '''
    meta = {'db_alias': 'test', 'indexes': [略]}
    user_id = IntField(required=True)
    invest_flag = StringField(required=True)
    invest_code = StringField(required=True)
    apply_sum = StringField(required=True)
    trade_acco = StringField(required=True)
    cycle_unit = StringField(required=True)
    jyrq = StringField(required=True)  
    delay_day = IntField(default=2)
    next_kkdate = DateTimeField()    # 考慮順延和工作日
    cur_kkdate = DateTimeField()      # 不考慮順延和工作日,用于連續循環更新
    total_sum = IntField(default=0)
    count = IntField(default=0)
    delay_count = IntField(default=0)
    fail_count = IntField(default=0)
    is_active = BooleanField(default=True)
    soft_del = BooleanField(default=False)
    created = DateTimeField(default=datetime.datetime.now)

    # 扣款日期描述
    @property
    def kkdate_desc(self):
        if self.cycle_unit == '0':
            desc = '每月' + str(int(self.jyrq)) + '日'
        elif self.cycle_unit == '1':
            desc = '每周' + WEEKDAY_DICT[int(self.jyrq)]
        elif self.cycle_unit == '2':
            desc = '每天'
        else:
            desc = ''
        return desc

class Aiphis(Document):
    meta = {'db_alias': 'test', 'indexes': [略)]}

    aip_id = StringField(required=True)
    state = StringField(required=True)
    order_id = StringField()           # 關聯交易訂單
    apply_sum = StringField()
    trade_date = DateTimeField()
    created = DateTimeField(default=datetime.datetime.now)

二、用戶的交互

與用戶的交互當然是前端操作,但后端需要考慮到操作需求,以便提供足夠豐富的接口,最基本的不外乎對于Aip的增刪改查和對Aiphis的查,Aiphis的增是在執行中調用。
1)create_aip # 創建
2)query_aip_detail # 查詢單個aip詳情,調用get_aiphis_list獲取對應的定投歷史
3)query_aip_list # 查詢用戶名下所有的定投計劃
4)update_aip # 更新、包括修改日期、金額、標的,暫停、激活、終止、重啟
5)create_aiphis # 定投執行時,不論成敗均會創建一條記錄
6)get_aiphis_list # 獲取定投歷史記錄

三、定投的執行

每日執行定投的任務腳本
1、只有扣款日期next_kkdate == today才會執行
2、定投成功,創建成功的Aiphis --- 5
3、余額不足則順延,

  • 1)日定投不順眼直接判定失敗,創建失敗的Aiphis --- 5
  • 2)順延次數+1,達到次數限制則判定失敗,aip的順延次數清零,創建失敗的Aiphis
  • 3)其余定投方式要將next_kkdate順延至下一個交易日,并創建Aiphis

4、定投失敗,同時要創建失敗的Aiphis --- 5
5、創建Aiphis

  • 1)失敗,要更新失敗次數 --- 6
  • 2)成功,關聯當前的order_id,更新aip的total_sum和count,并清空aip的順延和失敗次數
  • 3)不論成功失敗,都需要更新下一次扣款日期 --- 7

6、更新失失敗次數,+1,達到上限則終止定投計劃
8、更新下一次扣款日期(定投日期)

  • 1)首先根據cur_kkdate計算下一個周期的日期next_day(不一定是交易日)
  • 2)其次根據next_day尋找最近的一個交易日next_tradeday,包括next_day自身
  • 3)如果是按天定投,則cur_kkdate = next_kkdate = next_tradeday;否則,cur_kkdate = next_day,next_kkdate = next_tradeday。

9、考慮到會出現第三方買入接口超時導致結果不確定的情況,應當當時的order_id,創建Aiphis并標記為“未知”,然后第二天進行同步檢查 --- 0
0、 每天執行前,需要同步檢查前一日標記為“未知”的定投記錄,然后根據成功、失敗、未知分別處理。

最后貼上update_next_kkdate的代碼,并推薦一個處理時間間隔的第三方庫dateutil.relativedelta,讓你在處理時間的時候擺脫邊界問題的困擾。

# 按照Pep8的要求分塊引入內建、第三方、自己實現的庫
import datetime

from dateutil.relativedelta import relativedelta

from util.date import TradeDay        # 自定義抓取的交易日期記錄


def update_next_kkdate(aip):
    aip = aip if hasattr(aip, 'id') else Aip.get(id=aip)
    td = aip.cur_kkdate
    if aip.cycle_unit == '0':
        nd = td + relativedelta(months=1)      # 月
    elif aip.cycle_unit == '1':
        nd = td + relativedelta(weeks=1)        # 周
    elif aip.cycle_unit == '2':
        nd = td + relativedelta(days=1)          # 日
    else:
        nd = td
    # 最近的一個交易日
    tradeday = TradeDay.objects(
        trade_day__gte=int(nd.strftime("%Y%m%d"))).first()
    aip.next_kkdate = datetime.datetime.strptime(
        str(tradeday.trade_day), "%Y%m%d")

    if aip.cycle_unit == '2':
        aip.cur_kkdate = aip.next_kkdate
    else:
        aip.cur_kkdate = nd
    aip.save()
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容