(原创)基金定投的一种实现

绝大多数的理财投资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()

你可能感兴趣的:((原创)基金定投的一种实现)