绝大多数的理财投资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)定投结果要关联定投、标记结果状态--成功/失败(aip_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()