加密货币搬砖程序的设计——在不同交易所间获取差价利润的方法

3,183 阅读9分钟

背景

目前包括比特币在内的各种加密货币的交易在各个证券市场都是空前的活跃,在这些加密货币中,不仅有BTC、ETH(以太坊)、USDT(类美元)等知名而相对稳定的货币,也有如DOGE、ADA等波动较大的货币,还有以交易所发行的如BNB等从交易费计算发展而来的代币,

在大多数据情况下,这些货币在不同交易所的价格是完全一致的,但是这种一致并非是交易所的程序在进行同步,或者有什么中央价格系统在全局管控,他们的一致,是由市场的自由买卖实现的,当一个交易所的某一种货币的价格,低于其他交易所的时候,资本就会敏锐的发现这个现象,并在这个交易所快速买入,并在其他交易所卖出,这种行为会推高当前交易所的交易价格,通过这种机制实现动态的价格统一。

需求

在了解了背景的规律下,我们考虑是否不是可以编写一个程序,让其利用自己快速反应的特点,不断地在交易所中观察各种货币的价格,当出现差异时,立即进行买入、卖出的操作,从而实现“自动挣钱”,这么看来,我们的程序的设计需求就有了:

  • 能够实时监控至少2个交易所的多种货币的价格(深度价格、包含买价和卖价)

  • 能够快速的在至少2个以上的交易所按照需要进行货币的买卖下单

  • 提供一定的统计分析能力,例如本日获得的差价利润,交易笔数等内容的界面

  • 能在不同交易所之间移动货币(即提币)

程序设计

以下设计,均以2个交易所为例

业务流程

根据需求和背景中问题的描述,我们设计2个流程来实现需求,

主流程:负责判断机会并下单交易

提币流程;负责平衡不同交易所之间的货币余额,为搬砖交易提供头寸保障

主要程序架构

交易价格的获取和对比

下图展示了交易所中ETH/USDT ,即用USDT购买ETH的价格,注意看图中红色的部分,货币的价格,其实并不是一个数字,而是2个list,每个list中的元素是价格+数量,2个list的分别表示了当前货币的买家报价和卖价报价,即买家想以多少钱买多少个,卖家想以多少钱,卖多少个,

由于我们获取的利润来自于不同市场的价格差,通常这个价格差是稍纵即逝的,因此,我们在交易价格的获取上,不取list,而是获取每个list的第一个元素,即出价最高的买价,和价格最低的卖价及其数量。

对于下图来说,我们的卖价、买价分别是:

卖价:2094.35USDT卖70.7724 个****ETH

买价:2094.34USDT买20.8106个****ETH

image

因此,我们对比的价格的代码,应当是拿出交易所各方的最优价格进行对比

/**
判断指定的货币(symbol)是否存在获利机会,返回true表示一个交易所的卖价低于另一个交易所的买价。
*/
private boolean isAChance(Exchange bidDm, Exchange askDm, String symbol) {
    BigDecimal bestBidInFee = bidDm.getBestBid(symbol).getKey().multiply(new BigDecimal(1 - bidDm.getFee()));
    BigDecimal bestAskInFee = askDm.getBestBid(symbol).getKey().multiply(new BigDecimal(1 - askDm.getFee()));
    return bestAskInFee.compareTo(bestBidInFee) <= 0;
}

判断为一个机会以后,我们还需要进行如下3个步骤:

**1.计算订单总价:**在之前获取的最优卖价、最优买价的基础上,还要考虑他们的数量,

**2.判断头寸是否足够成交:**如果账户余额不足,则余额够买多少就买多少

**3.精度修正: **如果不同交易所对于指定货币的精度不同,例如在交易所A的价格是1.34592003 交易所B是1.3456,则需要统一精度

image

如图所示,2个交易所,发现了获利机会,以30239.77的价格在B交易所买入1.15451之后,在交易所A以30242.16的价格卖出,获利= (30242.16-30239.77)*1.15451 - 交易费

以下代码实现了上面的3个判断:

//判断订单的总价(买入价*数量 = 总价)是否满足最低订单要求
if (bestAskPrice.multiply(baseAmount).compareTo(getMaxOrderPrice(symbol)) > 0) {
    BigDecimal fixTooBigAmount = getMaxOrderPrice(symbol).divide(bestAskPrice, 8, BigDecimal.ROUND_DOWN);
    logger.info("[{}],Too Big :{}*{} = {},cut to {}", symbol.toUpperCase(), bestAskPrice.toPlainString(), baseAmount.toPlainString(), bestAskPrice.multiply(baseAmount).toPlainString(), fixTooBigAmount.toPlainString());
    baseAmount = fixTooBigAmount;
}
//确定当前数量,双侧头寸是否足够,如果足够,则数量不修改,如果头寸不足,头寸够多少,就定多少。
BigDecimal fixAmountByCash = getCashAmount(bidMgr, askMgr, symbolBase, symbolQuote, bestAskPrice, bestBidPrice, baseAmount);
if (fixAmountByCash.compareTo(baseAmount) != 0) {
    chanceType = ChanceType.BYCASH;
}
//进行精度修正,哪边的精度粗,以哪边的为准。
baseAmount = fixAmount(bidMgr, askMgr, fixAmountByCash, symbol);
if (!isCompliantOrderRule(bestBid, bestAsk, bidMgr, askMgr, symbol, baseAmount)) {
    continue;
}

下单

通过上述判断步骤,我们拿到了所有下单所需要的信息,包括;

  1. symbol即用什么币买什么币,例如BTC/USDT usdt买btc。

  2. 下单数量,双侧都是一致的,数量的精度也统一了

  3. 下单金额,这里卖价和买价应该是不同的。

在准备下单前,我们不妨将这个机会存在mysql数据库中,用于后续分析统计只用,

需要注意的是,存储mysql的操作,务必是异步的,因为

  1. 存不存mysql,我们的流程不受任何影响

  2. 机会稍纵即逝,可能是100毫秒内,机会就没了,因此mysql的save事件时间成本是无法忽略的,甚至是致命的(我自己实测同步写库下单成功率会降低50%以上)

存储的数据结构,可能是这样的

public class ChanceDto {
    private String id; 
    private String bidMarketName;//提供买入价的交易所名称
    private BigDecimal bidPrice; //买入价
    private BigDecimal bidAmount; //买入数量
    private Long bidTimestamp; //bid时间戳
    private String askMarketName; //提供卖出价的交易所名称
    private BigDecimal askPrice; //卖出价
    private BigDecimal askAmount; //卖出数量
    private Long askTimestamp; //ask时间戳
    private String symbol; //币种/货
    private String timestamp;  //系统时间戳
    private BigDecimal expectProfit; //预期利润
    private ChanceType chanceType; //机会的类型,目前全部是挂单、
}

通过Async注解,实现双侧并发下单

private void orderConcurrency(Exchange bidEx, Exchange askEx, String symbol, BigDecimal baseAmount, BigDecimal sellPrice,
                              BigDecimal buyPrice, String chanceId) {
    cdl = new CountDownLatch(2);
    //双侧下单,采用Async方式,即非阻塞并发下单。
    bidEx.sellBidAsync(symbol, baseAmount, sellPrice, chanceId, cdl);
    askEx.buyAskAsync(symbol, baseAmount, buyPrice, chanceId, cdl);
}
@Async
public void sellBidAsync(String symbol, BigDecimal amount, BigDecimal price, String chanceId, CountDownLatch cdl) {
    try {
        sellBid(symbol, amount, price, chanceId);
    } catch (Exception e) {
        logger.error(ExceptionUtils.getMessage(e));
    } finally {
        cdl.countDown();
    }
}

@Async
public void buyAskAsync(String symbol, BigDecimal amount, BigDecimal price, String chanceId, CountDownLatch cdl) {
    try {
        buyAsk(symbol, amount, price, chanceId);
    } catch (Exception e) {
        logger.error(ExceptionUtils.getMessage(e));
    } finally {
        cdl.countDown();
    }

}

提币判断

提币的流程较为简单,主要就是一个定时任务,定时获取各个交易所账户的余额,并且进行两两对比,如果发现差额达到一个阈值,则调用交易所的提币接口,将货币转至另一个交易所

//伪代码
config = 获取阈值配置
for(货币列表){
    交易所a余额 = 交易所a_client.getAccountBySymbol(货币)
    交易所b余额 = 交易所b_client.getAccountBySymbol(货币)
    if a - b /a > config.threshold(货币):
        交易所a_client.withdraw(交易所b的账户地址)
    。。。
}

交易所模块设计

通常交易所提供的服务接口包含两种传输方式

webSocket : 用于实时更新账户金额变化、深度摆盘更新以及订单状态更新的回调

http:主要用于客户侧程序主动发起的任务,例如订单创建、修改、删除,主动进行的账户信息查询、提币、主动进行的当前摆盘的拉取等

交易所封装接口

不同的交易所,肯定会有不同的SDK供我们使用,大家的功能基本大同小异,但是我们在程序中为了统一调用,做到:主流程只关注交易所统一的行为,不掺入交易所之间的程序差异,因此引入交易所接口来封装交易所提供的功能,根据之前的分析,我们可以得出,一个交易所client或者说交易所service应当具备如下能力


public interface Exchange {
    //创建订单
    public Long createOrder(String symbol, BigDecimal amount, BigDecimal price, String chanceId, OrderType orderType);
    //取消订单
    public void cancelOrder(long orderId, String symbol);
    //获取订单
    public OrderStateUpdateDto getOrder(String symbol, long orderId);
    //获取账户信息、余额by币种
    public Map<String, AccountDto> fetchAccount();
    //获取深度价格摆盘(价格列表)
    public Map<String, DepthDto> fetchDepth();
    //获取交易所规则(最小交易金额)
    public Collection<SymbolRuleDto> fetchSymbolRule();
    //主动获取订单信息
    public Map<String, Map<Long, SymbolOrderDto>> fetchOrderState();
    //交易所回调更新账户
    public void accountUpdate(AccountUpdateCallBack callBack);
    //交易所回调更新订单
    public void orderStateUpdate(OrderStateUpdateCallBack callBack);
    //交易所回调更新深度信息
    public void depthUpdate(DepthUpdateCallBack callBack);
    //提币
    public String withdraw(String currency, BigDecimal amount, String address, String tag);
}

实践和效果

程序的部署

通常我们将程序部署在离交易所较近的位置,以获得较低网络延迟,考虑到目前交易所的实际情况,可以考虑购买新加坡或者香港的云服务器,目前国内主要的云服务器厂商,均有对应区域的服务器。

运行效果

通过一个简单的页面和API,可以展示如下信息:

  • 利润

  • 当前各交易所账户头寸

  • 最近失败的订单top10

  • 提币记录

以下是展示效果,可以随时随地,关注程序的运行指标

实际的利润情况,取决于你做的币种,当然,你可以选择做很多币种,以捕获更多的机会,但是币种越多,每个币种的头寸就越紧张(除非你增加投资额),或者提高提币的频率(提币本身也有手续费)

image

程序运行日志,和mysql console上实现的简易看板

image

写在最后

程序的改进

目前,该程序仅能管理一套账户体系,后续可以考虑将账户模块进行提取,甚至是做成SaaS方式,拉亲朋好友一起,系统可对其他账户获得纯利润进行抽成,并通过web管理页面展示。

获利空间

随着机构越来越广泛的参与,通过搬砖获取高回报率的空间被不断要锁,通过这种方式躺着赚钱的日子或许渐行渐远了,并且,随着加密货币在越来越多的国家被认定为非法交易,和法币的兑换也越来越难了。

但是作为程序员用程序改变自己改变环境的念头,却是始终存在的,相比于说分享一个赚钱的方法,我更愿意说是分享一个用程序改变我们程序员生活的思路吧。

后续我还会分享其他能够具备经济价值的程序设计方案和思路,欢迎大家关注我!