今天我想和大家分享一个交易机器人的实现。首先我们需要一个技术任务:
- 必须能够独立交易
让我们按预期从应用程序的架构开始:
所以 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 机器人的基本逻辑,该机器人只能用于盈利。但它还没有完成。下次我们将通过测试来介绍机器人。文章的下一部分在这里。