夜雨聆风学习资料网

ARTICLE · 1048903

[创新技术阁][AiTrader]使用定时任务定时更新账户资产

[创新技术阁][AiTrader]使用定时任务定时更新账户资产

1. 新增账户信息

在新增账户信息的时候,我们需要考虑如下几点:

  • 判断今日是否已有记录,如果有则更新,没有则新增。
  • 如果是第一条记录,那么利润和总利润都是0;
  • profit (当日利润) = 今日总资产 - 上一条记录总资产 (若无记录则为0)。
  • total_profit (累计利润) = 上一条记录累计利润 + 今日利润。具体实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
async def create(self, data: QuantAccountCreateSchema) -> QuantAccountOutSchema:    """            创建或更新账户信息    逻辑:    1. 判断今日是否已有记录,如果有则更新,没有则新增。    2. 如果是第一条记录,那么利润和总利润都是0;    3. profit (当日利润) = 今日总资产 - 上一条记录总资产 (若无记录则为0)。    4. total_profit (累计利润) = 上一条记录累计利润 + 今日利润。    """    if isinstance(data.update_date, date):        today_date = data.update_date    elif isinstance(data.update_date, str):        try:            today_date = date.fromisoformat(data.update_date.split()[0].split("T")[0])        except Exception:            today_date = date.today()    else:        today_date = date.today()    crud = QuantAccountCRUD(self.auth, self.db)    # 查找今日之前最近的一条历史记录    stmt_last = (        select(QuantAccountModel)        .where(            QuantAccountModel.exchange_id == data.exchange_id,            QuantAccountModel.apikey_id == data.apikey_id,            QuantAccountModel.update_date < today_date,            QuantAccountModel.is_deleted == False,        )        .order_by(QuantAccountModel.update_date.desc(), QuantAccountModel.id.desc())        .limit(1)    )    last_record = (await self.db.execute(stmt_last)).scalars().first()    today_total = float(data.total_balance_usdt)    if last_record is None:        # 2. 如果是第一条记录,那么利润和总利润都是0        profit = 0.0        total_profit = 0.0    else:        # 3. profit (当日利润) = 今日总资产 - 上一条记录总资产 (若无记录则为0)        profit = round(today_total - float(last_record.total_balance_usdt), 2)        # 4. total_profit (累计利润) = 上一条记录累计利润 + 今日利润        total_profit = round(float(last_record.total_profit or 0.0) + profit, 2)    # 1. 判断今日是否已有记录,如果有则更新,没有则新增    today_obj = await crud.get(        exchange_id=data.exchange_id,        apikey_id=data.apikey_id,        update_date=today_date,    )    if today_obj:        update_data = data.model_dump(exclude_unset=True, exclude={"id"})        update_data["profit"] = profit        update_data["total_profit"] = total_profit        update_data["update_date"] = today_date        obj = await crud.update(id=today_obj.id, data=update_data)    else:        data.profit = profit        data.total_profit = total_profit        data.update_date = today_date        obj = await crud.create(data=data)    return QuantAccountOutSchema.model_validate(obj)

2.定时任务功能函数

定时更新账户的资产,我们需要先从数据库读取所有需要更新账户的apikey,然后循环根据apikey从交易所获取资产进行保存。

在app/plugin/module_quant/下新增一个job_func的包,然后新增一个文件sync_account.py来实现获取账户资产的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!/usr/bin/env python# encoding: utf-8# @version: v1.0# @author:  Kandy.Ye# @contact: Kandy.Ye@outlook.com# @file:    sync_account.py# @time:    2026/09/20 11:21from datetime import datetime, date, timedelta, timezoneimport ccxtfrom app.core.logger import loggerfrom app.core.database import async_db_sessionfrom app.core.base_schema import AuthSchemafrom app.plugin.module_quant.apikey.service import QuantApikeyServicefrom app.plugin.module_quant.exchange.service import QuantExchangeServicefrom app.plugin.module_quant.account.service import QuantAccountServicefrom app.plugin.module_quant.account.schema import QuantAccountCreateSchemaasync def sync_account():    return await SyncAccount().main()class SyncAccount(object):    def __init__(self):        self.exchanges = {}        self.apikeys = []    async def init(self, auth: AuthSchema, db):        exchanges = await QuantExchangeService(auth=auth, db=db).get_list(search={})        for exchange in exchanges:            self.exchanges[exchange.id] = exchange        apikeys = await QuantApikeyService(auth=auth, db=db).get_list(search={})        for apikey in apikeys:            self.apikeys.append(apikey)    async def get_total_balance(self, client: ccxt.Exchange, total: dict):        total_balance = 0        for symbol, amount in total.items():            if amount <= 0.00001:                continue            if symbol == 'USDT':                total_balance += amount            else:                try:                    ticker = client.fetch_ticker(f"{symbol}/USDT")                    price = ticker.get('last', 0)                    total_balance += amount * price                except Exception:                    pass        return total_balance    async def main(self):                async with async_db_session() as session:            auth = AuthSchema(db=session)            await self.init(auth=auth, db=session)            feedback = ''            for apikey in self.apikeys:                exchange = self.exchanges[apikey.exchange_id]                exchange_class = getattr(ccxt, exchange.name)                exchange_options = {                    'apiKey': apikey.api_key,                    'secret': apikey.secret_key,                    'enableRateLimit': True,                    'verbose': False,                }                if exchange.name in ('okx', 'bitget'):                    exchange_options['password'] = apikey.passphrase                try:                    client = exchange_class(exchange_options)                    balance = client.fetch_balance()                    logger.info(balance)                    info = balance.get('info')                    if exchange.name.lower() == 'binance':                        details = {key: value for key, value in balance.items() if type(value) is dict and 'total' in value and value['total'] > 0}                        param = {                            'total_balance_usdt': float(balance.get('USDT', {}).get('total', 0)),                            'total': {key: value for key, value in balance.get('total', {}).items() if value > 0},                            'free': {key: value for key, value in balance.get('free', {}).items() if value > 0},                            'used': {key: value for key, value in balance.get('used', {}).items() if value > 0},                            'details': [],                            'upl': "0",                            'update_date': date.today().strftime("%Y-%m-%d"),                            'profit': 0,                            'total_profit': 0,                            'spot_balance_usdt': float(balance.get('USDT', {}).get('total', 0)),                            'future_balance_usdt': 0,                        }                        for key, value in details.items():                            param['details'].append({                                "eq": float(value.get('total')),                                "ccy": key,                                "availEq": float(value.get('free')),                                "frozenBal": float(value.get('used')),                                "eqUsd": '0'                            })                    elif exchange.name == 'bitget':                        if type(info) == list and len(info) == 0:                            continue                        param = {                            'total_balance_usdt': float(info[0].get('frozen')) + float(info[0].get('available')),                            'total': balance.get('total'),                            'free': balance.get('free'),                            'used': balance.get('used'),                            'details': [],                            'upl': "0",                            'update_date': date.today().strftime("%Y-%m-%d"),                            'profit': 0,                            'total_profit': 0,                            'spot_balance_usdt': float(info[0].get('frozen')) + float(info[0].get('available')),                            'future_balance_usdt': 0,                        }                        for item in info:                            param['details'].append({                                "eq": float(item.get('frozen')) + float(item.get('available')),                                "ccy": item.get('coin'),                                "availEq": float(item.get('available')),                                "frozenBal": float(item.get('frozen')),                                "eqUsd": '0'                            })                    else:                        param = {                            'total_balance_usdt': float(info.get('data')[0].get('totalEq')),                            'total': balance.get('total'),                            'free': balance.get('free'),                            'used': balance.get('used'),                            'details': info.get('data')[0].get('details'),                            'upl': "0",                            'update_date': (datetime.strptime(balance.get('datetime'), "%Y-%m-%dT%H:%M:%S.%fZ").date() + timedelta(hours=8)).strftime("%Y-%m-%d"),                            'profit': 0,                            'total_profit': 0,                            'spot_balance_usdt': float(info.get('data')[0].get('totalEq')),                            'future_balance_usdt': 0,                        }                    param['exchange_id'] = apikey.exchange_id                    param['apikey_id'] = apikey.id                    param['total_balance_usdt'] = await self.get_total_balance(client, param.get('total'))                    param['created_id'] = apikey.created_id                    param['updated_id'] = apikey.updated_id                    param['status'] = '0'                    now_utc8_str = datetime.now(timezone(timedelta(hours=8))).strftime('%Y-%m-%d %H:%M:%S')                    param['created_time'] = now_utc8_str                    param['updated_time'] = now_utc8_str                    await QuantAccountService(auth=auth, db=session).create(data=QuantAccountCreateSchema(**param))                    await session.commit()                    feedback = feedback + f'获取{apikey.name}余额成功 '                except Exception as e:                    await session.rollback()                    logger.error(f"Error fetching balance for {apikey.name}: {e}")                    feedback = feedback + f'获取{apikey.name}余额失败 '            logger.info(feedback)        return feedback

这里需要注意,由于不同的交易所返回的信息不一样,所以我们需要根据交易所进行特定字段的解析,而且有些交易所并没有给出总资产的USDT价值,也需要我们根据返回的资产进行转换。

3.测试获取账户资产

在tests下新增一个文件test_sync_account.py来测试我们写的获取账户资产的功能,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env python  # encoding: utf-8  # @version: v1.0  # @author:  Kandy.Ye  # @contact: Kandy.Ye@outlook.com  # @file:    sync_account.py  # @time:    2026/09/20 11:21  import sys  from pathlib import Path  sys.path.insert(0, str(Path(__file__).parent.parent))  from app.core.base_model import MappedBase  from app.utils.import_util import ImportUtil  # 加载全部 ORM 模型以解析关系映射(如 UserMixin 引用的 UserModel)  ImportUtil.find_models(MappedBase)  from app.plugin.module_quant.job_fun.sync_account import sync_account  from app.core.database import async_engine  async def test_sync_account():      try:          result = await sync_account()          print(result)      finally:          await async_engine.dispose()  if __name__ == "__main__":      import asyncio      asyncio.run(test_sync_account())

运行效果如下:

4. 新建定时任务节点

启动前后端服务,在菜单任务管理->定时任务->节点管理里面新建一个节点:

  • 节点名称:同步账户资产
  • 节点编码:sync_account
  • 执行计划:因为我们需要每天早上8:10更新,所以Cron表达式
  • Cron表达式:0 10 8 * * ?,可以通过弹出的对话框进行选择,自动生成表达式,如下图
  • 开始时间:弹出的对话框里面选择今天
  • 结束时间:可以不填
  • 存储器:默认Redis
  • 执行器:线程池
  • 合并运行:是,错过执行时间后使用运行一次
  • 最大实例:1
  • 处理器:注意必须要定义handler函数。
1
2
3
4
5
6
7
8
9
def handler(**kwargs):    import asyncio    from app.core.ap_scheduler import scheduler    from app.plugin.module_quant.job_fun.sync_account import sync_account    loop = getattr(scheduler, '_eventloop', None)    if loop and loop.is_running():        return asyncio.run_coroutine_threadsafe(sync_account(), loop).result()    return asyncio.run(sync_account())
填写完所有内容之后,我们点击确认保存。然后在调度器监控里面自动生成一个一个调度器:
点击调试按钮,运行一次资产同步,如果出现获取余额成功的提示,表示我们的功能没有问题。
切换到资产页面,噩梦可以看到我们刚刚获取到的资产详情:
相关内容:
[创新技术阁][AiTrader]新增交易所管理并修改前端显示
[创新技术阁][AiTrader]新增交易对Symbol的管理
[创新技术阁][AiTrader]完善Symbol显示
[创新技术阁][AiTrader]新增Apikey的管理
[创新技术阁][AiTrader]完善Apikey显示和验证
[创新技术阁][AiTrader]新增账户管理以及问题修复
[创新技术阁][AiTrader]Apikey新增获取option接口并完善账户显示

相关学习资料