絕大多數的理財投資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()