第三章 关键技术--数据总线

一、概述

Databus负责从各种异构数据源向目标数据仓库进行数据转化,这个过程是根据不同的实效性来完成的。在构建数据仓库的过程中,Databus扮演着至关重要的角色,可以被视为构建数据仓库的第一步。

如果我们将数据仓库的模型设计比喻为大厦的设计蓝图,那么数据就是砖瓦。而Databus则负责将这些砖瓦按照设计蓝图的要求,构建成坚实的大厦。换句话说,Databus就是实现数据仓库模型设计的过程。

在具体的工作流程中,Databus首先从各种异构数据源中获取原始数据。这些数据源可能是关系型数据库、非关系型数据库、文件系统、Web API等。然后,Databus根据预设的规则和映射关系,将这些原始数据进行清洗、转换和聚合,使其符合目标数据仓库的模型设计。

根据不同的实效性要求,Databus可以按照不同的频率和方式进行数据转化。例如,对于实时数据源,Databus可以实时地捕获变化并立即将其应用到目标数据仓库中。对于批处理数据源,Databus可以按照预设的时间周期(如每天、每周或每月)进行批量转化。

在这个过程中,Databus还可以对数据进行一系列的优化和调整。例如,它可以自动处理重复的数据行、填充缺失值、对数据进行排序和去重等。此外,Databus还可以根据预设的规则进行数据筛选和过滤,只将符合条件的数据应用到目标数据仓库中。

通过这个过程,我们可以将各种异构数据源的数据整合到目标数据仓库中,实现数据的集中管理和统一访问。这样一来,我们就可以更好地利用这些数据进行业务分析和决策支持,推动业务的发展和创新。

二、技术架构

Databus的总体架构,它包括四个关键模块:Web控制台、调度中心、后台任务和监控。

  1. Web控制台:用户几乎所有的操作都围绕数据任务流程在Web控制台上进行。这包括流程的开发、测试、发布和运维等。Web控制台提供了一个直观和易于使用的界面,使用户能够轻松地管理和监控他们的数据任务。

  2. 调度中心:这个模块负责接收Web控制台的所有操作,并将其转化为实际的命令。然后,调度系统根据不同的指令生成不同的流程节点。这些节点可能是简单的同步型任务,也可能是周期性运行的任务,甚至可能是长时间运行的任务(如初始化全量股票数据)。调度中心还负责根据不同的实效性需求进行任务调度。实效性参数是指每隔多长时间调度一次任务。任务类型包括批任务与长任务。开发者通过配置任务实效参数和任务类型后,调度模块会启动不同类型的任务。同时,调度中心还提供命令行界面供查看和操作任务运行状态。

  3. 后台任务:这些是通过调度中心生成的job,主要为做全量和增量计算而生成的进程job。后台任务负责数据的转化,将数据从各种异构数据源转化为数据仓库内的结构化数据。

  4. 监控:最后,全量和增量job会生成相应的metrics,以便进行监控和告警。监控模块负责监控每个任务的运行情况和数据仓库内的数据正确性,以便及时发现问题并进行告警通知。这有助于确保数据的准确性和完整性,同时也能帮助开发者更好地理解任务的运行状态和性能表现。

综上所述,Databus的总体架构设计使得用户能够通过Web控制台灵活地管理和监控他们的数据任务。调度中心根据不同的需求生成和调度不同类型的任务。后台任务则负责从各种数据源中提取和转换数据。最后,监控模块确保了任务的正常运行和数据的准确性。这种架构使得Databus能够满足不同的实效性需求,并提供了一个高效、可靠的方式来构建和管理数据仓库。

三、模块简单实现

以下展示的模块实现代码较为简洁,主要目的是为了演示核心功能。

代码结构

.
├── bin
│   ├── __init__.py
│   ├── daily_job.bat       -- 调度器启动
│   ├── daily_job.conf      -- 调度作业配置
├── common                    -- 工具包及常量类
│   ├── ConfigUtils.py
│   ├── Constants.py
│   ├── Utils.py
│   ├── __init__.py
├── conf
│   ├── system.conf          -- 系统配置文件,配置全局参数及mysql参数等
├── service
│   ├── FundaService.py      -- 基本面服务,用于获得财务数据,如:营收、净利润等
│   ├── FutuService.py       -- 富图牛牛api接口,用于获得股票数据,如:close价格等
│   ├── MacroService.py      -- 宏观服务,用于获得宏观数据如:通胀、利率、M2等
│   ├── PoolStockService.py  -- 股票池服务,系统维护的股票列表及相关参数
│   ├── TradeDayService.py   -- 交易日服务,基于富图api获得的交易日列表
│   ├── DHandler.py          -- 获得日线数据,调用富图api,只是简单封装
│   ├── WHandler.py          -- 获得周线数据,同上
│   ├── __init__.py
├── SchDailyMain.py           -- 调度器
├── LaunchParam.py            -- 启动前的全局参数设置
├── OfflineJob.py            -- 离线作业,内部具有多个周期子作业
├── RealtimeJob.py           -- 实时作业,准实时获得股票数据
├── __init__.py

调度器

该模块由三个文件组成,包括启动文件、任务配置文件以及任务调度代码。任务调度代码会读取配置文件内容,并将其解析为几种类型的任务,然后启动这些任务。这种设计使得系统具有较高的灵活性和可扩展性,可以轻松地添加、修改或删除任务类型,而无需修改代码本身。

  • daily_job.bat

@echo off
rem @echo off
echo "APP_WORK_HOME=%APP_WORK_HOME%  daily_job.bat"
set APP_WORK_HOME=%cd%\..\
D:\soft\Anaconda2\python %APP_WORK_HOME%\SchDailyMain.py daily_job.conf >> d:\home\logs\daily_job.out
rem pause
  • daily_job.conf

#每天运行一次获得最近10条交易日
[trading_days]
cron=-i 1 -h 09 -m 10 -t CN
cmd=OfflineJob.py -t trade_day

# 30分钟一次获得标普500股票最新1条数据
[spx500]
cron=-i 2 -m 30 -t US -s 09:00 -e 16:40
cmd=Offline.py -t us_realtime_data -m US -f SPX_code.txt -l 1

# 1小时获得一次最近1条财报数据
[funda_us]
cron=-i 3 -h 01 -m 01 -t US -s 01:00 -e 23:00
cmd=OfflineJob.py -t funda -s income,cashflow,balance -l 1

# 准获得实时股票数据
[futu_us]
cron=-i 4
cmd=RealtimeJob.py -t futu_stock -m US -e online
  • SchDailyMain.py

# apscheduler调度器
scheduler = BackgroundScheduler()
#logger设置
formatter = logging.Formatter("%(asctime)s - %(thread)d - %(name)s.%(funcName)s:%(lineno)s %(levelname)s - %(message)s")
logging.basicConfig(level=logging.INFO)
# logging root新建console handler
console = logging.StreamHandler(stream=sys.stdout)
console.setLevel(logging.INFO)
console.setFormatter(formatter)
logging.getLogger('apscheduler.scheduler').addHandler(console)
logging.getLogger('apscheduler.executors.default').addHandler(console)
logger_sch = logging.getLogger('SchDailyMain')
logger_sch.addHandler(console)

#调度方法
def tick_delay(task_file_name, section, market_type, path):
    p = subprocess.Popen(path, shell=True)
    logger_sch.info('app end ' + task_file_name + ',section=' + section)
    p.wait()

if __name__ == '__main__':
    if os.environ.get('APP_WORK_HOME') is None:
        os.environ['APP_WORK_HOME'] = os.getcwd() + os.sep
    LaunchParam.initAppParam()
    # 获得参数
    param_list = sys.argv[1:]
    if len(param_list) == 0:
        param_list = ['daily_task.conf']

    task_file = param_list[0]
    print('##### schedule ' + task_file + ' file #####')
    # 配置加载
    task_file_path = ConfigUtils._global_workdir + os.sep + 'bin' + os.sep + task_file
    if os.path.exists(task_file_path) is False:
        print(task_file_path + ' is not exist.')
    task_conf_file = ConfigParser.ConfigParser()
    task_conf_file.read(task_file_path)

    #解析每块配置参数
    for section in task_conf_file.sections():
        cron_param = task_conf_file.get(section, "cron")
        cmd_param = task_conf_file.get(section, "cmd")
        opts, args = getopt.getopt(cron_param.split(' '), "i:h:m:s:e:t:")
        i, h, m, s, e, t = None, None, None, None, None, None, None
        for op, value in opts:
            if op == '-i':
                i = value
            elif op == '-h':
                h = value
            elif op == '-m':
                m = value
            elif op == '-s':
                start = value
            elif op == '-e':
                end = value
            elif op == '-t':
                market_type = value
        # cmd
        cmd = 'D:\soft\Anaconda2\python %s%s' % (ConfigUtils._global_workdir + os.sep, cmd_param)
        # 向调度器增加任务
        if i == '1':  # 每日定时
            logger_sch.info('cron task_type=1 ' + section + ' ...')
            scheduler.add_job(tick, trigger='cron', hour=h, minute=m, args=[task_file, section, market_type, cmd])
        elif i == '2':  # 分钟循环调度
            logger_sch.info('interval minute task_type=2 ' + section + ' ...')
            scheduler.add_job(interval_minute_tick, trigger='cron', minute='*/' + m,
                              args=[task_file, section, market_type, cmd, start, end])
        elif i == '3':  # 小时循环调度
            logger_sch.info('interval hour taskType=4 ' + section + ' ...')
            scheduler.add_job(interval_hour_tick, 'interval', hours=int(h), minutes=int(m),
                              args=[task_file, section, market_type, start, end])
        elif i == '4':  # 10秒后延迟运行一次
            logger_sch.info('run task task_type=3 ' + section + ' ...')
            a_dt = datetime.now() + timedelta(seconds=5)
            scheduler.add_job(tick_delay, trigger='date', run_date=a_dt, args=[task_file, section, market_type, cmd])
    # 启动调度器,执行任务
    try:
        scheduler.start()
        logger_sch.info("SchDailyMain scheduler start...")
    except (KeyboardInterrupt, SystemExit):
        scheduler.shutdown()
        logger_sch.error("SchDailyMain scheduler exception...")
    while True:  # 主进程阻塞
        time.sleep(999999)

任务

所有Task文件的代码在接收参数方面都遵循类似的模式,首先会初始化应用程序的根目录,然后解析传递过来的参数,最后执行具体的业务逻辑。以下以RealtimeJob.py为例进行说明:

在RealtimeJob.py文件中,首先会设置应用程序的根目录路径,以确保后续文件和资源的正确引用。然后,它会解析传递过来的参数,这些参数可以包括任务类型、任务配置等,以便在后续的逻辑中加以使用。最后,根据解析得到的参数执行具体的业务逻辑。这种设计使得任务文件的代码结构清晰、易于维护和扩展。

import AppContext
import LaunchParam
from springpython.context import ApplicationContext
from util import LogUtils

if __name__ == '__main__':
    if os.environ.get('APP_WORK_HOME') is None:
        os.environ['APP_WORK_HOME'] = os.getcwd()
    LaunchParam.initAppParam()
    # 获得参数
    list = sys.argv[1:]
    if len(list) == 0:
        list = ['-t', 'futu_stock', '-m', 'US', '-e', 'test']
        # list = ['-t', 'futu_stock', '-m', 'US', '-e', 'online']

    # check参数
    opts, args = getopt.getopt(list, "t:m:e:z:")
    task_name, market_type, env, size = None, None, 'test', None
    for op, value in opts:
        if op == '-t':
            task_name = value
        elif op == '-m':
            market_type = value
        elif op == '-e':
            env = value
        elif op == '-z':
            size = value

    # 参数非空判断
    if task_name == '':
        print('task_name is empty.')
        os._exit(-1)
    if market_type == '':
        print('market_type is empty.')
        os._exit(-1)
    if size is None:
        size = 2

    LogUtils.confLogging('RealtimeTask_' + task_name + '_' + market_type + '.log')
    logger = logging.getLogger("RealtimeTask")
    logger.info('##### RealtimeTask ' + task_name + '_' + market_type + ' start #####')

    if task_name.strip().startswith('futu_stock'):
        if market_type == 'US':
            america_tz = timezone('America/New_York')
        else:
            america_tz = timezone('Asia/Shanghai')
        # 初始化对象容器,相当于java spring ioc container
        appContext = ApplicationContext(AppContext.AppContext())
        # 初始化futu api
        newFutuService = appContext.get_object('newFutuService')
        newFutuService.init()
        # 初始化交易日service
        tradeDayService = appContext.get_object('tradeService')
        while True:
            now_dt = datetime.now(tz=america_tz)
            curDate = now_dt.strftime('%Y-%m-%d')
            curHM = now_dt.strftime('%H:%M')
            # 获得交易日,非交易日,不调用接口
            if market_type == 'SH' or market_type == 'SZ':
                tradeDay = tradeDayService.get_trade_day('CN', curDate)
            else:
                tradeDay = tradeDayService.get_trade_day(market_type, curDate)
            planStartHM = '09:00'
            if market_type == 'SH' or market_type == 'SZ':
                endHM = '15:10'
            else:
                endHM = '16:10'

            if env != 'test':
                if tradeDay is None:
                    logger.info('RealtimeTask ' + market_type.lower() + ' market is not open,sleep half hour.')
                    time.sleep(1800)
                    continue

            logger.info('start futu get data, param is market_type=' + market_type + ',am curDate=' + curDate + ',curHM=' + curHM)
            # 判断是否在开盘时间范围内或者调试环境
            while (curHM >= planStartHM and curHM < endHM) or env == 'test':
                try:
                    # 调用futu api获得数据
                    logger.info('==futu start, params market_type=' + market_type + ',curDate=' + curDate + ',curHM=' + curHM)
                    # 股票池服务,获得下载股票数据列表
                    poolStockService = appContext.get_object('poolStockService')
                    code_db_list, name_dict = poolStockService.findCodeByMarket(market_type)
                    dHander = appContext.get_object('dHander')
                    wHander = appContext.get_object('wHander')
                    dHander.get_cur_kline(market_type, code_db_list, name_dict, size) # 日K
                    wHander.get_cur_kline(market_type, code_db_list, name_dict, size) # 周K
                    logger.info('==futu success, params market_type=' + market_type + ',curDate=' + curDate + ',curHM=' + curHM)
                    # 重置时间
                    now_dt = datetime.now(tz=america_tz)
                    curDate = now_dt.strftime('%Y-%m-%d')
                    curHM = now_dt.strftime('%H:%M')
                    if env == 'test':
                        break
                    else:
                        time.sleep(random.randint(1, 3))  # futu api接口不能频繁调用
                except Exception as e:
                    logging.error('futu data ' + market_type + ' error, exception=' + traceback.format_exc())
                    time.sleep(5)
            logger.info('exit futu get data, params market_type=' + market_type + ',cn curDate=' + curDate + ',curHM=' + curHM)
            if env == 'test':
                break
            time.sleep(20 * 60)

主要逻辑代码

  • DHandler.py获得K线数据业务

# 调用futu api 获得天级最近kline data
def get_cur_kline(self, market_type, code_list=[], name_dict={}, num=5):
    for code in code_list:
        ucode = code.upper()
        cn_name = name_dict.get(ucode)
        if cn_name is None:
            cn_name = ''
        full_code_name = str(market_type + "." + ucode)
        try:
            # 调用富图api,先订阅
            self._newFutuService.ctx.subscribe([full_code_name], [SubType.K_DAY])
            # 获得最近日K线数据
            ret_code, ret_data = self._newFutuService.ctx.get_cur_kline(full_code_name, num=num, ktype='K_DAY')
            if ret_code != 0:
                self.logger.error("openfutu.get_cur_kline error,ret_code!=0, params code=" + full_code_name+', type=K_DAY')
                break
            # 解析返回值
            for index, row in ret_data.iterrows():  # 获取每行的index、row
                cycle = CycleData()
                cycle.market_type = market_type # 市场类型
                dt = row['time_key'].split(' ')[0] #交易日
                # 四价、成交量、成交金额不需要
                open = round(row['open'], 3)
                close = round(row['close'], 3)
                high = round(row['high'], 3)
                low = round(row['low'], 3)
                volume = row['volume']
                tur = 0
                #保存到数据库
                cycle.initDataDay(market_type, ucode, cn_name, dt, close, high, low, open, volume, tur)
                self._bizService.saveOrUpdateRecordByTable(cycle, 'day_record')
        except Exception as e:
            self.logger.error("openfutu.get_cur_kline K_DAY fail, code=%s, exception=%s" % (ucode, traceback.format_exc()))
    self.logger.info("dHandler get_cur_kline finish,market_type=" + market_type)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值