
今天我想和大家分享一个交易机器人的实现。首先我们需要一个技术任务:
- 必须能够独立交易
 
让我们按预期从应用程序的架构开始:

所以 Lambda 将从某些东西(例如,从 Trading View)接收 WebHook。此 WebHook 只能传递两个值:BUY 或 SELL(无附加参数)。机器人将决定买卖的数量和硬币。
在数据库中,我们将只有三个实体:
策略——每个单独的“买卖”都是一个独立的策略
保持 – 保持系统(更多内容见下文)
设置 – 机器人设置,例如表名称、币安 API 密钥和秘密以及策略配置。
让我们开始实施
首先,让我们安装必要的库:
yarn add dynamoose node-binance-api typescript uuid# dynamoose - 类似于 mongoose 仅用于 DynamoDB # node-binance-api - 用于使用 Binance API # typescript - 我们将使用 Typescript # uuid - 生成唯一 ID
此外,添加类型包:
纱线添加 -D @types/node @types/uuid
让我们描述一下数据库模型:
| import * as dynamoose from ‘dynamoose’ | |
| import {Document} from “dynamoose/dist/Document”; | |
| import config from ‘../../config’ | |
| export class HoldDocument extends Document { | |
| id?: string | |
| type?: string | |
| symbol?: string | |
| status?: string | |
| data?: string | |
| avgPrice?: number | |
| avgPriceProfit?: number | |
| orderId?: string | |
| qty?: number | |
| createdAt?: Date | |
| } | |
| export default dynamoose.model<HoldDocument>(config.tables.hold, { | |
| id: { | |
| type: String, | |
| hashKey: true | |
| }, | |
| type: { | |
| type: String, | |
| index: { | |
| name: “type”, | |
| global: true | |
| } | |
| }, | |
| symbol: { | |
| type: String, | |
| index: { | |
| name: “symbol”, | |
| global: true | |
| } | |
| }, | |
| status: { | |
| type: String, | |
| index: { | |
| name: “status”, | |
| global: true | |
| } | |
| }, | |
| data: String, | |
| avgPrice: Number, | |
| avgPriceProfit: Number, | |
| orderId: String, | |
| qty: Number, | |
| createdAt: Date, | |
| }, { | |
| // @ts-ignore-next-line | |
| saveUnknown: false | |
| }); | 
| import * as dynamoose from ‘dynamoose’ | |
| import config from ‘../../config’ | |
| import {Document} from “dynamoose/dist/Document”; | |
| export class SettingDocument extends Document { | |
| id?: string | |
| type?: string | |
| symbol?: string | |
| status?: string | |
| data?: string | |
| createdAt?: Date | |
| } | |
| export default dynamoose.model<SettingDocument>(config.tables.setting, { | |
| id: { | |
| type: String, | |
| hashKey: true | |
| }, | |
| type: { | |
| type: String, | |
| index: { | |
| name: “type”, | |
| global: true | |
| } | |
| }, | |
| symbol: { | |
| type: String, | |
| index: { | |
| name: “symbol”, | |
| global: true | |
| } | |
| }, | |
| data: String, | |
| createdAt: Date, | |
| }, { | |
| // @ts-ignore-next-line | |
| saveUnknown: false, | |
| }); | 
| import * as dynamoose from ‘dynamoose’ | |
| import config from ‘../../config’ | |
| import {Document} from “dynamoose/dist/Document”; | |
| export class StrategyDocument extends Document { | |
| id?: string | |
| type?: string | |
| symbol?: string | |
| status?: string | |
| profit?: number | |
| data?: string | |
| createdAt?: Date | |
| holdId?: string | |
| unHoldPrice?: number | |
| } | |
| export default dynamoose.model<StrategyDocument>(config.tables.strategy, { | |
| id: { | |
| type: String, | |
| hashKey: true | |
| }, | |
| type: { | |
| type: String, | |
| index: { | |
| name: “type”, | |
| global: true | |
| } | |
| }, | |
| symbol: { | |
| type: String, | |
| index: { | |
| name: “symbol”, | |
| global: true | |
| } | |
| }, | |
| status: { | |
| type: String, | |
| index: { | |
| name: “status”, | |
| global: true | |
| } | |
| }, | |
| profit: Number, | |
| data: String, | |
| createdAt: Date, | |
| holdId: { | |
| type: String, | |
| index: { | |
| name: “holdId”, | |
| global: true | |
| } | |
| }, | |
| unHoldPrice: Number, | |
| }, { | |
| // @ts-ignore-next-line | |
| saveUnknown: false, | |
| }); | 
在策略中设置设置。最重要的是:
- 资产——我们将购买哪种资产机器人
 - type – 我们策略的唯一名称。如果我们想将我们的 lambda 用于许多策略,我们将为每个策略设置一个唯一的名称
 - riskPercent — 在一种策略中使用的 USDT 总余额的百分比
 - minAmountUSDT — 策略中使用的最低 USDT 数量,如果我们的余额较少,机器人将跳过此 BUY 事件
 
添加配置文件:
| export default { | |
| tables: { | |
| hold: process.env.DYNAMODB_HOLD_TABLE as string, | |
| setting: process.env.DYNAMODB_SETTING_TABLE as string, | |
| strategy: process.env.DYNAMODB_STRATEGY_TABLE as string, | |
| }, | |
| binance: { | |
| key: process.env.BINANCE_API_KEY as string, | |
| secret: process.env.BINANCE_API_SECRET as string | |
| }, | |
| strategy: { | |
| type: ‘spot’, | |
| asset: ‘ETH’, | |
| currency: ‘USDT’, | |
| defaultSetting: {isReuseHold: true, riskPercent: 0.05, minAmountUSDT: 11} | |
| } | |
| } | 
以及使用数据库的提供者,该数据库具有机器人的所有必要方法。
添加用于使用 Binance API 的接口。我们只需要一些功能,如:“市价买入”、“市价卖出”、“创建限价单”、“取消订单”、“获取当前价格”、“获取我的余额”和“查看订单状态”。要实现这一点,请添加以下类:
| import HoldModel, {HoldDocument} from ‘../model/hold’ | |
| import Base from ‘./base’ | |
| import {HOLD_STATUS} from ‘../../constants’ | |
| class Hold extends Base<HoldDocument, THold> { | |
| getByTypeAndSymbol(type: string, symbol: string): Promise<THold | undefined> { | |
| return this.getFirst({type, symbol}) | |
| } | |
| getByTypeAndSymbolStatus(type: string, symbol: string, status: string): Promise<THold | undefined> { | |
| return this.getFirst({type, symbol, status}) | |
| } | |
| create(data: THold): Promise<THold> { | |
| return super.create({ | |
| status: HOLD_STATUS.STARTED, | |
| …data | |
| }) | |
| } | |
| getCurrentHolds(): Promise<THold[]> { | |
| return this.getList({status: HOLD_STATUS.STARTED}) | |
| } | |
| } | |
| export default new Hold(HoldModel); | 
| import SettingModel, {SettingDocument} from ‘../model/setting’ | |
| import Base from ‘./base’ | |
| import config from ‘../../config’ | |
| class Setting extends Base<SettingDocument, TSetting> { | |
| async getByTypeAndSymbol(type: string, symbol: string): Promise<TSetting> { | |
| let setting = await this.getFirst({type, symbol}) | |
| if (!setting) { | |
| setting = await this.create({ | |
| type, | |
| symbol, | |
| data: config.strategy.defaultSetting | |
| }) | |
| } | |
| return setting | |
| } | |
| } | |
| export default new Setting(SettingModel); | 
| import StrategyModel, {StrategyDocument} from ‘../model/strategy’ | |
| import Base from ‘./base’ | |
| import {STRATEGY_STATUS} from ‘../../constants’ | |
| import {getOrderPrice} from ‘../../helper/order’ | |
| class Strategy extends Base<StrategyDocument, TStrategy> { | |
| getByTypeAndSymbol(type: string, symbol: string): Promise<TStrategy[]> { | |
| return this.getList({type, symbol}) | |
| } | |
| create(data: TStrategy): Promise<TStrategy> { | |
| return super.create({ | |
| status: STRATEGY_STATUS.CREATED, | |
| …data | |
| }) | |
| } | |
| update(s: TStrategy): Promise<TStrategy> { | |
| if (s.id) { | |
| return super.update(s) | |
| } else { | |
| return this.create(s) | |
| } | |
| } | |
| getByHoldId(type: string, symbol: string, holdId: string): Promise<TStrategy[]> { | |
| return this.getList({type, symbol, holdId}) | |
| } | |
| async getSimilarHold(strategy: TStrategy, currentPrice: number): Promise<TStrategy | undefined> { | |
| const list = (await this.getList({ | |
| type: strategy.type, | |
| symbol: strategy.symbol, | |
| status: STRATEGY_STATUS.HOLD, | |
| })) || [] | |
| return list.find((s: TStrategy) => currentPrice >= getOrderPrice(s.data?.buyOrder)) | |
| } | |
| getByTypeAndSymbolStatus(type: string, symbol: string, status: string): Promise<TStrategy[]> { | |
| return this.getList({type, symbol, status}) | |
| } | |
| getCurrentStrategy(type: string, symbol: string): Promise<TStrategy | undefined> { | |
| return this.getFirst({type, symbol, status: STRATEGY_STATUS.STARTED}) | |
| } | |
| } | |
| export default new Strategy(StrategyModel); | 
让我们为一个不会赔钱的机器人创建一个策略。

例如,机器人用 2000 USDT 购买了 0.1 ETH。然后是卖出信号。有两种可能的结果:
- 如果当前价格高于买入价格(例如,2200 USDT),则机器人卖出并计算利润:(2200-2000)* 0.1 = 20USDT(策略状态 = FINISHED)
 - 如果当前价格低于购买价格,机器人会记住并持有此购买(不要亏本出售)。(策略状态=持有)
 
我们还创建了一个 Hold 实体,它将监控处于 HOLD 状态的所有策略,并下达限价单,以所有策略的平均买入价卖出。
例如,1900 USDT 又购买了 0.05 ETH。当前价格再次低于买入价。该策略也将被搁置。机器人将重新计算两种策略的平均持有价格 (0.1*2000 + 0.05*1900)/0.15 = 1967 USDT 并下达限价卖单。如果这个订单被执行,我们不会亏损,也不会获得利润,但我们会退还我们的 USDT。
每隔 5 分钟,机器人会检查该卖单,一旦成交,机器人会将 HOLD 策略的所有状态更改为 UNHOLD 状态。
您还可以在 HOLD 状态下添加策略的重用。如果 BUY 信号到达,当前价格为 1950USDT,我们有一个持有状态的策略,买入价为 1900USDT。机器人不会再次购买。它将购买订单数据复制到新策略中,取消之前的策略并将其从保留中移除。
现在我们可以继续编写机器人的逻辑了。处理买入信号的主要功能是:
以及处理卖出信号的主要功能:
| async sell(): Promise<void> { | |
| // get current price | |
| const currentPrice = await this.getPrice() | |
| const buyPrice = getOrderPrice(this.strategy?.data?.buyOrder) | |
| const buyQty = getOrderQuantity(this.strategy?.data?.buyOrder) | |
| if (buyPrice < currentPrice) { | |
| // if buyPrice < currentPrice then sell this amount to profit | |
| if (buyQty > 0) { | |
| const sellOrder = await this.marketSell(buyQty) as any | |
| const { | |
| avgPrice, | |
| totalQty, | |
| commission, | |
| commissionAsset | |
| } = calculateOrderFills(sellOrder && sellOrder.fills) | |
| sellOrder.avgPrice = avgPrice | |
| sellOrder.totalQty = totalQty | |
| sellOrder.commission = commission | |
| sellOrder.commissionAsset = commissionAsset | |
| this.strategy.profit = calculateProfit(this.strategy?.data?.buyOrder, sellOrder) | |
| this.setData({sellOrder}) | |
| this.strategy.status = STRATEGY_STATUS.FINISHED | |
| await strategyProvider.update(this.strategy) | |
| } | |
| } else { | |
| // if buyPrice >= currentPrice then hold this strategy | |
| if (buyQty > 0) { | |
| await this.addToHold() | |
| } | |
| } | |
| } | 
当我们添加一个持有策略时会发生什么的更多细节:
| async addToHold(): Promise<void> { | |
| // get or create Hold | |
| let hold = await holdProvider.getByTypeAndSymbolStatus(this.type, this.symbol, HOLD_STATUS.STARTED) | |
| if (!hold) { | |
| hold = await holdProvider.create({ | |
| type: this.type, | |
| symbol: this.symbol, | |
| status: HOLD_STATUS.STARTED | |
| }) | |
| } | |
| // add status and holdId to strategy | |
| this.strategy.holdId = hold.id | |
| this.strategy.status = STRATEGY_STATUS.HOLD | |
| await strategyProvider.update(this.strategy) | |
| // recalculate averageHoldPrice | |
| const calcHold = await this.recalculateHold(hold) | |
| if (calcHold) { | |
| // create or move hold limit sell order | |
| await this.createOrUpdateOrder(calcHold) | |
| } | |
| } | 
最后缺少的是对 Hold 下的限价卖单每 5 分钟进行一次检查。让我们添加这个检查:
| class CheckHold { | |
| api: BaseApiSpotService | |
| hold: THold | |
| constructor(hold: THold) { | |
| this.hold = hold | |
| this.api = new BaseApiSpotService(this.hold.symbol) | |
| } | |
| async check() { | |
| if (this.hold.orderId) { | |
| await this.checkOrder() | |
| if (getOrderStatus(this.hold.data.sellOrder) === ORDER.STATUS.FILLED) { | |
| await this.unHold() | |
| } | |
| } | |
| } | |
| async checkOrder(): Promise<void> { | |
| const orderData: any = await this.api.checkStatus(this.hold.orderId!) | |
| if (!this.hold.data) { | |
| this.hold.data = {} | |
| } | |
| // update hold data if order changed | |
| if (orderData && orderData.orderId && (orderData.orderId!==this.hold.orderId || getOrderStatus(this.hold.data.sellOrder) !== String(orderData.status))) { | |
| this.hold.data.sellOrder = orderData | |
| const {avgPrice, totalQty, commission, commissionAsset} = calculateOrderFills( | |
| orderData && orderData.fills) | |
| this.hold.data.sellOrder.totalQty = totalQty | |
| this.hold.data.sellOrder.commission = commission | |
| this.hold.data.sellOrder.avgPrice = avgPrice | |
| this.hold.data.sellOrder.commissionAsset = commissionAsset | |
| await holdProvider.update(this.hold) | |
| } | |
| } | |
| async unHold(): Promise<void> { | |
| // get all strategies which related with this hold | |
| const strategies = await strategyProvider.getByHoldId(this.hold.type, this.hold.symbol, this.hold.id!) | |
| if (strategies && strategies.length > 0) { | |
| for (const s of strategies) { | |
| try { | |
| // calculate profit for each strategy | |
| const qty = getOrderQuantity(s.data?.buyOrder) | |
| const commission = this.hold.data.sellOrder.totalQty > 0 ? this.hold.data.sellOrder.commission * qty / | |
| this.hold.data.sellOrder.totalQty : 0 | |
| const sell = {…this.hold.data.sellOrder, totalQty: qty, commission} | |
| s.profit = calculateProfit(s.data?.buyOrder, sell) | |
| s.data = {…s.data, sellOrder: sell} | |
| // save UNHOLD status, profit, and data to DB | |
| await strategyProvider.update({…s, status: STRATEGY_STATUS.UNHOLD}) | |
| } catch (e) { | |
| console.log(‘error set profit’, {s, hold: this.hold}) | |
| } | |
| } | |
| } | |
| // update hold status to FINISHED | |
| this.hold.status = HOLD_STATUS.FINISHED | |
| await holdProvider.update(this.hold) | |
| } | |
| } | 
结论
在本文中,我们学习了如何实现 TradingView Lambda 机器人的基本逻辑,该机器人只能用于盈利。但它还没有完成。下次我们将通过测试来介绍机器人。文章的下一部分在这里。
