From 7fbec72b2dfae5b72db85124202e08dd4679d8ae Mon Sep 17 00:00:00 2001 From: konbluesky Date: Thu, 21 Nov 2024 08:11:16 +0800 Subject: [PATCH] Site updated: 2024-11-21 08:11:16 --- about/index.html | 4 ++-- content.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/about/index.html b/about/index.html index bbf146f..782c36f 100644 --- a/about/index.html +++ b/about/index.html @@ -1,5 +1,5 @@ -about me - Gavin's Home

about me

姓名 Gong Wei
性别
工作经验 10years+
学历 本科
学校 北京航空航天大学 - 计算机科学与技术
邮箱 blackjackhoho@gmail.com
Telegram @blackjackhoho
Github https://github.com/konbluesky

+

about me

姓名 Gong Wei
性别
工作经验 10years+
学历 本科
学校 北京航空航天大学 - 计算机科学与技术
邮箱 blackjackhoho@gmail.com
Telegram @blackjackhoho
Github https://github.com/konbluesky


14年的老开发,在寻找一个激情、开放、和谐的团队,与志同道合的伙伴共同创造卓越的项目。


diff --git a/content.json b/content.json index 1e39837..5b0b69c 100644 --- a/content.json +++ b/content.json @@ -1 +1 @@ -{"posts":[{"title":"Carry-Coin 一个自动化搬砖套利平台","text":"Carry-Coin 是一个套利程序,程序从23年初开始开发至今,目前已经基本稳定,现在将程序的设计整理出来;套利思路很简单:程序监控Cex和Dex平台,针对同一币种发现差价后自动化搬运; carry-config-generator(python) 框架:web3,pandas ; 工程主要负责根据dex,cex,第三方:1inch,odos,dexscreener 数据,进行数据分析最终生成套利配置; carry-core (Java) 一个基于Java的套利核心程序,dex<->cex套利逻辑的顶层抽象,SwapEngine,SwapStategy, ArbitrageProcessor,CenterExchange,DecenterExchanage,SwapProtocol 等; carry-worker (Java) 框架:Springboot3.2.5,Xchange,Web3j,RxJava3,Guava等;工程基于core实现的不同dex,cex的监控、告警、通知、搬运、买卖逻辑; carry-protocol-adapter(Nodejs) 基于Uniswap-sdk ,jupiter-swap-api(solana)开发的套利协议适配器,适配v2/v3询价; carry-web-front (Nodejs) Vue3.0+TypeScript+Vite5+Ant-Design-Vue等,工程主要管理平台的前端页面,包括套利开关、线上配置、交易数据监控,链上数据监控报表等; carry-web-server (Java) 框架: Spring Cloud Alibaba, Mysql, 管理平台的后端服务; 部署架构 程序截图 技术栈语言 Java 11 Python Nodejs Bash Shell 框架Java Spring Boot JPA Xchange RxJava Guava transmittable-thread-local fastjson web3j lombok assertj jasypt Hutool Slf4j、Logback Python Flask pickledb web3 pandas Nodejs pm2 uniswap-core,V2/v3-sdk solana/web3、spl-token nestjs ethers.js 平台 jeecg-boot","link":"/2024/10/13/carry_coin_architecture_1/"},{"title":"Carry-Coin 架构设计 Core模块(1)","text":"Carry-Coin Core的Center,Decenter,protocol的设计 Core组成部分Swap基本概念 SwapEngine 套利引擎,内部独立线程,Handler执行 ,Center构建,Job注册 SwapEngine最初设计可以支持dex(n)<->cex(n),目前阶段仅SingleSwapEngine 实现了dex(1,n)<->cex(1)的套利动作,即一个Engine绑定1个cex和N个dex; SwapContext(标记接口) SwapContext是SwapEngine的上下文,是个巨型类,为了跨交易所共用上下文预留的,主要是保存SwapEngine运行时的数据,包含了所有重要对象的引用:交易所信息,交易对信息,交易所账户信息; SwapConfig SwapConfig是SwapEngine的配置类,主要是保存SwapEngine运行时所需的配置信息; SwapHandler 用来初始化Context,同步job的阻塞加载,异步job的注册,BizDataLoaderContext首次初始化和校验 SwapInitializerConfig 根据yml初始化SwapLauncher来触发Engine工作; SwapLauncher 交易引擎启动器 Center Cex中心交易所相关的抽象和套利动作 CenterExchangeHolder 中心交易所的顶级抽象,主要是一些技术动作: 注册同步和异步的取数器(Job),获取Context数据等动作 交易所支持同步和异步取数两种模式,同步在SwapEngine初始化时执行一次,异步周期性运行; 周期性执行使用ScheduledExecutorService,实现使用了alibaba的transmittable-thread-local库,主要目的解决异步执行时上下文传递的问题; Job方式取来的数据都会放在BizDataLoaderContext容器中; AbstractCenterExchangeHolder 对CenterExchangeHolder的基本实现 CenterExchangeHolderBehavior 业务动作抽象接口:Limit/Marker下单,撤单,获取订单状态,提现,转账等业务动作,这个接口也是pipeline和job中操作Cex的核心,非必要业务流中不操作CenterExchangeHolder,AbstractCenterExchangeHolder这种顶级接口; Cex交易所操作使用Xchange完成; FacilitySupport 提供一套基本的Cex实现模板 CenterSymbolInfo Cex侧套利相关配置信息 Decenter Dex去中心交易平台相关的抽象, Decenter.protocol Decenter中SwapProtocol协议抽象,目前已实现包含solana,bsc链,odos,zerox平台的询价、交易动作;设计过程中难点在于对不同链的交易动作进行同一视角进行抽象和设计; EVM部分 GenericWeb3jBehavior 链上动作的通用抽象,默认只实现getNetwork,getWeb3jManager,考虑到多链支持Client客户端不定,所以作为泛型传入; 必要动作queryTxConfirmed,getRawAmountOut,getBalanceOfNode做抽象方法,考虑到这几个方法不挑网络,入参回参明确,所以放在此处; 不同链差异化动作放在下层实现. EthGenericWeb3jBehavior 具象evm链的实现,主要是实现网络层面、Token层面的一些方法了allowance, getTransactionGasLimit, signTransaction, waitTxConfirmed, queryTxConfirmed, getNonce, getBalanceOfNode, tokenTransfer, getGasProvider, gasProvider GenericSwapV2Impl,GenericSwapV3Impl 基于EthGenericWeb3jBehavior完成Dex swap全过程的方法,包含:getAmountsOut,swapExactTokensForTokens,swapExactTokensForTokensSupportingFeeOnTransferTokens getRawAmountOut区别于getAmountsOut,是用来通过evm中log数据获取最终交换到的token数量; SwapExactTForTParam 上层通过该类传入swap所需参数; Solana部分ODOS部分Strategy 交易策略 swap 套利动作","link":"/2024/10/14/carry_coin_architecture_2/"},{"title":"Carry-Coin 架构设计 Core模块(2)","text":"Carry-Coin Core的PipelineAction,Job的设计 Action 组成部分 AbstractForwardPipeline 正向套利(cex->dex)流水线抽象类 AbstractReversePipeline 逆向套利(dex->cex)流水线抽象类 CommonCutOffForwardPipeline CommonLimitWaitForwardPipeline CommonReversePipeline 逆向套利(dex->cex)流水线抽象类默认实现 DebugReversePipeline 调试用 WithdrawAndSellPipeline Cex账户留币策略时触发:当账户留币且链上价格比交易所高时,提现->dex->卖出 DepositAndSellPipeline Dex留币策略触发:钱包中留币且交易所价格大于链上:充币->cex->卖出 OnlySwapInChainPipeline Dex留币策略触发:钱包中留币且链上价格高于Cex:卖出 SolanaReversePipeline Solana逆向套利流水线 TransactionRecordFactory Job 组成部分 异步任务,优先使用websocket取数,如cex不支持则使用rest方式,rest调用一定要实现限频策略,具体见Xchange BalanceRest200msJob 每200ms 调rest接口取余额; ChainAccountMonitor10mJob 每10分钟 监控链上账户余额; CleanBalance1mJob 每1分钟 扫描账户符合条件触发留币策略 ConsumerMiddleCoinBuyAndSellJob 三角套利时,中间币消耗情况入库,订阅的交易所购买中间币(目前BNB,SOL) FundingRecordRest200msJob 每200ms 调rest接口取充提记录监控充提状态; LoaderDataContextMonitor1sJob 最初构想用来存储下单时,当前depth,ticker,orderbook等数据,数据量太大,有条件的情况下上nosql, 就不入db了; PendingPlaceOrderJob 定时扫描未完成订单,并尝试重试 ResetSymbolConfigJob 重置套利币本配置 RiseMonitorRest10sJob SelfOrderRest100msJob","link":"/2024/10/15/carry_coin_architecture_3/"},{"title":"Carry-Coin 架构设计 SymbolLedger (4)","text":"Carry-Coin 套利币本 SymbolLedger 设计,SymbolLedger负责存放套利过程中交易对信息,其中包括symbol在Cex中的各项配置、套利阈值等,Dex中的各种合约信息、阈值、交易参数等 SymbolLedger一个交易所对应一个SymbolLedger实例,程序启动后通过json配置文件进行加载; 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576/** * <pre> * 此类用来存储市面上所有的交易对信息 * 维护方式: 基础信息手工,其他信息通过程序自动获取; * 更新周期: 定期更新; * 作用: 以此账簿上的币作为循化基础,再从各交易所拉取对应信息; * </pre> * <p> @Date : 2023/3/21 </p> * <p> @Project : block-farming</p> * * <p> @author konbluesky </p> */@Slf4j@Datapublic class SymbolLedger { /** * 如是内存模式则不进行数据库持久化 */ private boolean memoryMode = false; /** * 更新symbolPairConfigs时,一定要重新对symbolPairConfigMap和symbolPairConfigMap_symbolKey进行更新 * 否则getSymbolPairConfig会失效 * @param symbolPairConfigs */ public void setSymbolPairConfigs(List<SymbolPairConfig> symbolPairConfigs) { this.symbolPairConfigs = symbolPairConfigs; for(SymbolPairConfig symbolPairConfig : symbolPairConfigs){ updateSymbolPairConfig(symbolPairConfig); } log.info("SymbolLedger更新symbolPairConfigs"); } private List<SymbolPairConfig> symbolPairConfigs = Lists.newCopyOnWriteArrayList(); private Map<String, SymbolPairConfig> symbolPairConfigMap = Maps.newConcurrentMap(); private Map<String, SymbolPairConfig> symbolPairConfigMap_symbolKey = Maps.newConcurrentMap(); // private Table<String, String, SymbolPairConfig> symbolPairConfigMap = Tables.synchronizedTable(HashBasedTable.create()); public void put(SymbolPairConfig symbolPairConfig) { symbolPairConfigs.add(symbolPairConfig); updateSymbolPairConfig(symbolPairConfig); } private void updateSymbolPairConfig(SymbolPairConfig symbolPairConfig){ CenterSymbolInfo centerSymbolInfo = symbolPairConfig.getCenterSymbolInfo(); symbolPairConfigMap.put(centerSymbolInfo.getBaseCurrency() .toLowerCase(), symbolPairConfig); symbolPairConfigMap_symbolKey.put(centerSymbolInfo.getBaseCurrency() .toLowerCase() + "/" + centerSymbolInfo.getQuoteCurrency() .toLowerCase(), symbolPairConfig); } @Deprecated public SymbolPairConfig getSymbolPairConfig(String baseCurrency) { return symbolPairConfigMap.get(baseCurrency.toLowerCase()); } public List<SymbolPairConfig> getSymbolPairConfigsBy(String baseCurrency) { List<SymbolPairConfig> result = Lists.newArrayList(); symbolPairConfigMap_symbolKey.forEach((k, v) -> { if(k.toLowerCase().startsWith(baseCurrency.toLowerCase())) { result.add(v); } }); return result; } public SymbolPairConfig getSymbolPairConfig(String baseCurrency, String quoteCurrency) { return symbolPairConfigMap_symbolKey.get(baseCurrency.toLowerCase() + "/" + quoteCurrency.toLowerCase()); }} SymbolLedger构建的静态工厂,json配置通过carry-config-generator(python)生成 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249/** * <pre> * 1. 交易对配置文件工厂,目前实现从HUOBI.txt文件中读取; * 2. 可以在jar包所在目录下创建config目录,将配置文件放入config目录下,程序会自动读取; * </pre> */@Slf4jpublic class SymbolLedgerFactory { public static final String SYMBOL_HUOBI_BOOKS_FILE = "HUOBI.txt"; public static final String SYMBOL_XT_BOOKS_FILE = "XT.txt"; public static final String SYMBOL_BINANCE_BOOKS_FILE = "BIAN.txt"; public static final String SYMBOL_MEXC_BOOKS_FILE = "MXCAll.txt"; public static final String SYMBOL_GATEIO_BOOKS_FILE = "GATEIO.txt"; public static final String SYMBOL_KUCOIN_BOOKS_FILE = "KUCOIN.txt"; /** * TODO 从文件中读取交易对配置,目前只解析部分字段;有需要实时更新的字段到时候从服务器上实时拉取; * swapStableCoinContractAdd 字段配置决定了与交易所的对的匹配; * * @param symbolBooksFile * @param swapConfig * @return */ private static SymbolLedger create(String symbolBooksFile, SwapConfig swapConfig) { Preconditions.checkArgument(swapConfig != null); JSONArray jsonArray = getJsonArrayByFile(symbolBooksFile); SymbolLedger symbolLedger = new SymbolLedger(); for (int i = 0; i < jsonArray.size(); i++) { JSONObject item = jsonArray.getJSONObject(i); // continue to parse the json object if (Strings.isNullOrEmpty(item.getString("swapContract")) || item.getString("swapStableCoinContract") .equalsIgnoreCase("cake")) { continue; } SymbolPairConfig symbolPairConfig = new SymbolPairConfig(); symbolPairConfig.setBaseCurrency(item.getString("swapCoinContract")); int chainId = item.getInteger("chainId") != null ? item.getInteger("chainId") : NetworkEnum.BSC.getChainId(); String stableName = SymbolTokenHelper.getTokenInfoByAddressReturnSymbol(chainId, item.getString("swapStableCoinContractAdd")); symbolPairConfig.setQuoteCurrency(Currency.USDT.getCurrencyCode()); // symbolPairConfig.setSymbol(symbolPairConfig.getBaseCurrency() + "/" + symbolPairConfig.getQuoteCurrency()); // 链上交易对和cex交易对 分别存储,交易所对,目前统一使用usdt CenterSymbolInfo centerSymbolInfo = new CenterSymbolInfo(); centerSymbolInfo.setBaseCurrency(symbolPairConfig.getBaseCurrency()); centerSymbolInfo.setQuoteCurrency(Currency.USDT.getCurrencyCode()); centerSymbolInfo.setCexMaxOrderForUSDT(item.getBigDecimal("maxOrderForUsdt") == null ? swapConfig.getGlobalMaxOrderForUsdt() : item.getBigDecimal("maxOrderForUsdt")); centerSymbolInfo.setCexMinOrderForUSDT(item.getBigDecimal("minOrderForUsdt") == null ? swapConfig.getGlobalMinOrderForUsdt() : item.getBigDecimal("minOrderForUsdt")); centerSymbolInfo.getAskPosition() .set(item.getInteger("askPosition") == null || item.getInteger("askPosition") == 0 ? 3 : item.getInteger("askPosition")); symbolPairConfig.setCenterSymbolInfo(centerSymbolInfo); // 设置去中心化交易所配置信息; DecenterSymbolInfo decenterSymbolInfo = new DecenterSymbolInfo(); decenterSymbolInfo.setBaseCurrency(symbolPairConfig.getBaseCurrency()) .setQuoteCurrency(stableName == null ? CoinEnum.getCoin(item.getString("swapStableCoinContractAdd")) .name() : stableName) .setMiddleCurrency(item.getString("swapMiddleCoinContract")) .setDexName(item.getString("dex_name")) .setDexProtocolVersion(item.getString("liquidity_type")) // 链id .setChainIds(Set.of(chainId)) .setStableCoinContractAddress(item.getString("swapStableCoinContractAdd")) .setMiddleCoinContractAddress(item.getString("swapMiddleCoinContractAdd")) .setTradeCoinContractAddress(item.getString("swapCoinContractAdd")) .setSwapContractAddress(item.getString("swapContract")) .setBurnFee(item.getBigDecimal("burn")) .setBuyTax(item.getBigDecimal("buyTax"))// 买入税率 .setSellTax(item.getBigDecimal("sellTax"))//卖出税率 // .setSellTax(item.getBigDecimal("burn"))// 卖出税率, 老的配置文件中就是sellTax .setDexBuySlipPoint(item.getBigDecimal("dexSlipPoint") .add(swapConfig.getGlobalDexSlipPoint())) .setStableCoinDecimals(item.getIntValue("stableCoinDecimals")) .setTradeCoinDecimals(item.getIntValue("coinDecimals")) .setMiddleCoinDecimals(item.getIntValue("middleCoinDecimals")) .setMethod(item.getString("method")) .setBuyPosition(item.getIntValue("currentDepthPosition")) .setReplyPro(item.getBigDecimal("replyPro")) // 反向利润放大的比例 .setDepthPro(item.getBigDecimal("depthPro")) // 反向深度缩小的比例 .setV3LoopContractAddress(item.getString("lpAdd")) // v3 的loopAddress .setV3Fee(item.getString("feev3")) // v3 的fee .setDexMaxOrderForUSDT(item.getBigDecimal("dexMax") == null ? swapConfig.getGlobalMaxOrderForUsdt() : item.getBigDecimal("dexMax")) // 反向最大下单量 .setDexMinOrderForUSDT(item.getBigDecimal("dexMin") == null ? swapConfig.getGlobalMinOrderForUsdt() : item.getBigDecimal("dexMin"));// 反向最小下单量 decenterSymbolInfo.setExtensionDexHandlerConfig(new ExtensionDexHandlerConfig(item)); decenterSymbolInfo.init(); symbolPairConfig.setDecenterSymbolInfo(decenterSymbolInfo); symbolPairConfig.set_rawJson(item); symbolLedger.put(symbolPairConfig); } return symbolLedger; } private static JSONArray getJsonArrayByFile(String symbolHuobiBooksFile) { File configFile = PathUtil.stairsLoad(symbolHuobiBooksFile, "config"); try { if (configFile == null) { log.warn("Not fount SymbolBook config file.{}", symbolHuobiBooksFile); String tempPath = System.getProperty("java.io.tmpdir") + System.currentTimeMillis(); String tempFile = Paths.get(tempPath, File.separator, symbolHuobiBooksFile) .toString(); Resource resource = new ClassPathResource(symbolHuobiBooksFile); InputStream initialStream = resource.getInputStream(); byte[] buffer = new byte[initialStream.available()]; initialStream.read(buffer); configFile = new File(tempFile); configFile.getParentFile() .mkdirs(); Files.write(buffer, configFile); log.info("Loading default SymbolBook config file from classpath: {} ", resource.getURL()); } String jsonContext = Joiner.on("") .join(Files.readLines(configFile, Charsets.UTF_8)); if (JSON.isValidArray(jsonContext)) { return JSON.parseObject(jsonContext, JSONArray.class); } } catch (Exception e) { log.error(e.getMessage(), e); throw new SwapException("SymbolBook config loading failed."); } return new JSONArray(); } public static SymbolLedger createXT(SwapConfig swapConfig) { return create(SYMBOL_XT_BOOKS_FILE, swapConfig); } public static SymbolLedger createHuoBi() { return create(SYMBOL_HUOBI_BOOKS_FILE, new SwapConfig()); } public static SymbolLedger createGateio() { return create(SYMBOL_GATEIO_BOOKS_FILE, new SwapConfig()); } public static SymbolLedger createGateio(SwapConfig swapConfig) { return create(SYMBOL_GATEIO_BOOKS_FILE, swapConfig); } public static SymbolLedger createKucoin(SwapConfig swapConfig) { return create(SYMBOL_KUCOIN_BOOKS_FILE, swapConfig); } public static SymbolLedger createOKex() { return create(SYMBOL_HUOBI_BOOKS_FILE, new SwapConfig()); } public static SymbolLedger createOKex(SwapConfig swapConfig) { return create(SYMBOL_HUOBI_BOOKS_FILE, swapConfig); } public static SymbolLedger createHuoBi(SwapConfig swapConfig) { return create(SYMBOL_HUOBI_BOOKS_FILE, swapConfig); } public static SymbolLedger createBinance(SwapConfig swapConfig) { return create(SYMBOL_BINANCE_BOOKS_FILE, swapConfig); } public static SymbolLedger createMEXC(SwapConfig swapConfig) { return create(SYMBOL_MEXC_BOOKS_FILE, swapConfig); } /** * 将配置文件内容刷入db * 外部通过SwapControlHolder 来对状态进行控制和判断 * * @param symbolLedger * @param exchangeType * @param isMonitor 开关用来控制swapSymbolPairConfigRecord 记录默认是监听状态还是, 非监听状态, * 首次程序启动的时候是非监听的,需要web端显式的开启,后续job中动态调整默认都是监听状态的 */ public static void flushToDb(SymbolLedger symbolLedger, ExchangeType exchangeType, boolean isMonitor) { if (symbolLedger.isMemoryMode()) { return; } SwapSymbolPairConfigRecordRepository res = SpringUtil.getBean(SwapSymbolPairConfigRecordRepository.class); res.deleteAllByExchangeType(exchangeType); res.flush(); List<SwapSymbolPairConfigRecord> all = Lists.newArrayList(); symbolLedger.getSymbolPairConfigs().forEach(symbolPairConfig -> { SwapSymbolPairConfigRecord swapSymbolPairConfigRecord = null; if (isMonitor) { swapSymbolPairConfigRecord = SwapSymbolPairConfigRecord.createLoadMonitor(symbolPairConfig, exchangeType); SwapControlManager.putItem(exchangeType, symbolPairConfig.getUniDexIdentify(), swapSymbolPairConfigRecord); } else { swapSymbolPairConfigRecord = SwapSymbolPairConfigRecord.createUnMonitor(symbolPairConfig, exchangeType); SwapControlManager.putItem(exchangeType, symbolPairConfig.getUniDexIdentify(), swapSymbolPairConfigRecord); } all.add(swapSymbolPairConfigRecord); }); res.saveAllAndFlush(all); log.info("Load config from config file, Flush to db is ok ! Record size is :{} ", all.size()); } /** * 增量更新配置文件 * 1. 数据库中载入当前交易所币配置 * 2. 从内存中获取当前交易所币配置 * 3. 比较数据库中不在内存的配置,更新状态 * 4. 将关闭的币种组装日志打印出来 */ public static void incrementalUpdate(SymbolLedger symbolLedger, ExchangeType exchangeType) { if (symbolLedger.isMemoryMode()) { return; } SwapSymbolPairConfigRecordRepository res = SpringUtil.getBean(SwapSymbolPairConfigRecordRepository.class); List<SwapSymbolPairConfigRecord> all = res.findAllByExchangeType(exchangeType); String changeCurrency = ""; int closeCount = 0; for (SwapSymbolPairConfigRecord swapSymbolPairConfigRecord : all) { String symbol = swapSymbolPairConfigRecord.getSymbol(); if (symbolLedger.getSymbolPairConfig(symbol.split("/")[0], symbol.split("/")[1]) == null) { swapSymbolPairConfigRecord.setMonitorStatus(SwapSymbolPairConfigRecord.MonitorStatus.UNMONITOR); changeCurrency += symbol + ","; closeCount++; } } res.saveAllAndFlush(all); String msg = Utils.format("{} 交易所内存币本配置已过滤,总数:{} 关闭数量:{} 关闭币种:{}", exchangeType, all.size(), closeCount, changeCurrency); log.info(msg); SwapMessageSender.sendMessage(exchangeType, msg); log.info("Incremental update config from config file, Flush to db is ok ! Record size is :{} ", all.size()); } SymbolPairConfig 类, 用来存储某个交易对(ABC/USDT)的配置信息 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100/** * <p> @Date : 2023/3/21 </p> * <p> @Project : block-farming</p> * * <p> @author konbluesky </p> */@Datapublic class SymbolPairConfig { //TODO 现阶段 symbolInfo 是1-1关系 /** * 交易对在中心化交易所的信息 */ private CenterSymbolInfo centerSymbolInfo; /** * 交易对在去中心化交易所的信息 */ private DecenterSymbolInfo decenterSymbolInfo; /** * 交易对显示名称 BTC/USDT */ public String getSymbol() { return baseCurrency+"/"+quoteCurrency; } /** * RITE/USDT-RITE/USDT-PancakeV2-UniV2 * @return */ public String getUniDexIdentify(){ return centerSymbolInfo.getSymbol() + "-" + decenterSymbolInfo.getSymbol() + "-" + decenterSymbolInfo.getDexName() + "-" + decenterSymbolInfo.getDexProtocolVersion(); } /** * 交易对基础货币 BTC */ private String baseCurrency; /** * 交易对报价货币 USDT */ private String quoteCurrency; /** * 允许开启的实例数 */ private AtomicInteger reverseAllowInstanceNum = new AtomicInteger(1); private AtomicInteger forwardAllowInstanceNum = new AtomicInteger(1); /** * 开启翻倍后,设置x分钟激情时间,进行翻倍 */ private AtomicLong reversePassionTime = new AtomicLong(0); private AtomicLong forwardPassionTime = new AtomicLong(0); /** * 交易对的利润阈值,一般在交易所holder初始化的时候从SwapConfig获取; * TODO 这里的阈值可基于全局的做覆盖; */ private BigDecimal profitThreshold; private BigDecimal profitThresholdRate; private BigDecimal _origin_profitThresholdRate; private JSONObject _rawJson; public void setProfitThresholdRate(BigDecimal profitThresholdRate) { this.profitThresholdRate = profitThresholdRate; this._origin_profitThresholdRate = profitThresholdRate; } public void setProfitThresholdRateDouble() { profitThresholdRate = profitThresholdRate.multiply(new BigDecimal("1.3")); } public void resetProfitThresholdRate() { profitThresholdRate = _origin_profitThresholdRate; } /** * 提现次数自增一次 * Increase WithdrawTime */ public void increaseWithdrawTimes() { centerSymbolInfo.getLastWithdrawTime().set(System.currentTimeMillis()); centerSymbolInfo.increaseWithdrawTimes(); } public boolean isMiddleCoin() { return !Strings.isNullOrEmpty(decenterSymbolInfo.getMiddleCoinContractAddress()); } public String getSymbolNoOblique() { return getSymbol().replace("/", ""); }} CenterSymbolInfo 类, 用来存储某个交易对(ABC/USDT)的Cex配置 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105/** * * <p> @Date : 2023/3/20 </p> * <p> @Project : block-farming</p> * * <p> @author konbluesky </p> */@Data@Slf4jpublic class CenterSymbolInfo { public String getSymbol() { return baseCurrency + "/" + quoteCurrency; } /** * 注意这里是[交易所]对应的基础货币 BTC */ private String baseCurrency; /** * 注意这里是[交易所]对应的交易对报价货币 USDT */ private String quoteCurrency; private Double price; /** * 交易所体现时一般需要指定链名或者网络名 */ private String chainName; /** * 提现次数 */ private AtomicInteger withdrawTimes = new AtomicInteger(0); /** * 最小提现手续费 */ private BigDecimal withdrawMinFee = BigDecimal.ZERO; /** * 手续费换算成美元 */ private BigDecimal withdrawMinFee$ = BigDecimal.ZERO; /** * 传入实时的单价,计算出手续费绝对刀 * @param unitPrice */ public void setWithdrawMinFee$(BigDecimal unitPrice) { this.withdrawMinFee$ = Utils.getScale(unitPrice.multiply(withdrawMinFee)); } /** * 最后一次的提现时间 */ private AtomicLong lastWithdrawTime = new AtomicLong(System.currentTimeMillis()); /** * 最后一次下卖单时间 */ private AtomicLong lastPlaceSellOrderTime = new AtomicLong(System.currentTimeMillis()); private AtomicLong lastPlaceBuyOrderTime = new AtomicLong(System.currentTimeMillis()); /** * 卖单默认位置,此位置是口语中的卖一卖二,不是数组下标 */ private AtomicInteger askPosition = new AtomicInteger(0); /** * 挂单检查时间(卖单) */ private int askOrderWaitSeconds = 5; /** * 交易所固定最大下单金额(usdt为单位) */ private BigDecimal CexMaxOrderForUSDT; /** * 交易所固定最小下单金额(usdt为单位) */ private BigDecimal CexMinOrderForUSDT; /** * 留币时保存的卖出下单ID; * remain 留存 sell 出售 orderId 订单ID */ private String remainSellOrderId; /** * 提现次数自增一次 */ public void increaseWithdrawTimes() { withdrawTimes.set(withdrawTimes.get() + 1); }} DecenterSymbolInfo 类, 用来存储某个交易对(ABC/USDT)的Dex配置 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245/** * <p> @Date : 2023/3/20 </p> * <p> @Project : block-farming</p> * * <p> @author konbluesky </p> */@Accessors(chain = true)@Data@Slf4jpublic class DecenterSymbolInfo { private String dexName; private String dexProtocolVersion; public String getSymbol() { if(Strings.isNullOrEmpty(middleCoinContractAddress)) { return baseCurrency + "/" + quoteCurrency; }else{ return baseCurrency + "/" + middleCurrency + "/" + quoteCurrency; } } /** * 注意这里是[链上]对应的基础货币 BTC */ private String baseCurrency; /** * 注意这里是[链上]对应的交易对报价货币 USDT */ private String quoteCurrency; /** * 注意这里是[链上]对应的交易对中间报价货币 */ private String middleCurrency; private Set<Integer> chainIds; /** * TODO 未来需要支持一个币种在多个链上的支持, 目前只考虑币种只在一个链上活跃 * @return */ public Integer getFirstChainId(){ return chainIds.iterator().next(); } /**************************************************************************************************/ /************************************** 链上的地址配置信息 *******************************************/ /**************************************************************************************************/ /** * 为了方便存取,围绕token,将常用的信息包装起来 */ private CurrencyBag stable; private CurrencyBag trade; private CurrencyBag middle; public void init() { if (stable == null) { stable = CurrencyBag.of(stableCoinContractAddress, quoteCurrency, getStableCoinDecimals()); } if (trade == null) { trade = CurrencyBag.of(tradeCoinContractAddress, baseCurrency, getTradeCoinDecimals()); } if (middle == null) { middle = CurrencyBag.of(middleCoinContractAddress, middleCurrency, getMiddleCoinDecimals()); } } /** * 稳定币的合约地址 * USTD,ETH,BTC 等 */ private String stableCoinContractAddress; /** * 中间币的合约地址 * BNB 有自己独立的链的等 */ private String middleCoinContractAddress; /** * 交易币的合约地址 * 即最终操作的目标币地址 * 原代码中的:swapCoinContractAdd */ private String tradeCoinContractAddress; /** * v3流通性的合约地址 */ private String v3LoopContractAddress; /** * v3手续费的 */ private String v3Fee; /** * 去中心化交易所的合约地址 */ private String swapContractAddress; /** * 反向套利,从蛋糕到交易所,稳定币额度 * bnb对就进行交易所价格查询,转换成对应的bnb * <pre> * 声明 BidDepths,bidOnePrice,BidDepthsAllAmount; * swapStableCoinNumOut = bidOnePrice * BidDepthsAllAmount; * </pre> */ private AtomicDouble swapStableCoinNumOut; /** * 非稳定币的兑换数量,个数 */ private AtomicDouble swapFStableCoinNum; /** * 稳定币的兑换数量,个数 */ private AtomicDouble swapStableCoinNum; /** * 价格精度 最小就是0.0001 */ private String priceDecimals; /** * 数量精度 最小就是0.01 */ private String numDecimals; /** * 卖时燃烧的手续费 */ private BigDecimal burnFee; /** * 其他费用,初步用来保存转移费用,有些币种的转移费用 */ private BigDecimal otherFee = BigDecimal.ZERO; /** * 链上买币滑点 */ private BigDecimal dexBuySlipPoint; private BigDecimal _origin_dexBuySlipPoint; /** * 买时 税率 */ private BigDecimal buyTax = BigDecimal.ZERO; /** * 卖时 税率 */ private BigDecimal sellTax = BigDecimal.ZERO; /** * 交易币的精度,在程序启动时通过TokenClient从链上合约中查询 */ private int tradeCoinDecimals = 0; /** * 稳定币的精度,在程序启动时通过TokenClient从链上合约中查询 */ private int stableCoinDecimals = 0; /** * 中间币的精度,在程序启动时通过TokenClient从链上合约中查询 */ private int middleCoinDecimals = 0; /** * 链上固定最大下单金额(usdt为单位) */ private BigDecimal dexMaxOrderForUSDT; /** * 链上固定最小下单金额(usdt为单位) */ private BigDecimal dexMinOrderForUSDT; /** * dex购买状态,用于判断是否蛋糕买了,然后回交易所 * 对于这种情况,要进行停止蛋糕购买及交易所到蛋糕的业务 * 根据交易所充值到账时间 一般需要2分钟,即120秒,那130秒内都必须把价格降 */ private AtomicLong dexBuyTimestamp = new AtomicLong(0); private String method; /** * 反向套利时,需要监控买盘的数据, 这是买盘的位置 * 对应的是centerSymbolInfo的askPosition */ private int buyPosition; /** * 反向套利时,深度需要*depthPro ,进行打折 */ private BigDecimal depthPro; public DecenterSymbolInfo setDexBuySlipPoint(BigDecimal dexBuySlipPoint) { this.dexBuySlipPoint = dexBuySlipPoint; // 保留原始值用于恢复 this._origin_dexBuySlipPoint = dexBuySlipPoint; return this; } private ExtensionDexHandlerConfig extensionDexHandlerConfig; /** * 反向套利时的放大比例 */ private BigDecimal replyPro; public boolean isV3(){ return getDexProtocolVersion().toLowerCase().contains("v3".toLowerCase()); } public boolean isV2(){ return getDexProtocolVersion().toLowerCase().contains("v2".toLowerCase()); } /** * 对dexBuySlipPoint滑点1.5倍; */ public void setDexBuySlipPointDouble() { dexBuySlipPoint = dexBuySlipPoint.multiply(replyPro); } public void resetDexBuySlipPoint() { dexBuySlipPoint = _origin_dexBuySlipPoint; }}","link":"/2024/11/05/carry_coin_architecture_4/"},{"title":"Carry-Coin 中 Jasypt 的应用","text":"自从最近L君小弟电脑中毒,疑似私钥泄漏,导致3W u被盗,反观我的程序一直以来裸奔的状态下,再次菊花一紧。于是,我开始了对Spring Boot的jasypt加密配置的研究。 最坏情况服务器即使被黑,也要尽可能保证程序的安全。 配置文件中关键信息: 交易所token,钱包私钥加密; jar中yml打包时排除,服务器上用密文配置执行; 解密密码动态设置; jasypt 集成程序引入pom.xml 12345 <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.5</version></dependency> application.yml 123jasypt: encryptor: password: ${JASYPT_ENCRYPTOR_PASSWORD} Application.java 12345678910111213141516171819202122/** * <p> @Date : 2023/3/19 </p> * <p> @author konbluesky </p> */@SpringBootApplication(scanBasePackages = "com.block")@EnableScheduling@EnableAsync@EnableJpaAuditing// WARN bad smell@EncryptablePropertySources({ @EncryptablePropertySource(value = "file:./config/application.yml",ignoreResourceNotFound = true), @EncryptablePropertySource(value = "classpath:application.yml",ignoreResourceNotFound = true)})@EnableEncryptablePropertiespublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }} 打包排除pom.xml,设置打包时排除yml文件,避免铭文yml误打包到jar中 123456789101112 <resources> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.*</include> </includes> <!-- 用springboot 打包时 排除yml--> <excludes> <exclude>**/*.yml</exclude> </excludes> </resource></resources> 启动脚本原启动方式需要通过-Djasypt.encryptor.password=”the password”,来设置加密密码。这种方式用jps -mlv可以看到启动命令,但是这种方式不安全,依然容易泄露密码。为了安全起见,我们需要在启动脚本中设置环境变量 JASYPT_ENCRYPTOR_PASSWORD 来加密配置文件中的敏感信息。设置后又担心环境变量被echo出来,所以需要设置环境变量时,需要隐藏输入内容同时控制变量作用域在shell内部; 最终脚本如下,关键语句是export和unset; 12345678910111213141516171819202122232425262728293031323334353637383940#!/bin/bash# 获取当前脚本所在目录的绝对路径SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"# 进入脚本所在目录cd "$SCRIPT_DIR"# 提示用户输入 JASYPT_ENCRYPTOR_PASSWORD,并隐藏输入内容read -sp "Enter App password: " JASYPT_PASSWORDechoexport JASYPT_ENCRYPTOR_PASSWORD=$JASYPT_PASSWORD# 检查是否输入了 JASYPT_ENCRYPTOR_PASSWORDif [ -z "$JASYPT_PASSWORD" ]; then echo "jasypt.encryptor.password is required." exit 1fi# 启动Java程序并使用nohup确保它在后台运行nohup java -jar $(ls -t trade-monitor-*.jar | head -1) \\ --spring.config.location=file:./config/ \\ --spring.application.name=gateio\\ -XX:+PrintGCDetails \\ -XX:+PrintGCDateStamps \\ -Xloggc:$SCRIPT_DIR/gc.log \\ -Xmx16G \\ -Xms8G \\ -XX:+UseG1GC \\ -XX:NewRatio=3 \\ -XX:SurvivorRatio=8 \\ -XX:MaxMetaspaceSize=1024M \\ -XX:MaxGCPauseMillis=200 \\ -Xss1M >/dev/null 2>&1 &# 清除环境变量unset JASYPT_ENCRYPTOR_PASSWORD","link":"/2024/07/13/carry_coin_jasypt/"},{"title":"使用 Python 脚本清理 Carry-Coin 程序中的 Logback 日志","text":"随着监控的币种越来越多,我的 Carry-Coin 程序中日志数据的日质量也不断增加,每天的日志文件大小达到约 40G。这不仅占用了大量的存储空间,还给日志分析带来了挑战。为了更有效地管理这些日志,我已经编写了一个程序来提取日志中与交易相关的信息,供后续分析使用。 然而,在配置 Logback 的 logback-spring.xml 文件时,我设置了 appender.rollingPolicy.maxHistory 字段,旨在只保留最近 2 天的日志。然而,这一配置并未如预期生效。因此,我决定暂时使用 Python 脚本来手动清理旧的日志文件。 问题背景在 Carry-Coin 程序中,随着对越来越多币种的监控,日志文件的大小急剧增加,导致每天产生的日志文件约为 40G。虽然我已编写程序提取交易信息,但大量的日志数据依然占用了过多的磁盘空间。为此,我尝试通过调整 Logback 的配置来限制保留的日志数量。 Logback 日志管理为了管理日志,我在 logback-spring.xml 文件中配置了以下内容: 12345678910<appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/carry-coin.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/carry-coin.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>2</maxHistory> <!-- 只保留最近2天的日志 --> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern> </encoder></appender> 尽管如此,maxHistory 的配置并未生效,因此需要寻找替代解决方案。 Python 脚本实现我编写了一个 Python 脚本来自动清理日志文件,保留最近 2 天的日志。以下是脚本的实现: 12345678910111213141516171819202122import osimport timefrom datetime import datetime, timedelta# 获取两天前的时间days_to_keep = 2cutoff_time = datetime.now() - timedelta(days=days_to_keep)# 定义日志目录log_directory = "/path/to/logs"# 遍历日志目录,删除修改时间在 cutoff_time 之前的 .log 文件for filename in os.listdir(log_directory): file_path = os.path.join(log_directory, filename) if os.path.isfile(file_path) and filename.endswith(".log"): file_mtime = os.path.getmtime(file_path) file_mtime_dt = datetime.fromtimestamp(file_mtime) if file_mtime_dt < cutoff_time: os.remove(file_path) print(f"Deleted: {file_path}") 说明 脚本获取当前时间的两天前,遍历指定日志目录,删除修改时间在该时间之前的 .log 文件。 仅处理以 .log 结尾的文件,确保只删除日志文件。 设置 Cron 定时任务为了定期执行这个清理脚本,我使用 cron 设置了一个定时任务,以下是设置步骤: 打开 crontab 编辑器: 1crontab -e 添加如下定时任务,每天凌晨 1 点执行: 10 1 * * * /usr/bin/python3 /path/to/your_script.py >> /path/to/cron_log.log 2>&1 保存并退出 crontab。 总结通过使用 Python 脚本,我能够有效地管理 Carry-Coin 程序中生成的日志文件,避免了因日志文件过大导致的磁盘空间不足的问题。虽然 Logback 的配置未能如预期生效,但临时解决方案为我带来了便利。在未来的项目中,我将继续探索更好的日志管理策略,以确保程序高效运行。 参考文档 Logback 文档","link":"/2024/02/13/carry_coin_log_cleanup/"},{"title":"Google 2FA 脚本","text":"批量显示Google 2FA 工具,5秒刷新一次 secrets.csv 123456789101112131415username,secretiAvloyola,ENOG7VLRJJ7GDNZJLilBlue561,IWPTHMCHSR74EOSHKwaciWorsnop,TUXN5TNLTKOPTJEYvikeshchotai,L333DPD5JHXF3O2Kperaltasocimo,UH4EDK7425BFCXSHmakzuzu,OUIG375O7OARDXLJJsaFikitha,AIWBIBQGXZK3ZDE3limliangtung,HHD2VM5LHCV6TDOVsinhde18,L6LYS4ECQL5KKPFFActionDT,O64IEGT5NPCOHCQtimansur,3ASN5ZIGJH4ICYUXDarmyrez,BTKNH5OMOMPZ7CFYaninditamario,G2VHH4IHFE367PNWizzanfurkan,EYP7VV6AK6EIDIJB 批量显示验证码脚本 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556import csvimport datetimeimport pyotpimport timeimport osimport base64# 定义读取CSV文件的函数def read_secrets_from_csv(csv_file_path): secrets = [] with open(csv_file_path, mode='r') as file: reader = csv.DictReader(file) for row in reader: secrets.append(row) return secrets# 定义验证Base32密钥的函数def is_valid_base32(secret): try: # 尝试解码,如果失败则说明不是有效的Base32 base64.b32decode(secret, casefold=True) return True except (base64.binascii.Error, ValueError): return False# 定义生成2FA验证码的函数def generate_2fa_codes(secrets): codes = [] for secret in secrets: if is_valid_base32(secret['secret']): totp = pyotp.TOTP(secret['secret']) code = totp.now() codes.append({'username': secret['username'], 'code': code}) else: codes.append({'username': secret['username'], 'code': '无效的Base32密钥'}) return codes# 主函数def main(): csv_file_path = '2fa/secrets.csv' # 请根据实际情况修改CSV文件路径 secrets = read_secrets_from_csv(csv_file_path) while True: os.system('cls' if os.name == 'nt' else 'clear') # 清屏 codes = generate_2fa_codes(secrets) print(f"时间:{datetime.datetime.now()}") for entry in codes: print(f"用户 {entry['username']} 的当前2FA验证码是: {entry['code']}") time.sleep(5) # 每30秒刷新一次if __name__ == "__main__": main()","link":"/2024/11/03/google-2fa/"},{"title":"Hummingbot Create First Strategy Bot","text":"使用simple_amm策略创建第一个机器人 使用命令: 123start --script [SCRIPT NAME]create --script-config [SCRIPT_FILE]start --script [SCRIPT_FILE] --conf [SCRIPT_CONFIG_FILE] 初始的文件目录 12345678910111213141516(hummingbot) root@vmi2090919:~/hummingbot/conf# tree.├── __init__.py├── conf_client.yml├── conf_fee_overrides.yml├── connectors│ ├── __init__.py│ ├── mexc.yml│ └── okx.yml├── controllers│ └── __init__.py├── hummingbot_logs.yml├── scripts│ └── __init__.py└── strategies └── __init__.py hummingbot cli中 通过下列指令创建基本策略 123456789101112131415161718>>> create --script-config simple_pmm For more information, please visit https://pyperclip.readthedocs.io/en/latest/introduction.html#not-implemented-erro r (See log file for stack trace dump)Exchange where the bot will trade >>> mexcTrading pair in which the bot will place orders >>> MX-USDTOrder amount (denominated in base asset) >>> 0.01Bid order spread (in percent) >>> 0.001Ask order spread (in percent) >>> 0.001Order refresh time (in seconds) >>> 15Price type to use (mid or last) >>> midEnter a new file name for your configuration >>> conf_simple_pmm_1.ymlA new config file has been created: conf_simple_pmm_1.yml ps: 配置过程中要终止配置的话,使用快捷键ctrl+x 查看文件目录,新配置已生成 1234567891011121314151617(hummingbot) root@vmi2090919:~/hummingbot/conf# tree.├── __init__.py├── conf_client.yml├── conf_fee_overrides.yml├── connectors│ ├── __init__.py│ ├── mexc.yml│ └── okx.yml├── controllers│ └── __init__.py├── hummingbot_logs.yml├── scripts│ ├── __init__.py│ └── conf_simple_pmm_1.yml└── strategies └── __init__.py conf_simple_pmm_1.yml 内容 12345678script_file_name: simple_pmm.pyexchange: mexctrading_pair: MX-USDTorder_amount: 0.01bid_spread: 0.001ask_spread: 0.001order_refresh_time: 15price_type: mid 使用start --script simple_pmm --conf conf_simple_pmm_1.yml 启动bot 错误123456789102024-11-15 13:10:31,262 - 2684330 - hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange - INFO - Network status has changed to NetworkStatus.CONNECTED. Starting networking...2024-11-15 13:10:31,715 - 2684330 - hummingbot.connector.exchange.mexc.mexc_api_order_book_data_source.MexcAPIOrderBookDataSource - ERROR - Unexpected error occurred subscribing to order book trading and delta streams...Traceback (most recent call last): File "/root/hummingbot/hummingbot/connector/exchange/mexc/mexc_api_order_book_data_source.py", line 76, in _subscribe_channels symbol = await self._connector.exchange_symbol_associated_to_pair(trading_pair=trading_pair) File "hummingbot/connector/exchange_base.pyx", line 97, in exchange_symbol_associated_to_pair return symbol_map.inverse[trading_pair] File "/root/miniconda3/envs/hummingbot/lib/python3.10/site-packages/bidict/_base.py", line 524, in __getitem__ return self._fwdm[key]KeyError: 'MX-USDT' Mexc 中并不是所有交易对都支持api交易的,交易对要到https://api.mexc.com/api/v3/defaultSymbols接口中检查下换了个PNUT-USDT交易对,正常交易了。 关于 PMM with Price Shift and Dynamic Spreads simple_amm.py 使用的策略 参考资料关于 PMM with Price Shift and Dynamic Spreads","link":"/2024/11/15/hummingbot_create_simple_pmm_bot/"},{"title":"Hummingbot:开源的加密货币高频交易机器人","text":"Hummingbot 是一个开源的高频交易机器人框架,旨在为加密货币市场提供自动化交易工具。无论是市场做市(market making)、套利(arbitrage),还是跨交易所市场做市(cross-exchange market making),Hummingbot 都为用户提供了多种实用的策略模板,帮助用户轻松上手高频交易,参与到加密货币交易市场中。 Hummingbot 的主要特点1. 开源与社区驱动Hummingbot 是一个开源项目,其代码公开透明,用户可以根据自身需求对代码进行修改,也可以为社区贡献代码。社区驱动的开发模式使得 Hummingbot 不断进步,拥有活跃的支持和开发者社群,并定期发布更新和新功能。 2. 支持多种交易所Hummingbot 支持多个主流的中心化交易所(CEX)和去中心化交易所(DEX),如 Binance、Coinbase Pro、FTX、Uniswap、Balancer 等。它具有一个插件系统,使得开发者可以为还不支持的交易所编写接口,以满足更多的市场需求。 3. 灵活的策略配置Hummingbot 提供多种内置交易策略模板,涵盖了: 简单市场做市(Market Making): 通过挂买卖单,赚取价差。 套利(Arbitrage): 利用不同交易所之间的价格差获利。 跨交易所做市(Cross-Exchange Market Making): 在多个交易所间挂单捕捉价差。 用户可以通过配置文件轻松调整策略参数,例如控制交易频率、订单大小、价差范围等,使得策略更加灵活、适应不同市场情况。 4. 用户友好的 CLI 界面Hummingbot 提供了简洁的命令行界面(CLI),用户可以快速配置和监控机器人的运行状态,并且支持实时监控和日志记录。对于希望快速上手交易的用户来说,这个界面非常友好。 5. 强大的策略开发支持对于有编程基础的用户,Hummingbot 支持 Python 自定义交易策略。用户可以根据市场需求和个人交易风格调整策略,例如自定义套利触发条件、调整加仓和减仓逻辑等。这一特性让 Hummingbot 成为一个灵活的量化交易框架。 6. 灵活的部署方式Hummingbot 支持本地部署,也可以部署在云服务器上,适应不同用户的需求。它能够与各类外部数据源、交易所的 API 无缝集成,适合于大规模实时交易需求。 Hummingbot 的应用场景Hummingbot 支持的策略广泛适用于以下场景: 市场做市(Market Making): 提供流动性,通过在买卖之间的价差获利。 套利交易(Arbitrage): 抓住不同交易所之间的价差机会获利。 跨交易所做市(Cross-Exchange Market Making): 同时在两个或多个交易所挂买卖单,以捕捉价差。 总结Hummingbot 是一个高度灵活且功能强大的开源交易机器人,对希望参与加密货币高频交易的开发者和量化交易者来说,是一个值得尝试的选择。无论你是交易新手,还是经验丰富的量化交易员,Hummingbot 都提供了丰富的功能和高效的工具,助你在加密货币市场中捕捉更多机会。","link":"/2024/11/09/hummingbot_info/"},{"title":"Hummingbot Dashboard","text":"Hummingbot Dashboard Hummingbot Dashboard 是一款开源应用,旨在帮助用户创建、回测和优化各种算法交易策略。一旦策略得到完善,它们可以作为 Hummingbot 实例部署到实盘交易模式中,从策略制定到实际交易执行实现无缝衔接。 功能 机器人编排:部署和管理多个 Hummingbot 实例 策略回测与优化:通过历史数据评估策略表现,并使用 Optuna 进行优化 一键部署:轻松将策略部署为 Hummingbot 实例,支持模拟或实盘交易 性能分析监控:监控并分析已部署策略的表现 凭证管理:创建和管理 API 密钥的独立账户 文档:https://hummingbot.org/dashboard/ 安装Dashboard 两种方式build from sourcehttps://github.com/hummingbot/dashboard#installation docker先装docker compose 12345678sudo curl -L "https://github.com/docker/compose/releases/download/v2.28.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-composesudo chmod +x /usr/local/bin/docker-composedocker-compose --versiongit clone https://github.com/hummingbot/deploycd deploybash setup.sh 参考资料 Hummingbot stategies_v1","link":"/2024/11/15/hummingbot_dashboard/"},{"title":"Hummingbot 目录结构","text":"Hummingbot 目录结构 安装后目录123456789hummingbot ┣ conf ┣ connectors ┣ strategies ┣ scripts ┣ logs ┣ data ┣ scripts ┣ hummingbot /conf:配置文件的通用文件夹 /conf/connectors:配置 Exchange API 密钥 /conf/strategies:配置策略文件,在cli-ui中通过create和import命令创建或导入策略 /conf/scripts:编写脚本配置文件,create --script-config /logs:脚本和策略生成的日志文件 /data:用于记录脚本和策略执行的交易的 SQLite 数据库和 CSV 文件 /scripts:此文件夹包含示例脚本,可以在此处添加新脚本,以使其可供start命令使用","link":"/2024/11/12/hummingbot_post_install/"},{"title":"Hummingbot Install(macos)","text":"Hummingbot Macos 安装步骤 安装要求(macos) Component Specification Operating System MacOS 12+ - Intel x86 or Apple Silicon (M1 / M2 / M3) Memory 4 GB RAM per instance Storage 5 GB HDD space per instance CPU At least 1 vCPU per instance / controller 1xcode-select --install 官方建议用conda做环境MacOS with Intel x86: 12curl -o Miniconda3-latest-MacOSX-x86_64.sh https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.shbash Miniconda3-latest-MacOSX-x86_64.sh MacOS with Apple Silicon (M1 / M2 / M3): 12curl -o Miniconda3-latest-MacOSX-x86_64.sh https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.shbash Miniconda3-latest-MacOSX-x86_64.sh 12345git clone https://github.com/hummingbot/hummingbot.gitcd hummingbot./installconda activate hummingbot./compile 如果安装后conda命令无法识别,可以尝试将miniconda3安装目录下的bin目录添加到环境变量中。/root/.bashrc中添加:export PATH="/Users/your_username/miniconda3/bin:$PATH"然后执行source /root/.bashrc使环境变量生效。 1./start.sh 启动界面 在命令行ui界面,设置密码后进入主界面 参考资料 Hummingbot Install(macos)","link":"/2024/11/11/hummingbot_install/"},{"title":"BSC节点区块监控脚本","text":"脚本主要监听私有BSC节点区块状态,如发生区块漏块过多,发送告警消息到DD群中,carry-coin调整rpc访问策略; 监控脚本 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123from web3 import Web3import timeimport threadingimport sysimport loggingfrom logging.handlers import TimedRotatingFileHandlerimport osfrom stop_event_trigger import StopEventTrigger# 日志配置log_dir = "logs"if not os.path.exists(log_dir): os.makedirs(log_dir)app_name="block_monitor-7.143"can_call=Falsestop_event_trigger = StopEventTrigger()log_file = os.path.join(log_dir, "block_monitor.log")# 创建一个TimedRotatingFileHandler,用于按照日期切割日志文件file_handler = TimedRotatingFileHandler(log_file, when="midnight", interval=1, backupCount=7, encoding='utf-8')file_handler.setFormatter(logging.Formatter(app_name+' %(asctime)s - %(levelname)s - %(message)s'))# 创建一个StreamHandler,用于将日志输出到控制台console_handler = logging.StreamHandler()console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))# 配置root loggerlogging.basicConfig(level=logging.INFO, handlers=[file_handler, console_handler])from chatbot import DingtalkChatbot# 配置 DingTalk 消息通知webhook = "https://oapi.dingtalk.com/robot/send?access_token={机器人TOKEN}"secret = "SEC开头密钥"dd = DingtalkChatbot(webhook, secret=secret, fail_notice=True)private_rpc_url = "http://私有节点ip:8545/"# 公共节点public_rpc_urls = [ "https://bsc-dataseed.binance.org", "https://bsc-dataseed1.defibit.io", "https://bsc-dataseed1.binance.org", "https://bsc-dataseed2.defibit.io", "https://bsc-dataseed3.ninicoin.io"]def get_ethereum_block_height(rpc_url): start_time = time.time() # 记录开始时间 web3 = Web3(Web3.HTTPProvider(rpc_url)) block_height = web3.eth.block_number end_time = time.time() # 记录结束时间 elapsed_time = end_time - start_time logging.info(f"节点 {rpc_url} 当前区块高度: {block_height},查询耗时: {elapsed_time:.3f}秒") return block_heightdef send_dingtalk_message(message): dd.send_text(msg=message)def monitor_block_height(private_rpc_url, public_rpc_urls): inspection_timer = 0 # 初始化巡检计时器,单位为秒 while True: try: my_block_height = get_ethereum_block_height(private_rpc_url) # 获取所有节点的区块高度 public_block_heights = [get_ethereum_block_height(url) for url in public_rpc_urls] logging.info(f"私有节点高度:{my_block_height} 所有节点的区块高度: {public_block_heights}") # 找到最大的区块高度 max_block_height = max(public_block_heights) logging.info(f"最大区块高度: {max_block_height}") # 比对节点是否领先 # 如果私有节点的区块高度小于最大区块高度,并且区块差异大于3 则发送警告 diff_height = max_block_height - my_block_height if my_block_height < max_block_height and diff_height > 3: # if my_block_height < max_block_height: currentTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) message = f"{app_name}私有节点 {my_block_height} 低于于其他节点,最大区块高度为 {max_block_height},差异:{diff_height} 时间 {currentTime}" logging.warning(message) # 使用警告级别的日志 send_dingtalk_message(message) if can_call: stop_event_trigger.pin_point_and_stop_engine_event() # 每隔10分钟发送一次巡检记录 if inspection_timer >= 1800: currentTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) inspection_message = f"{app_name} 巡检记录:私有节点 {my_block_height}," inspection_message += f"公共节点最大区块高度: {max_block_height}," inspection_message += f"时间 {currentTime}" logging.info(inspection_message) send_dingtalk_message(inspection_message) inspection_timer = 0 # 重置计时器 except Exception as e: logging.error(f"发生错误:{e}") time.sleep(5) inspection_timer += 5 # 每次循环增加计时器的时间,单位为秒if __name__ == "__main__": # 读取命令行传入参数 if len(sys.argv) > 1: app_name = sys.argv[1] if app_name is None: app_name = "block_monitor" if len(sys.argv) > 2: canCall = sys.argv[2] if canCall is None: canCall = False if can_call: stop_event_trigger.pin_point_and_stop_engine_event(app_name) logging.info(f"app_name: {app_name}") t = threading.Thread(target=monitor_block_height, args=(private_rpc_url, public_rpc_urls)) t.start() t.join() 启动脚本 1234567891011121314#!/bin/bash# 增加一个启动参数,app_name,如传入则使用传入的app_name,并加入到启动命令中app_name=$1# 设置启动日志文件路径start_log_file="logs/start_script.log"# 启动 Python 脚本,并将输出保存到启动日志文件nohup python3.10 monitor.py $app_name > $start_log_file 2>&1 &# 获取启动的 Python 进程的PID并保存到文件中echo $! > pid_file.txtecho "脚本已在后台运行。PID为:$(cat pid_file.txt)" 停止脚本 12345678910111213141516#!/bin/bash# 获取之前保存的PID文件pid_file="pid_file.txt"pid=$(cat $pid_file 2>/dev/null)if [ -z "$pid" ]; then echo "未找到PID。脚本可能未在运行。"else # 终止Python进程 kill -TERM $pid echo "PID为 $pid 的脚本已停止。" # 删除PID文件 rm $pid_filefi","link":"/2024/11/02/node-monitor/"},{"title":"Hummingbot Strategies v2","text":"组件v2 对比 v1 来说,架构做了调整,多了几个组件更好的工作和解耦 脚本(Script):所有策略的入口点,这个Python文件负责协调整个策略的执行。它可以是一个包含所有策略逻辑的简单文件,或者是一个加载一个或多个控制器的文件。 市场数据提供器(Market Data Provider):用于访问交易所的市场数据的单一入口,比如历史OHCLV(开盘价、高点、低点、收盘价、成交量)K线数据、订单簿数据和交易记录。 执行器(Executor):根据用户预设管理订单和仓位,确保根据策略指令下单、修改或取消订单。 控制器(Controller):基于策略控制器的基础类(如方向性策略或做市策略)定义一个交易策略。 继承关系 V1 策略 策略基类(StrategyBase):StrategyBase 是所有策略的 Cython 基类,而 StrategyPyBase 继承自它,并作为所有 Python 基础策略的根类。 V1 脚本(Scripts):ScriptStrategyBase 是在上述类的基础上构建的,创建简单策略变得更加容易。这个类目前仍然支持,但后面可能会被弃用。所以建议在新脚本实现中使用 StrategyV2Base。 控制器与 V2 脚本 V2 策略基类(StrategyV2Base):StrategyV2Base 继承自 ScriptStrategyBase,但它使用执行器(Executors)来管理订单,而不再通过 buy() / sell() 方法。控制器(Controllers)在此基础上进一步扩展,作为通过事件队列松散耦合的附加组件。 请务必牢记继承结构,这会大大帮助理解如何编写自己的自定义策略。 参考资料 Hummingbot stategies_v1","link":"/2024/11/15/hummingbot_strategy_v2/"},{"title":"Carry-Coin 记服务迁移和流量优化","text":"最近Contabo服务器频繁死机,发邮件给官方反应问题,一开始嘴硬说没问题让自查,沟通2天又是截图又是各种开票,最后承认问题说技术团队排查但是不给解决时间 邮件回复 VNC过去看到 sda3硬盘一直挂不上/dev/sda3: recovering journal,猜测不是硬件存储坏了就是虚拟化平台抽风;好在重启5-8次大概能进系统一次,赶紧拷数据闪人; 目前部署架构 原来contabo的机器8u24g,32TTraffic一个月$26, 相同配置国内厂商看了一圈没有羊毛,最后选择tx,但是相同配置明显贵上天,只能调整架构先开台低配2u4g/90ssd轻量服务器,把front、server、db弄回来,worker后面再说; 程序迁移后大问题没有,每种不足就是出口流量太吃紧了,轻量应用流量包只有2T,跑了10个小时流量80多G,一天毛估估200G; 优化过程iftop大概观察下流量去向,其实心里也有数,整机对外一个是通过nginx访问的front 这是给自己看的前端,还有一个就是mysql,几个worker每秒多线程读写,这部分传输过程中流量花费巨大;资源的话cpu的使用率基本维持在50左右,内存40%左右,优化空间还是有的 jeecgboot 前端肿的不行,所以nginx gzip该压的压起来,改了后观察监控发现提升可以忽略不计。 mysql是大头,这块翻了一些优化料大部分都是持久化侧的,表压缩之类的;所以换了个思路,既然是传输过程中的损耗那么大概率是jdbc驱动的事情,这么常规的场景应该有支持; 翻了mysql-connector-j-8.0.33.jar代码发现果然有戏com.mysql.cj.protocol.a.NativeProtocol中有个字段useCompression开启后mysql的传输过程会压缩,但是默认是关闭的,可以在jdbc连接字符串后面加上useCompression=true来开启压缩; CompressedPacketSender开关打开后会使用com.mysql.cj.protocol.a.CompressedPacketSender来发送数据; 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697/** * Packet sender implementation for the compressed MySQL protocol. For compressed transmission of multi-packets, split the packets up in the same way as the * uncompressed protocol. We fit up to MAX_PACKET_SIZE bytes of split uncompressed packet, including the header, into an compressed packet. The first packet * of the multi-packet is 4 bytes of header and MAX_PACKET_SIZE - 4 bytes of the payload. The next packet must send the remaining four bytes of the payload * followed by a new header and payload. If the second split packet is also around MAX_PACKET_SIZE in length, then only MAX_PACKET_SIZE - 4 (from the * previous packet) - 4 (for the new header) can be sent. This means the payload will be limited by 8 bytes and this will continue to increase by 4 at every * iteration. * * @param packet * data bytes * @param packetLen * packet length * @param packetSequence * sequence id * @throws IOException * if i/o exception occurs */ public void send(byte[] packet, int packetLen, byte packetSequence) throws IOException { this.compressedSequenceId = packetSequence; // short-circuit send small packets without compression and return if (packetLen < MIN_COMPRESS_LEN) { writeCompressedHeader(packetLen + NativeConstants.HEADER_LENGTH, this.compressedSequenceId, 0); writeUncompressedHeader(packetLen, packetSequence); this.outputStream.write(packet, 0, packetLen); this.outputStream.flush(); return; } if (packetLen + NativeConstants.HEADER_LENGTH > NativeConstants.MAX_PACKET_SIZE) { this.compressedPacket = new byte[NativeConstants.MAX_PACKET_SIZE]; } else { this.compressedPacket = new byte[NativeConstants.HEADER_LENGTH + packetLen]; } PacketSplitter packetSplitter = new PacketSplitter(packetLen); int unsentPayloadLen = 0; int unsentOffset = 0; // loop over constructing and sending compressed packets while (true) { this.compressedPayloadLen = 0; if (packetSplitter.nextPacket()) { // rest of previous packet if (unsentPayloadLen > 0) { addPayload(packet, unsentOffset, unsentPayloadLen); } // current packet int remaining = NativeConstants.MAX_PACKET_SIZE - unsentPayloadLen; // if remaining is 0 then we are sending a very huge packet such that are 4-byte header-size carryover from last packet accumulated to the size // of a whole packet itself. We don't handle this. Would require 4 million packet segments (64 gigs in one logical packet) int len = Math.min(remaining, NativeConstants.HEADER_LENGTH + packetSplitter.getPacketLen()); int lenNoHdr = len - NativeConstants.HEADER_LENGTH; addUncompressedHeader(packetSequence, packetSplitter.getPacketLen()); addPayload(packet, packetSplitter.getOffset(), lenNoHdr); completeCompression(); // don't send payloads with incompressible data if (this.compressedPayloadLen >= len) { // combine the unsent and current packet in an uncompressed packet writeCompressedHeader(unsentPayloadLen + len, this.compressedSequenceId++, 0); this.outputStream.write(packet, unsentOffset, unsentPayloadLen); writeUncompressedHeader(lenNoHdr, packetSequence); this.outputStream.write(packet, packetSplitter.getOffset(), lenNoHdr); } else { sendCompressedPacket(len + unsentPayloadLen); } packetSequence++; unsentPayloadLen = packetSplitter.getPacketLen() - lenNoHdr; unsentOffset = packetSplitter.getOffset() + lenNoHdr; resetPacket(); } else if (unsentPayloadLen > 0) { // no more packets, send remaining unsent data addPayload(packet, unsentOffset, unsentPayloadLen); completeCompression(); if (this.compressedPayloadLen >= unsentPayloadLen) { writeCompressedHeader(unsentPayloadLen, this.compressedSequenceId, 0); this.outputStream.write(packet, unsentOffset, unsentPayloadLen); } else { sendCompressedPacket(unsentPayloadLen); } resetPacket(); break; } else { // nothing left to send (only happens on boundaries) break; } } this.outputStream.flush(); // release reference to (possibly large) compressed packet buffer this.compressedPacket = null; } 整体思路高效地发送数据包,无论是小包还是大包,同时通过压缩减少传输数据的大小。它通过拆分、压缩和适当的序列管理确保数据完整和顺序发送. 改了以后程序跑起来,超出预期,流量消耗少了近50%,代价是cpu提了10%左右,划算。 先跑着一个月后再看。","link":"/2024/09/02/traffic_optimization_idea/"},{"title":"Hummingbot Strategies v1","text":"Hummingbot 内置很多策略模板分v1/v2,目前社区表示全力发展v2版的策略,v1虽然官方不维护了,但是不影响我们学习; v1的策略在/hummingbot/strategy目录里 策略 描述 pure_market_making Hummingbot 的原始单对市场做市策略 cross_exchange_market_making 一种通过在另一个交易所对冲来减轻库存风险的做市策略 amm_arb 一种利用 AMM 去中心化交易所与其他交易所之间价格差异的套利策略 avellaneda_market_making 基于经典的 Avellaneda-Stoikov 论文的单对市场做市策略 cross_exchange_mining 社区维护的交叉交易所做市策略的修改版 hedge 使用永续合约对冲现货交易所的库存风险 liquidity_mining 使用单一的基础币或报价币在多个交易对上提供流动性 perpetual_market_making 社区维护的永续市场做市策略 spot_perpetual_arbitrage 利用现货市场与永续合约交易所之间的价格差异进行套利 twap 在一定时间段内批量下限价单 amm-v3-lp 动态维护 AMM 去中心化交易所中的区间流动性头寸 参考资料 Hummingbot stategies_v1","link":"/2024/11/14/hummingbot_strategy_v1/"}],"tags":[{"name":"Java","slug":"Java","link":"/tags/Java/"},{"name":"Python","slug":"Python","link":"/tags/Python/"},{"name":"Architecture","slug":"Architecture","link":"/tags/Architecture/"},{"name":"jasypt","slug":"jasypt","link":"/tags/jasypt/"},{"name":"SpringBoot","slug":"SpringBoot","link":"/tags/SpringBoot/"},{"name":"python","slug":"python","link":"/tags/python/"},{"name":"日志清理","slug":"日志清理","link":"/tags/%E6%97%A5%E5%BF%97%E6%B8%85%E7%90%86/"},{"name":"HummingBot","slug":"HummingBot","link":"/tags/HummingBot/"},{"name":"bsc","slug":"bsc","link":"/tags/bsc/"},{"name":"optimization","slug":"optimization","link":"/tags/optimization/"}],"categories":[{"name":"CarryCoin","slug":"CarryCoin","link":"/categories/CarryCoin/"},{"name":"HummingBot","slug":"HummingBot","link":"/categories/HummingBot/"}],"pages":[{"title":"about me","text":"姓名 Gong Wei性别 男工作经验 10years+学历 本科学校 北京航空航天大学 - 计算机科学与技术邮箱 blackjackhoho@gmail.comTelegram @blackjackhohoGithub https://github.com/konbluesky 14年的老开发,在寻找一个激情、开放、和谐的团队,与志同道合的伙伴共同创造卓越的项目。 基础技能 精通Java,超过10年的企业级Java程序开发经验,对项目中用到的框架包括但不限于(Spring Full Stack,Mybatis,Netty等)有Cover和Hack能力; 熟悉Nodejs、Python、Shell等动态语言,能够灵活运用这些脚本语言解决项目中的碎片化问题,如原型验证、数据处理、自动化脚本等; 擅长OOP&OOD,有复杂系统0-1设计和1-N的重构经验; 擅长Linux下工作和DevOps实践;","link":"/about/index.html"}]} \ No newline at end of file +{"posts":[{"title":"Carry-Coin 一个自动化搬砖套利平台","text":"Carry-Coin 是一个套利程序,程序从23年初开始开发至今,目前已经基本稳定,现在将程序的设计整理出来;套利思路很简单:程序监控Cex和Dex平台,针对同一币种发现差价后自动化搬运; carry-config-generator(python) 框架:web3,pandas ; 工程主要负责根据dex,cex,第三方:1inch,odos,dexscreener 数据,进行数据分析最终生成套利配置; carry-core (Java) 一个基于Java的套利核心程序,dex<->cex套利逻辑的顶层抽象,SwapEngine,SwapStategy, ArbitrageProcessor,CenterExchange,DecenterExchanage,SwapProtocol 等; carry-worker (Java) 框架:Springboot3.2.5,Xchange,Web3j,RxJava3,Guava等;工程基于core实现的不同dex,cex的监控、告警、通知、搬运、买卖逻辑; carry-protocol-adapter(Nodejs) 基于Uniswap-sdk ,jupiter-swap-api(solana)开发的套利协议适配器,适配v2/v3询价; carry-web-front (Nodejs) Vue3.0+TypeScript+Vite5+Ant-Design-Vue等,工程主要管理平台的前端页面,包括套利开关、线上配置、交易数据监控,链上数据监控报表等; carry-web-server (Java) 框架: Spring Cloud Alibaba, Mysql, 管理平台的后端服务; 部署架构 程序截图 技术栈语言 Java 11 Python Nodejs Bash Shell 框架Java Spring Boot JPA Xchange RxJava Guava transmittable-thread-local fastjson web3j lombok assertj jasypt Hutool Slf4j、Logback Python Flask pickledb web3 pandas Nodejs pm2 uniswap-core,V2/v3-sdk solana/web3、spl-token nestjs ethers.js 平台 jeecg-boot","link":"/2024/10/13/carry_coin_architecture_1/"},{"title":"Carry-Coin 架构设计 Core模块(1)","text":"Carry-Coin Core的Center,Decenter,protocol的设计 Core组成部分Swap基本概念 SwapEngine 套利引擎,内部独立线程,Handler执行 ,Center构建,Job注册 SwapEngine最初设计可以支持dex(n)<->cex(n),目前阶段仅SingleSwapEngine 实现了dex(1,n)<->cex(1)的套利动作,即一个Engine绑定1个cex和N个dex; SwapContext(标记接口) SwapContext是SwapEngine的上下文,是个巨型类,为了跨交易所共用上下文预留的,主要是保存SwapEngine运行时的数据,包含了所有重要对象的引用:交易所信息,交易对信息,交易所账户信息; SwapConfig SwapConfig是SwapEngine的配置类,主要是保存SwapEngine运行时所需的配置信息; SwapHandler 用来初始化Context,同步job的阻塞加载,异步job的注册,BizDataLoaderContext首次初始化和校验 SwapInitializerConfig 根据yml初始化SwapLauncher来触发Engine工作; SwapLauncher 交易引擎启动器 Center Cex中心交易所相关的抽象和套利动作 CenterExchangeHolder 中心交易所的顶级抽象,主要是一些技术动作: 注册同步和异步的取数器(Job),获取Context数据等动作 交易所支持同步和异步取数两种模式,同步在SwapEngine初始化时执行一次,异步周期性运行; 周期性执行使用ScheduledExecutorService,实现使用了alibaba的transmittable-thread-local库,主要目的解决异步执行时上下文传递的问题; Job方式取来的数据都会放在BizDataLoaderContext容器中; AbstractCenterExchangeHolder 对CenterExchangeHolder的基本实现 CenterExchangeHolderBehavior 业务动作抽象接口:Limit/Marker下单,撤单,获取订单状态,提现,转账等业务动作,这个接口也是pipeline和job中操作Cex的核心,非必要业务流中不操作CenterExchangeHolder,AbstractCenterExchangeHolder这种顶级接口; Cex交易所操作使用Xchange完成; FacilitySupport 提供一套基本的Cex实现模板 CenterSymbolInfo Cex侧套利相关配置信息 Decenter Dex去中心交易平台相关的抽象, Decenter.protocol Decenter中SwapProtocol协议抽象,目前已实现包含solana,bsc链,odos,zerox平台的询价、交易动作;设计过程中难点在于对不同链的交易动作进行同一视角进行抽象和设计; EVM部分 GenericWeb3jBehavior 链上动作的通用抽象,默认只实现getNetwork,getWeb3jManager,考虑到多链支持Client客户端不定,所以作为泛型传入; 必要动作queryTxConfirmed,getRawAmountOut,getBalanceOfNode做抽象方法,考虑到这几个方法不挑网络,入参回参明确,所以放在此处; 不同链差异化动作放在下层实现. EthGenericWeb3jBehavior 具象evm链的实现,主要是实现网络层面、Token层面的一些方法了allowance, getTransactionGasLimit, signTransaction, waitTxConfirmed, queryTxConfirmed, getNonce, getBalanceOfNode, tokenTransfer, getGasProvider, gasProvider GenericSwapV2Impl,GenericSwapV3Impl 基于EthGenericWeb3jBehavior完成Dex swap全过程的方法,包含:getAmountsOut,swapExactTokensForTokens,swapExactTokensForTokensSupportingFeeOnTransferTokens getRawAmountOut区别于getAmountsOut,是用来通过evm中log数据获取最终交换到的token数量; SwapExactTForTParam 上层通过该类传入swap所需参数; Solana部分ODOS部分Strategy 交易策略 swap 套利动作","link":"/2024/10/14/carry_coin_architecture_2/"},{"title":"Carry-Coin 架构设计 Core模块(2)","text":"Carry-Coin Core的PipelineAction,Job的设计 Action 组成部分 AbstractForwardPipeline 正向套利(cex->dex)流水线抽象类 AbstractReversePipeline 逆向套利(dex->cex)流水线抽象类 CommonCutOffForwardPipeline CommonLimitWaitForwardPipeline CommonReversePipeline 逆向套利(dex->cex)流水线抽象类默认实现 DebugReversePipeline 调试用 WithdrawAndSellPipeline Cex账户留币策略时触发:当账户留币且链上价格比交易所高时,提现->dex->卖出 DepositAndSellPipeline Dex留币策略触发:钱包中留币且交易所价格大于链上:充币->cex->卖出 OnlySwapInChainPipeline Dex留币策略触发:钱包中留币且链上价格高于Cex:卖出 SolanaReversePipeline Solana逆向套利流水线 TransactionRecordFactory Job 组成部分 异步任务,优先使用websocket取数,如cex不支持则使用rest方式,rest调用一定要实现限频策略,具体见Xchange BalanceRest200msJob 每200ms 调rest接口取余额; ChainAccountMonitor10mJob 每10分钟 监控链上账户余额; CleanBalance1mJob 每1分钟 扫描账户符合条件触发留币策略 ConsumerMiddleCoinBuyAndSellJob 三角套利时,中间币消耗情况入库,订阅的交易所购买中间币(目前BNB,SOL) FundingRecordRest200msJob 每200ms 调rest接口取充提记录监控充提状态; LoaderDataContextMonitor1sJob 最初构想用来存储下单时,当前depth,ticker,orderbook等数据,数据量太大,有条件的情况下上nosql, 就不入db了; PendingPlaceOrderJob 定时扫描未完成订单,并尝试重试 ResetSymbolConfigJob 重置套利币本配置 RiseMonitorRest10sJob SelfOrderRest100msJob","link":"/2024/10/15/carry_coin_architecture_3/"},{"title":"Carry-Coin 架构设计 SymbolLedger (4)","text":"Carry-Coin 套利币本 SymbolLedger 设计,SymbolLedger负责存放套利过程中交易对信息,其中包括symbol在Cex中的各项配置、套利阈值等,Dex中的各种合约信息、阈值、交易参数等 SymbolLedger一个交易所对应一个SymbolLedger实例,程序启动后通过json配置文件进行加载; 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576/** * <pre> * 此类用来存储市面上所有的交易对信息 * 维护方式: 基础信息手工,其他信息通过程序自动获取; * 更新周期: 定期更新; * 作用: 以此账簿上的币作为循化基础,再从各交易所拉取对应信息; * </pre> * <p> @Date : 2023/3/21 </p> * <p> @Project : block-farming</p> * * <p> @author konbluesky </p> */@Slf4j@Datapublic class SymbolLedger { /** * 如是内存模式则不进行数据库持久化 */ private boolean memoryMode = false; /** * 更新symbolPairConfigs时,一定要重新对symbolPairConfigMap和symbolPairConfigMap_symbolKey进行更新 * 否则getSymbolPairConfig会失效 * @param symbolPairConfigs */ public void setSymbolPairConfigs(List<SymbolPairConfig> symbolPairConfigs) { this.symbolPairConfigs = symbolPairConfigs; for(SymbolPairConfig symbolPairConfig : symbolPairConfigs){ updateSymbolPairConfig(symbolPairConfig); } log.info("SymbolLedger更新symbolPairConfigs"); } private List<SymbolPairConfig> symbolPairConfigs = Lists.newCopyOnWriteArrayList(); private Map<String, SymbolPairConfig> symbolPairConfigMap = Maps.newConcurrentMap(); private Map<String, SymbolPairConfig> symbolPairConfigMap_symbolKey = Maps.newConcurrentMap(); // private Table<String, String, SymbolPairConfig> symbolPairConfigMap = Tables.synchronizedTable(HashBasedTable.create()); public void put(SymbolPairConfig symbolPairConfig) { symbolPairConfigs.add(symbolPairConfig); updateSymbolPairConfig(symbolPairConfig); } private void updateSymbolPairConfig(SymbolPairConfig symbolPairConfig){ CenterSymbolInfo centerSymbolInfo = symbolPairConfig.getCenterSymbolInfo(); symbolPairConfigMap.put(centerSymbolInfo.getBaseCurrency() .toLowerCase(), symbolPairConfig); symbolPairConfigMap_symbolKey.put(centerSymbolInfo.getBaseCurrency() .toLowerCase() + "/" + centerSymbolInfo.getQuoteCurrency() .toLowerCase(), symbolPairConfig); } @Deprecated public SymbolPairConfig getSymbolPairConfig(String baseCurrency) { return symbolPairConfigMap.get(baseCurrency.toLowerCase()); } public List<SymbolPairConfig> getSymbolPairConfigsBy(String baseCurrency) { List<SymbolPairConfig> result = Lists.newArrayList(); symbolPairConfigMap_symbolKey.forEach((k, v) -> { if(k.toLowerCase().startsWith(baseCurrency.toLowerCase())) { result.add(v); } }); return result; } public SymbolPairConfig getSymbolPairConfig(String baseCurrency, String quoteCurrency) { return symbolPairConfigMap_symbolKey.get(baseCurrency.toLowerCase() + "/" + quoteCurrency.toLowerCase()); }} SymbolLedger构建的静态工厂,json配置通过carry-config-generator(python)生成 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249/** * <pre> * 1. 交易对配置文件工厂,目前实现从HUOBI.txt文件中读取; * 2. 可以在jar包所在目录下创建config目录,将配置文件放入config目录下,程序会自动读取; * </pre> */@Slf4jpublic class SymbolLedgerFactory { public static final String SYMBOL_HUOBI_BOOKS_FILE = "HUOBI.txt"; public static final String SYMBOL_XT_BOOKS_FILE = "XT.txt"; public static final String SYMBOL_BINANCE_BOOKS_FILE = "BIAN.txt"; public static final String SYMBOL_MEXC_BOOKS_FILE = "MXCAll.txt"; public static final String SYMBOL_GATEIO_BOOKS_FILE = "GATEIO.txt"; public static final String SYMBOL_KUCOIN_BOOKS_FILE = "KUCOIN.txt"; /** * TODO 从文件中读取交易对配置,目前只解析部分字段;有需要实时更新的字段到时候从服务器上实时拉取; * swapStableCoinContractAdd 字段配置决定了与交易所的对的匹配; * * @param symbolBooksFile * @param swapConfig * @return */ private static SymbolLedger create(String symbolBooksFile, SwapConfig swapConfig) { Preconditions.checkArgument(swapConfig != null); JSONArray jsonArray = getJsonArrayByFile(symbolBooksFile); SymbolLedger symbolLedger = new SymbolLedger(); for (int i = 0; i < jsonArray.size(); i++) { JSONObject item = jsonArray.getJSONObject(i); // continue to parse the json object if (Strings.isNullOrEmpty(item.getString("swapContract")) || item.getString("swapStableCoinContract") .equalsIgnoreCase("cake")) { continue; } SymbolPairConfig symbolPairConfig = new SymbolPairConfig(); symbolPairConfig.setBaseCurrency(item.getString("swapCoinContract")); int chainId = item.getInteger("chainId") != null ? item.getInteger("chainId") : NetworkEnum.BSC.getChainId(); String stableName = SymbolTokenHelper.getTokenInfoByAddressReturnSymbol(chainId, item.getString("swapStableCoinContractAdd")); symbolPairConfig.setQuoteCurrency(Currency.USDT.getCurrencyCode()); // symbolPairConfig.setSymbol(symbolPairConfig.getBaseCurrency() + "/" + symbolPairConfig.getQuoteCurrency()); // 链上交易对和cex交易对 分别存储,交易所对,目前统一使用usdt CenterSymbolInfo centerSymbolInfo = new CenterSymbolInfo(); centerSymbolInfo.setBaseCurrency(symbolPairConfig.getBaseCurrency()); centerSymbolInfo.setQuoteCurrency(Currency.USDT.getCurrencyCode()); centerSymbolInfo.setCexMaxOrderForUSDT(item.getBigDecimal("maxOrderForUsdt") == null ? swapConfig.getGlobalMaxOrderForUsdt() : item.getBigDecimal("maxOrderForUsdt")); centerSymbolInfo.setCexMinOrderForUSDT(item.getBigDecimal("minOrderForUsdt") == null ? swapConfig.getGlobalMinOrderForUsdt() : item.getBigDecimal("minOrderForUsdt")); centerSymbolInfo.getAskPosition() .set(item.getInteger("askPosition") == null || item.getInteger("askPosition") == 0 ? 3 : item.getInteger("askPosition")); symbolPairConfig.setCenterSymbolInfo(centerSymbolInfo); // 设置去中心化交易所配置信息; DecenterSymbolInfo decenterSymbolInfo = new DecenterSymbolInfo(); decenterSymbolInfo.setBaseCurrency(symbolPairConfig.getBaseCurrency()) .setQuoteCurrency(stableName == null ? CoinEnum.getCoin(item.getString("swapStableCoinContractAdd")) .name() : stableName) .setMiddleCurrency(item.getString("swapMiddleCoinContract")) .setDexName(item.getString("dex_name")) .setDexProtocolVersion(item.getString("liquidity_type")) // 链id .setChainIds(Set.of(chainId)) .setStableCoinContractAddress(item.getString("swapStableCoinContractAdd")) .setMiddleCoinContractAddress(item.getString("swapMiddleCoinContractAdd")) .setTradeCoinContractAddress(item.getString("swapCoinContractAdd")) .setSwapContractAddress(item.getString("swapContract")) .setBurnFee(item.getBigDecimal("burn")) .setBuyTax(item.getBigDecimal("buyTax"))// 买入税率 .setSellTax(item.getBigDecimal("sellTax"))//卖出税率 // .setSellTax(item.getBigDecimal("burn"))// 卖出税率, 老的配置文件中就是sellTax .setDexBuySlipPoint(item.getBigDecimal("dexSlipPoint") .add(swapConfig.getGlobalDexSlipPoint())) .setStableCoinDecimals(item.getIntValue("stableCoinDecimals")) .setTradeCoinDecimals(item.getIntValue("coinDecimals")) .setMiddleCoinDecimals(item.getIntValue("middleCoinDecimals")) .setMethod(item.getString("method")) .setBuyPosition(item.getIntValue("currentDepthPosition")) .setReplyPro(item.getBigDecimal("replyPro")) // 反向利润放大的比例 .setDepthPro(item.getBigDecimal("depthPro")) // 反向深度缩小的比例 .setV3LoopContractAddress(item.getString("lpAdd")) // v3 的loopAddress .setV3Fee(item.getString("feev3")) // v3 的fee .setDexMaxOrderForUSDT(item.getBigDecimal("dexMax") == null ? swapConfig.getGlobalMaxOrderForUsdt() : item.getBigDecimal("dexMax")) // 反向最大下单量 .setDexMinOrderForUSDT(item.getBigDecimal("dexMin") == null ? swapConfig.getGlobalMinOrderForUsdt() : item.getBigDecimal("dexMin"));// 反向最小下单量 decenterSymbolInfo.setExtensionDexHandlerConfig(new ExtensionDexHandlerConfig(item)); decenterSymbolInfo.init(); symbolPairConfig.setDecenterSymbolInfo(decenterSymbolInfo); symbolPairConfig.set_rawJson(item); symbolLedger.put(symbolPairConfig); } return symbolLedger; } private static JSONArray getJsonArrayByFile(String symbolHuobiBooksFile) { File configFile = PathUtil.stairsLoad(symbolHuobiBooksFile, "config"); try { if (configFile == null) { log.warn("Not fount SymbolBook config file.{}", symbolHuobiBooksFile); String tempPath = System.getProperty("java.io.tmpdir") + System.currentTimeMillis(); String tempFile = Paths.get(tempPath, File.separator, symbolHuobiBooksFile) .toString(); Resource resource = new ClassPathResource(symbolHuobiBooksFile); InputStream initialStream = resource.getInputStream(); byte[] buffer = new byte[initialStream.available()]; initialStream.read(buffer); configFile = new File(tempFile); configFile.getParentFile() .mkdirs(); Files.write(buffer, configFile); log.info("Loading default SymbolBook config file from classpath: {} ", resource.getURL()); } String jsonContext = Joiner.on("") .join(Files.readLines(configFile, Charsets.UTF_8)); if (JSON.isValidArray(jsonContext)) { return JSON.parseObject(jsonContext, JSONArray.class); } } catch (Exception e) { log.error(e.getMessage(), e); throw new SwapException("SymbolBook config loading failed."); } return new JSONArray(); } public static SymbolLedger createXT(SwapConfig swapConfig) { return create(SYMBOL_XT_BOOKS_FILE, swapConfig); } public static SymbolLedger createHuoBi() { return create(SYMBOL_HUOBI_BOOKS_FILE, new SwapConfig()); } public static SymbolLedger createGateio() { return create(SYMBOL_GATEIO_BOOKS_FILE, new SwapConfig()); } public static SymbolLedger createGateio(SwapConfig swapConfig) { return create(SYMBOL_GATEIO_BOOKS_FILE, swapConfig); } public static SymbolLedger createKucoin(SwapConfig swapConfig) { return create(SYMBOL_KUCOIN_BOOKS_FILE, swapConfig); } public static SymbolLedger createOKex() { return create(SYMBOL_HUOBI_BOOKS_FILE, new SwapConfig()); } public static SymbolLedger createOKex(SwapConfig swapConfig) { return create(SYMBOL_HUOBI_BOOKS_FILE, swapConfig); } public static SymbolLedger createHuoBi(SwapConfig swapConfig) { return create(SYMBOL_HUOBI_BOOKS_FILE, swapConfig); } public static SymbolLedger createBinance(SwapConfig swapConfig) { return create(SYMBOL_BINANCE_BOOKS_FILE, swapConfig); } public static SymbolLedger createMEXC(SwapConfig swapConfig) { return create(SYMBOL_MEXC_BOOKS_FILE, swapConfig); } /** * 将配置文件内容刷入db * 外部通过SwapControlHolder 来对状态进行控制和判断 * * @param symbolLedger * @param exchangeType * @param isMonitor 开关用来控制swapSymbolPairConfigRecord 记录默认是监听状态还是, 非监听状态, * 首次程序启动的时候是非监听的,需要web端显式的开启,后续job中动态调整默认都是监听状态的 */ public static void flushToDb(SymbolLedger symbolLedger, ExchangeType exchangeType, boolean isMonitor) { if (symbolLedger.isMemoryMode()) { return; } SwapSymbolPairConfigRecordRepository res = SpringUtil.getBean(SwapSymbolPairConfigRecordRepository.class); res.deleteAllByExchangeType(exchangeType); res.flush(); List<SwapSymbolPairConfigRecord> all = Lists.newArrayList(); symbolLedger.getSymbolPairConfigs().forEach(symbolPairConfig -> { SwapSymbolPairConfigRecord swapSymbolPairConfigRecord = null; if (isMonitor) { swapSymbolPairConfigRecord = SwapSymbolPairConfigRecord.createLoadMonitor(symbolPairConfig, exchangeType); SwapControlManager.putItem(exchangeType, symbolPairConfig.getUniDexIdentify(), swapSymbolPairConfigRecord); } else { swapSymbolPairConfigRecord = SwapSymbolPairConfigRecord.createUnMonitor(symbolPairConfig, exchangeType); SwapControlManager.putItem(exchangeType, symbolPairConfig.getUniDexIdentify(), swapSymbolPairConfigRecord); } all.add(swapSymbolPairConfigRecord); }); res.saveAllAndFlush(all); log.info("Load config from config file, Flush to db is ok ! Record size is :{} ", all.size()); } /** * 增量更新配置文件 * 1. 数据库中载入当前交易所币配置 * 2. 从内存中获取当前交易所币配置 * 3. 比较数据库中不在内存的配置,更新状态 * 4. 将关闭的币种组装日志打印出来 */ public static void incrementalUpdate(SymbolLedger symbolLedger, ExchangeType exchangeType) { if (symbolLedger.isMemoryMode()) { return; } SwapSymbolPairConfigRecordRepository res = SpringUtil.getBean(SwapSymbolPairConfigRecordRepository.class); List<SwapSymbolPairConfigRecord> all = res.findAllByExchangeType(exchangeType); String changeCurrency = ""; int closeCount = 0; for (SwapSymbolPairConfigRecord swapSymbolPairConfigRecord : all) { String symbol = swapSymbolPairConfigRecord.getSymbol(); if (symbolLedger.getSymbolPairConfig(symbol.split("/")[0], symbol.split("/")[1]) == null) { swapSymbolPairConfigRecord.setMonitorStatus(SwapSymbolPairConfigRecord.MonitorStatus.UNMONITOR); changeCurrency += symbol + ","; closeCount++; } } res.saveAllAndFlush(all); String msg = Utils.format("{} 交易所内存币本配置已过滤,总数:{} 关闭数量:{} 关闭币种:{}", exchangeType, all.size(), closeCount, changeCurrency); log.info(msg); SwapMessageSender.sendMessage(exchangeType, msg); log.info("Incremental update config from config file, Flush to db is ok ! Record size is :{} ", all.size()); } SymbolPairConfig 类, 用来存储某个交易对(ABC/USDT)的配置信息 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100/** * <p> @Date : 2023/3/21 </p> * <p> @Project : block-farming</p> * * <p> @author konbluesky </p> */@Datapublic class SymbolPairConfig { //TODO 现阶段 symbolInfo 是1-1关系 /** * 交易对在中心化交易所的信息 */ private CenterSymbolInfo centerSymbolInfo; /** * 交易对在去中心化交易所的信息 */ private DecenterSymbolInfo decenterSymbolInfo; /** * 交易对显示名称 BTC/USDT */ public String getSymbol() { return baseCurrency+"/"+quoteCurrency; } /** * RITE/USDT-RITE/USDT-PancakeV2-UniV2 * @return */ public String getUniDexIdentify(){ return centerSymbolInfo.getSymbol() + "-" + decenterSymbolInfo.getSymbol() + "-" + decenterSymbolInfo.getDexName() + "-" + decenterSymbolInfo.getDexProtocolVersion(); } /** * 交易对基础货币 BTC */ private String baseCurrency; /** * 交易对报价货币 USDT */ private String quoteCurrency; /** * 允许开启的实例数 */ private AtomicInteger reverseAllowInstanceNum = new AtomicInteger(1); private AtomicInteger forwardAllowInstanceNum = new AtomicInteger(1); /** * 开启翻倍后,设置x分钟激情时间,进行翻倍 */ private AtomicLong reversePassionTime = new AtomicLong(0); private AtomicLong forwardPassionTime = new AtomicLong(0); /** * 交易对的利润阈值,一般在交易所holder初始化的时候从SwapConfig获取; * TODO 这里的阈值可基于全局的做覆盖; */ private BigDecimal profitThreshold; private BigDecimal profitThresholdRate; private BigDecimal _origin_profitThresholdRate; private JSONObject _rawJson; public void setProfitThresholdRate(BigDecimal profitThresholdRate) { this.profitThresholdRate = profitThresholdRate; this._origin_profitThresholdRate = profitThresholdRate; } public void setProfitThresholdRateDouble() { profitThresholdRate = profitThresholdRate.multiply(new BigDecimal("1.3")); } public void resetProfitThresholdRate() { profitThresholdRate = _origin_profitThresholdRate; } /** * 提现次数自增一次 * Increase WithdrawTime */ public void increaseWithdrawTimes() { centerSymbolInfo.getLastWithdrawTime().set(System.currentTimeMillis()); centerSymbolInfo.increaseWithdrawTimes(); } public boolean isMiddleCoin() { return !Strings.isNullOrEmpty(decenterSymbolInfo.getMiddleCoinContractAddress()); } public String getSymbolNoOblique() { return getSymbol().replace("/", ""); }} CenterSymbolInfo 类, 用来存储某个交易对(ABC/USDT)的Cex配置 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105/** * * <p> @Date : 2023/3/20 </p> * <p> @Project : block-farming</p> * * <p> @author konbluesky </p> */@Data@Slf4jpublic class CenterSymbolInfo { public String getSymbol() { return baseCurrency + "/" + quoteCurrency; } /** * 注意这里是[交易所]对应的基础货币 BTC */ private String baseCurrency; /** * 注意这里是[交易所]对应的交易对报价货币 USDT */ private String quoteCurrency; private Double price; /** * 交易所体现时一般需要指定链名或者网络名 */ private String chainName; /** * 提现次数 */ private AtomicInteger withdrawTimes = new AtomicInteger(0); /** * 最小提现手续费 */ private BigDecimal withdrawMinFee = BigDecimal.ZERO; /** * 手续费换算成美元 */ private BigDecimal withdrawMinFee$ = BigDecimal.ZERO; /** * 传入实时的单价,计算出手续费绝对刀 * @param unitPrice */ public void setWithdrawMinFee$(BigDecimal unitPrice) { this.withdrawMinFee$ = Utils.getScale(unitPrice.multiply(withdrawMinFee)); } /** * 最后一次的提现时间 */ private AtomicLong lastWithdrawTime = new AtomicLong(System.currentTimeMillis()); /** * 最后一次下卖单时间 */ private AtomicLong lastPlaceSellOrderTime = new AtomicLong(System.currentTimeMillis()); private AtomicLong lastPlaceBuyOrderTime = new AtomicLong(System.currentTimeMillis()); /** * 卖单默认位置,此位置是口语中的卖一卖二,不是数组下标 */ private AtomicInteger askPosition = new AtomicInteger(0); /** * 挂单检查时间(卖单) */ private int askOrderWaitSeconds = 5; /** * 交易所固定最大下单金额(usdt为单位) */ private BigDecimal CexMaxOrderForUSDT; /** * 交易所固定最小下单金额(usdt为单位) */ private BigDecimal CexMinOrderForUSDT; /** * 留币时保存的卖出下单ID; * remain 留存 sell 出售 orderId 订单ID */ private String remainSellOrderId; /** * 提现次数自增一次 */ public void increaseWithdrawTimes() { withdrawTimes.set(withdrawTimes.get() + 1); }} DecenterSymbolInfo 类, 用来存储某个交易对(ABC/USDT)的Dex配置 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245/** * <p> @Date : 2023/3/20 </p> * <p> @Project : block-farming</p> * * <p> @author konbluesky </p> */@Accessors(chain = true)@Data@Slf4jpublic class DecenterSymbolInfo { private String dexName; private String dexProtocolVersion; public String getSymbol() { if(Strings.isNullOrEmpty(middleCoinContractAddress)) { return baseCurrency + "/" + quoteCurrency; }else{ return baseCurrency + "/" + middleCurrency + "/" + quoteCurrency; } } /** * 注意这里是[链上]对应的基础货币 BTC */ private String baseCurrency; /** * 注意这里是[链上]对应的交易对报价货币 USDT */ private String quoteCurrency; /** * 注意这里是[链上]对应的交易对中间报价货币 */ private String middleCurrency; private Set<Integer> chainIds; /** * TODO 未来需要支持一个币种在多个链上的支持, 目前只考虑币种只在一个链上活跃 * @return */ public Integer getFirstChainId(){ return chainIds.iterator().next(); } /**************************************************************************************************/ /************************************** 链上的地址配置信息 *******************************************/ /**************************************************************************************************/ /** * 为了方便存取,围绕token,将常用的信息包装起来 */ private CurrencyBag stable; private CurrencyBag trade; private CurrencyBag middle; public void init() { if (stable == null) { stable = CurrencyBag.of(stableCoinContractAddress, quoteCurrency, getStableCoinDecimals()); } if (trade == null) { trade = CurrencyBag.of(tradeCoinContractAddress, baseCurrency, getTradeCoinDecimals()); } if (middle == null) { middle = CurrencyBag.of(middleCoinContractAddress, middleCurrency, getMiddleCoinDecimals()); } } /** * 稳定币的合约地址 * USTD,ETH,BTC 等 */ private String stableCoinContractAddress; /** * 中间币的合约地址 * BNB 有自己独立的链的等 */ private String middleCoinContractAddress; /** * 交易币的合约地址 * 即最终操作的目标币地址 * 原代码中的:swapCoinContractAdd */ private String tradeCoinContractAddress; /** * v3流通性的合约地址 */ private String v3LoopContractAddress; /** * v3手续费的 */ private String v3Fee; /** * 去中心化交易所的合约地址 */ private String swapContractAddress; /** * 反向套利,从蛋糕到交易所,稳定币额度 * bnb对就进行交易所价格查询,转换成对应的bnb * <pre> * 声明 BidDepths,bidOnePrice,BidDepthsAllAmount; * swapStableCoinNumOut = bidOnePrice * BidDepthsAllAmount; * </pre> */ private AtomicDouble swapStableCoinNumOut; /** * 非稳定币的兑换数量,个数 */ private AtomicDouble swapFStableCoinNum; /** * 稳定币的兑换数量,个数 */ private AtomicDouble swapStableCoinNum; /** * 价格精度 最小就是0.0001 */ private String priceDecimals; /** * 数量精度 最小就是0.01 */ private String numDecimals; /** * 卖时燃烧的手续费 */ private BigDecimal burnFee; /** * 其他费用,初步用来保存转移费用,有些币种的转移费用 */ private BigDecimal otherFee = BigDecimal.ZERO; /** * 链上买币滑点 */ private BigDecimal dexBuySlipPoint; private BigDecimal _origin_dexBuySlipPoint; /** * 买时 税率 */ private BigDecimal buyTax = BigDecimal.ZERO; /** * 卖时 税率 */ private BigDecimal sellTax = BigDecimal.ZERO; /** * 交易币的精度,在程序启动时通过TokenClient从链上合约中查询 */ private int tradeCoinDecimals = 0; /** * 稳定币的精度,在程序启动时通过TokenClient从链上合约中查询 */ private int stableCoinDecimals = 0; /** * 中间币的精度,在程序启动时通过TokenClient从链上合约中查询 */ private int middleCoinDecimals = 0; /** * 链上固定最大下单金额(usdt为单位) */ private BigDecimal dexMaxOrderForUSDT; /** * 链上固定最小下单金额(usdt为单位) */ private BigDecimal dexMinOrderForUSDT; /** * dex购买状态,用于判断是否蛋糕买了,然后回交易所 * 对于这种情况,要进行停止蛋糕购买及交易所到蛋糕的业务 * 根据交易所充值到账时间 一般需要2分钟,即120秒,那130秒内都必须把价格降 */ private AtomicLong dexBuyTimestamp = new AtomicLong(0); private String method; /** * 反向套利时,需要监控买盘的数据, 这是买盘的位置 * 对应的是centerSymbolInfo的askPosition */ private int buyPosition; /** * 反向套利时,深度需要*depthPro ,进行打折 */ private BigDecimal depthPro; public DecenterSymbolInfo setDexBuySlipPoint(BigDecimal dexBuySlipPoint) { this.dexBuySlipPoint = dexBuySlipPoint; // 保留原始值用于恢复 this._origin_dexBuySlipPoint = dexBuySlipPoint; return this; } private ExtensionDexHandlerConfig extensionDexHandlerConfig; /** * 反向套利时的放大比例 */ private BigDecimal replyPro; public boolean isV3(){ return getDexProtocolVersion().toLowerCase().contains("v3".toLowerCase()); } public boolean isV2(){ return getDexProtocolVersion().toLowerCase().contains("v2".toLowerCase()); } /** * 对dexBuySlipPoint滑点1.5倍; */ public void setDexBuySlipPointDouble() { dexBuySlipPoint = dexBuySlipPoint.multiply(replyPro); } public void resetDexBuySlipPoint() { dexBuySlipPoint = _origin_dexBuySlipPoint; }}","link":"/2024/11/05/carry_coin_architecture_4/"},{"title":"Carry-Coin 中 Jasypt 的应用","text":"自从最近L君小弟电脑中毒,疑似私钥泄漏,导致3W u被盗,反观我的程序一直以来裸奔的状态下,再次菊花一紧。于是,我开始了对Spring Boot的jasypt加密配置的研究。 最坏情况服务器即使被黑,也要尽可能保证程序的安全。 配置文件中关键信息: 交易所token,钱包私钥加密; jar中yml打包时排除,服务器上用密文配置执行; 解密密码动态设置; jasypt 集成程序引入pom.xml 12345 <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.5</version></dependency> application.yml 123jasypt: encryptor: password: ${JASYPT_ENCRYPTOR_PASSWORD} Application.java 12345678910111213141516171819202122/** * <p> @Date : 2023/3/19 </p> * <p> @author konbluesky </p> */@SpringBootApplication(scanBasePackages = "com.block")@EnableScheduling@EnableAsync@EnableJpaAuditing// WARN bad smell@EncryptablePropertySources({ @EncryptablePropertySource(value = "file:./config/application.yml",ignoreResourceNotFound = true), @EncryptablePropertySource(value = "classpath:application.yml",ignoreResourceNotFound = true)})@EnableEncryptablePropertiespublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }} 打包排除pom.xml,设置打包时排除yml文件,避免铭文yml误打包到jar中 123456789101112 <resources> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.*</include> </includes> <!-- 用springboot 打包时 排除yml--> <excludes> <exclude>**/*.yml</exclude> </excludes> </resource></resources> 启动脚本原启动方式需要通过-Djasypt.encryptor.password=”the password”,来设置加密密码。这种方式用jps -mlv可以看到启动命令,但是这种方式不安全,依然容易泄露密码。为了安全起见,我们需要在启动脚本中设置环境变量 JASYPT_ENCRYPTOR_PASSWORD 来加密配置文件中的敏感信息。设置后又担心环境变量被echo出来,所以需要设置环境变量时,需要隐藏输入内容同时控制变量作用域在shell内部; 最终脚本如下,关键语句是export和unset; 12345678910111213141516171819202122232425262728293031323334353637383940#!/bin/bash# 获取当前脚本所在目录的绝对路径SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"# 进入脚本所在目录cd "$SCRIPT_DIR"# 提示用户输入 JASYPT_ENCRYPTOR_PASSWORD,并隐藏输入内容read -sp "Enter App password: " JASYPT_PASSWORDechoexport JASYPT_ENCRYPTOR_PASSWORD=$JASYPT_PASSWORD# 检查是否输入了 JASYPT_ENCRYPTOR_PASSWORDif [ -z "$JASYPT_PASSWORD" ]; then echo "jasypt.encryptor.password is required." exit 1fi# 启动Java程序并使用nohup确保它在后台运行nohup java -jar $(ls -t trade-monitor-*.jar | head -1) \\ --spring.config.location=file:./config/ \\ --spring.application.name=gateio\\ -XX:+PrintGCDetails \\ -XX:+PrintGCDateStamps \\ -Xloggc:$SCRIPT_DIR/gc.log \\ -Xmx16G \\ -Xms8G \\ -XX:+UseG1GC \\ -XX:NewRatio=3 \\ -XX:SurvivorRatio=8 \\ -XX:MaxMetaspaceSize=1024M \\ -XX:MaxGCPauseMillis=200 \\ -Xss1M >/dev/null 2>&1 &# 清除环境变量unset JASYPT_ENCRYPTOR_PASSWORD","link":"/2024/07/13/carry_coin_jasypt/"},{"title":"使用 Python 脚本清理 Carry-Coin 程序中的 Logback 日志","text":"随着监控的币种越来越多,我的 Carry-Coin 程序中日志数据的日质量也不断增加,每天的日志文件大小达到约 40G。这不仅占用了大量的存储空间,还给日志分析带来了挑战。为了更有效地管理这些日志,我已经编写了一个程序来提取日志中与交易相关的信息,供后续分析使用。 然而,在配置 Logback 的 logback-spring.xml 文件时,我设置了 appender.rollingPolicy.maxHistory 字段,旨在只保留最近 2 天的日志。然而,这一配置并未如预期生效。因此,我决定暂时使用 Python 脚本来手动清理旧的日志文件。 问题背景在 Carry-Coin 程序中,随着对越来越多币种的监控,日志文件的大小急剧增加,导致每天产生的日志文件约为 40G。虽然我已编写程序提取交易信息,但大量的日志数据依然占用了过多的磁盘空间。为此,我尝试通过调整 Logback 的配置来限制保留的日志数量。 Logback 日志管理为了管理日志,我在 logback-spring.xml 文件中配置了以下内容: 12345678910<appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/carry-coin.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/carry-coin.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>2</maxHistory> <!-- 只保留最近2天的日志 --> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern> </encoder></appender> 尽管如此,maxHistory 的配置并未生效,因此需要寻找替代解决方案。 Python 脚本实现我编写了一个 Python 脚本来自动清理日志文件,保留最近 2 天的日志。以下是脚本的实现: 12345678910111213141516171819202122import osimport timefrom datetime import datetime, timedelta# 获取两天前的时间days_to_keep = 2cutoff_time = datetime.now() - timedelta(days=days_to_keep)# 定义日志目录log_directory = "/path/to/logs"# 遍历日志目录,删除修改时间在 cutoff_time 之前的 .log 文件for filename in os.listdir(log_directory): file_path = os.path.join(log_directory, filename) if os.path.isfile(file_path) and filename.endswith(".log"): file_mtime = os.path.getmtime(file_path) file_mtime_dt = datetime.fromtimestamp(file_mtime) if file_mtime_dt < cutoff_time: os.remove(file_path) print(f"Deleted: {file_path}") 说明 脚本获取当前时间的两天前,遍历指定日志目录,删除修改时间在该时间之前的 .log 文件。 仅处理以 .log 结尾的文件,确保只删除日志文件。 设置 Cron 定时任务为了定期执行这个清理脚本,我使用 cron 设置了一个定时任务,以下是设置步骤: 打开 crontab 编辑器: 1crontab -e 添加如下定时任务,每天凌晨 1 点执行: 10 1 * * * /usr/bin/python3 /path/to/your_script.py >> /path/to/cron_log.log 2>&1 保存并退出 crontab。 总结通过使用 Python 脚本,我能够有效地管理 Carry-Coin 程序中生成的日志文件,避免了因日志文件过大导致的磁盘空间不足的问题。虽然 Logback 的配置未能如预期生效,但临时解决方案为我带来了便利。在未来的项目中,我将继续探索更好的日志管理策略,以确保程序高效运行。 参考文档 Logback 文档","link":"/2024/02/13/carry_coin_log_cleanup/"},{"title":"Google 2FA 脚本","text":"批量显示Google 2FA 工具,5秒刷新一次 secrets.csv 123456789101112131415username,secretiAvloyola,ENOG7VLRJJ7GDNZJLilBlue561,IWPTHMCHSR74EOSHKwaciWorsnop,TUXN5TNLTKOPTJEYvikeshchotai,L333DPD5JHXF3O2Kperaltasocimo,UH4EDK7425BFCXSHmakzuzu,OUIG375O7OARDXLJJsaFikitha,AIWBIBQGXZK3ZDE3limliangtung,HHD2VM5LHCV6TDOVsinhde18,L6LYS4ECQL5KKPFFActionDT,O64IEGT5NPCOHCQtimansur,3ASN5ZIGJH4ICYUXDarmyrez,BTKNH5OMOMPZ7CFYaninditamario,G2VHH4IHFE367PNWizzanfurkan,EYP7VV6AK6EIDIJB 批量显示验证码脚本 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556import csvimport datetimeimport pyotpimport timeimport osimport base64# 定义读取CSV文件的函数def read_secrets_from_csv(csv_file_path): secrets = [] with open(csv_file_path, mode='r') as file: reader = csv.DictReader(file) for row in reader: secrets.append(row) return secrets# 定义验证Base32密钥的函数def is_valid_base32(secret): try: # 尝试解码,如果失败则说明不是有效的Base32 base64.b32decode(secret, casefold=True) return True except (base64.binascii.Error, ValueError): return False# 定义生成2FA验证码的函数def generate_2fa_codes(secrets): codes = [] for secret in secrets: if is_valid_base32(secret['secret']): totp = pyotp.TOTP(secret['secret']) code = totp.now() codes.append({'username': secret['username'], 'code': code}) else: codes.append({'username': secret['username'], 'code': '无效的Base32密钥'}) return codes# 主函数def main(): csv_file_path = '2fa/secrets.csv' # 请根据实际情况修改CSV文件路径 secrets = read_secrets_from_csv(csv_file_path) while True: os.system('cls' if os.name == 'nt' else 'clear') # 清屏 codes = generate_2fa_codes(secrets) print(f"时间:{datetime.datetime.now()}") for entry in codes: print(f"用户 {entry['username']} 的当前2FA验证码是: {entry['code']}") time.sleep(5) # 每30秒刷新一次if __name__ == "__main__": main()","link":"/2024/11/03/google-2fa/"},{"title":"Hummingbot Create First Strategy Bot","text":"使用simple_amm策略创建第一个机器人 使用命令: 123start --script [SCRIPT NAME]create --script-config [SCRIPT_FILE]start --script [SCRIPT_FILE] --conf [SCRIPT_CONFIG_FILE] 初始的文件目录 12345678910111213141516(hummingbot) root@vmi2090919:~/hummingbot/conf# tree.├── __init__.py├── conf_client.yml├── conf_fee_overrides.yml├── connectors│ ├── __init__.py│ ├── mexc.yml│ └── okx.yml├── controllers│ └── __init__.py├── hummingbot_logs.yml├── scripts│ └── __init__.py└── strategies └── __init__.py hummingbot cli中 通过下列指令创建基本策略 123456789101112131415161718>>> create --script-config simple_pmm For more information, please visit https://pyperclip.readthedocs.io/en/latest/introduction.html#not-implemented-erro r (See log file for stack trace dump)Exchange where the bot will trade >>> mexcTrading pair in which the bot will place orders >>> MX-USDTOrder amount (denominated in base asset) >>> 0.01Bid order spread (in percent) >>> 0.001Ask order spread (in percent) >>> 0.001Order refresh time (in seconds) >>> 15Price type to use (mid or last) >>> midEnter a new file name for your configuration >>> conf_simple_pmm_1.ymlA new config file has been created: conf_simple_pmm_1.yml ps: 配置过程中要终止配置的话,使用快捷键ctrl+x 查看文件目录,新配置已生成 1234567891011121314151617(hummingbot) root@vmi2090919:~/hummingbot/conf# tree.├── __init__.py├── conf_client.yml├── conf_fee_overrides.yml├── connectors│ ├── __init__.py│ ├── mexc.yml│ └── okx.yml├── controllers│ └── __init__.py├── hummingbot_logs.yml├── scripts│ ├── __init__.py│ └── conf_simple_pmm_1.yml└── strategies └── __init__.py conf_simple_pmm_1.yml 内容 12345678script_file_name: simple_pmm.pyexchange: mexctrading_pair: MX-USDTorder_amount: 0.01bid_spread: 0.001ask_spread: 0.001order_refresh_time: 15price_type: mid 使用start --script simple_pmm --conf conf_simple_pmm_1.yml 启动bot 错误123456789102024-11-15 13:10:31,262 - 2684330 - hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange - INFO - Network status has changed to NetworkStatus.CONNECTED. Starting networking...2024-11-15 13:10:31,715 - 2684330 - hummingbot.connector.exchange.mexc.mexc_api_order_book_data_source.MexcAPIOrderBookDataSource - ERROR - Unexpected error occurred subscribing to order book trading and delta streams...Traceback (most recent call last): File "/root/hummingbot/hummingbot/connector/exchange/mexc/mexc_api_order_book_data_source.py", line 76, in _subscribe_channels symbol = await self._connector.exchange_symbol_associated_to_pair(trading_pair=trading_pair) File "hummingbot/connector/exchange_base.pyx", line 97, in exchange_symbol_associated_to_pair return symbol_map.inverse[trading_pair] File "/root/miniconda3/envs/hummingbot/lib/python3.10/site-packages/bidict/_base.py", line 524, in __getitem__ return self._fwdm[key]KeyError: 'MX-USDT' Mexc 中并不是所有交易对都支持api交易的,交易对要到https://api.mexc.com/api/v3/defaultSymbols接口中检查下换了个PNUT-USDT交易对,正常交易了。 关于 PMM with Price Shift and Dynamic Spreads simple_amm.py 使用的策略 参考资料关于 PMM with Price Shift and Dynamic Spreads","link":"/2024/11/15/hummingbot_create_simple_pmm_bot/"},{"title":"Hummingbot Dashboard","text":"Hummingbot Dashboard Hummingbot Dashboard 是一款开源应用,旨在帮助用户创建、回测和优化各种算法交易策略。一旦策略得到完善,它们可以作为 Hummingbot 实例部署到实盘交易模式中,从策略制定到实际交易执行实现无缝衔接。 功能 机器人编排:部署和管理多个 Hummingbot 实例 策略回测与优化:通过历史数据评估策略表现,并使用 Optuna 进行优化 一键部署:轻松将策略部署为 Hummingbot 实例,支持模拟或实盘交易 性能分析监控:监控并分析已部署策略的表现 凭证管理:创建和管理 API 密钥的独立账户 文档:https://hummingbot.org/dashboard/ 安装Dashboard 两种方式build from sourcehttps://github.com/hummingbot/dashboard#installation docker先装docker compose 12345678sudo curl -L "https://github.com/docker/compose/releases/download/v2.28.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-composesudo chmod +x /usr/local/bin/docker-composedocker-compose --versiongit clone https://github.com/hummingbot/deploycd deploybash setup.sh 参考资料 Hummingbot stategies_v1","link":"/2024/11/15/hummingbot_dashboard/"},{"title":"Hummingbot:开源的加密货币高频交易机器人","text":"Hummingbot 是一个开源的高频交易机器人框架,旨在为加密货币市场提供自动化交易工具。无论是市场做市(market making)、套利(arbitrage),还是跨交易所市场做市(cross-exchange market making),Hummingbot 都为用户提供了多种实用的策略模板,帮助用户轻松上手高频交易,参与到加密货币交易市场中。 Hummingbot 的主要特点1. 开源与社区驱动Hummingbot 是一个开源项目,其代码公开透明,用户可以根据自身需求对代码进行修改,也可以为社区贡献代码。社区驱动的开发模式使得 Hummingbot 不断进步,拥有活跃的支持和开发者社群,并定期发布更新和新功能。 2. 支持多种交易所Hummingbot 支持多个主流的中心化交易所(CEX)和去中心化交易所(DEX),如 Binance、Coinbase Pro、FTX、Uniswap、Balancer 等。它具有一个插件系统,使得开发者可以为还不支持的交易所编写接口,以满足更多的市场需求。 3. 灵活的策略配置Hummingbot 提供多种内置交易策略模板,涵盖了: 简单市场做市(Market Making): 通过挂买卖单,赚取价差。 套利(Arbitrage): 利用不同交易所之间的价格差获利。 跨交易所做市(Cross-Exchange Market Making): 在多个交易所间挂单捕捉价差。 用户可以通过配置文件轻松调整策略参数,例如控制交易频率、订单大小、价差范围等,使得策略更加灵活、适应不同市场情况。 4. 用户友好的 CLI 界面Hummingbot 提供了简洁的命令行界面(CLI),用户可以快速配置和监控机器人的运行状态,并且支持实时监控和日志记录。对于希望快速上手交易的用户来说,这个界面非常友好。 5. 强大的策略开发支持对于有编程基础的用户,Hummingbot 支持 Python 自定义交易策略。用户可以根据市场需求和个人交易风格调整策略,例如自定义套利触发条件、调整加仓和减仓逻辑等。这一特性让 Hummingbot 成为一个灵活的量化交易框架。 6. 灵活的部署方式Hummingbot 支持本地部署,也可以部署在云服务器上,适应不同用户的需求。它能够与各类外部数据源、交易所的 API 无缝集成,适合于大规模实时交易需求。 Hummingbot 的应用场景Hummingbot 支持的策略广泛适用于以下场景: 市场做市(Market Making): 提供流动性,通过在买卖之间的价差获利。 套利交易(Arbitrage): 抓住不同交易所之间的价差机会获利。 跨交易所做市(Cross-Exchange Market Making): 同时在两个或多个交易所挂买卖单,以捕捉价差。 总结Hummingbot 是一个高度灵活且功能强大的开源交易机器人,对希望参与加密货币高频交易的开发者和量化交易者来说,是一个值得尝试的选择。无论你是交易新手,还是经验丰富的量化交易员,Hummingbot 都提供了丰富的功能和高效的工具,助你在加密货币市场中捕捉更多机会。","link":"/2024/11/09/hummingbot_info/"},{"title":"Hummingbot Install(macos)","text":"Hummingbot Macos 安装步骤 安装要求(macos) Component Specification Operating System MacOS 12+ - Intel x86 or Apple Silicon (M1 / M2 / M3) Memory 4 GB RAM per instance Storage 5 GB HDD space per instance CPU At least 1 vCPU per instance / controller 1xcode-select --install 官方建议用conda做环境MacOS with Intel x86: 12curl -o Miniconda3-latest-MacOSX-x86_64.sh https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.shbash Miniconda3-latest-MacOSX-x86_64.sh MacOS with Apple Silicon (M1 / M2 / M3): 12curl -o Miniconda3-latest-MacOSX-x86_64.sh https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.shbash Miniconda3-latest-MacOSX-x86_64.sh 12345git clone https://github.com/hummingbot/hummingbot.gitcd hummingbot./installconda activate hummingbot./compile 如果安装后conda命令无法识别,可以尝试将miniconda3安装目录下的bin目录添加到环境变量中。/root/.bashrc中添加:export PATH="/Users/your_username/miniconda3/bin:$PATH"然后执行source /root/.bashrc使环境变量生效。 1./start.sh 启动界面 在命令行ui界面,设置密码后进入主界面 参考资料 Hummingbot Install(macos)","link":"/2024/11/11/hummingbot_install/"},{"title":"Hummingbot 目录结构","text":"Hummingbot 目录结构 安装后目录123456789hummingbot ┣ conf ┣ connectors ┣ strategies ┣ scripts ┣ logs ┣ data ┣ scripts ┣ hummingbot /conf:配置文件的通用文件夹 /conf/connectors:配置 Exchange API 密钥 /conf/strategies:配置策略文件,在cli-ui中通过create和import命令创建或导入策略 /conf/scripts:编写脚本配置文件,create --script-config /logs:脚本和策略生成的日志文件 /data:用于记录脚本和策略执行的交易的 SQLite 数据库和 CSV 文件 /scripts:此文件夹包含示例脚本,可以在此处添加新脚本,以使其可供start命令使用","link":"/2024/11/12/hummingbot_post_install/"},{"title":"Hummingbot Strategies v1","text":"Hummingbot 内置很多策略模板分v1/v2,目前社区表示全力发展v2版的策略,v1虽然官方不维护了,但是不影响我们学习; v1的策略在/hummingbot/strategy目录里 策略 描述 pure_market_making Hummingbot 的原始单对市场做市策略 cross_exchange_market_making 一种通过在另一个交易所对冲来减轻库存风险的做市策略 amm_arb 一种利用 AMM 去中心化交易所与其他交易所之间价格差异的套利策略 avellaneda_market_making 基于经典的 Avellaneda-Stoikov 论文的单对市场做市策略 cross_exchange_mining 社区维护的交叉交易所做市策略的修改版 hedge 使用永续合约对冲现货交易所的库存风险 liquidity_mining 使用单一的基础币或报价币在多个交易对上提供流动性 perpetual_market_making 社区维护的永续市场做市策略 spot_perpetual_arbitrage 利用现货市场与永续合约交易所之间的价格差异进行套利 twap 在一定时间段内批量下限价单 amm-v3-lp 动态维护 AMM 去中心化交易所中的区间流动性头寸 参考资料 Hummingbot stategies_v1","link":"/2024/11/14/hummingbot_strategy_v1/"},{"title":"Hummingbot Strategies v2","text":"组件v2 对比 v1 来说,架构做了调整,多了几个组件更好的工作和解耦 脚本(Script):所有策略的入口点,这个Python文件负责协调整个策略的执行。它可以是一个包含所有策略逻辑的简单文件,或者是一个加载一个或多个控制器的文件。 市场数据提供器(Market Data Provider):用于访问交易所的市场数据的单一入口,比如历史OHCLV(开盘价、高点、低点、收盘价、成交量)K线数据、订单簿数据和交易记录。 执行器(Executor):根据用户预设管理订单和仓位,确保根据策略指令下单、修改或取消订单。 控制器(Controller):基于策略控制器的基础类(如方向性策略或做市策略)定义一个交易策略。 继承关系 V1 策略 策略基类(StrategyBase):StrategyBase 是所有策略的 Cython 基类,而 StrategyPyBase 继承自它,并作为所有 Python 基础策略的根类。 V1 脚本(Scripts):ScriptStrategyBase 是在上述类的基础上构建的,创建简单策略变得更加容易。这个类目前仍然支持,但后面可能会被弃用。所以建议在新脚本实现中使用 StrategyV2Base。 控制器与 V2 脚本 V2 策略基类(StrategyV2Base):StrategyV2Base 继承自 ScriptStrategyBase,但它使用执行器(Executors)来管理订单,而不再通过 buy() / sell() 方法。控制器(Controllers)在此基础上进一步扩展,作为通过事件队列松散耦合的附加组件。 请务必牢记继承结构,这会大大帮助理解如何编写自己的自定义策略。 参考资料 Hummingbot stategies_v1","link":"/2024/11/15/hummingbot_strategy_v2/"},{"title":"BSC节点区块监控脚本","text":"脚本主要监听私有BSC节点区块状态,如发生区块漏块过多,发送告警消息到DD群中,carry-coin调整rpc访问策略; 监控脚本 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123from web3 import Web3import timeimport threadingimport sysimport loggingfrom logging.handlers import TimedRotatingFileHandlerimport osfrom stop_event_trigger import StopEventTrigger# 日志配置log_dir = "logs"if not os.path.exists(log_dir): os.makedirs(log_dir)app_name="block_monitor-7.143"can_call=Falsestop_event_trigger = StopEventTrigger()log_file = os.path.join(log_dir, "block_monitor.log")# 创建一个TimedRotatingFileHandler,用于按照日期切割日志文件file_handler = TimedRotatingFileHandler(log_file, when="midnight", interval=1, backupCount=7, encoding='utf-8')file_handler.setFormatter(logging.Formatter(app_name+' %(asctime)s - %(levelname)s - %(message)s'))# 创建一个StreamHandler,用于将日志输出到控制台console_handler = logging.StreamHandler()console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))# 配置root loggerlogging.basicConfig(level=logging.INFO, handlers=[file_handler, console_handler])from chatbot import DingtalkChatbot# 配置 DingTalk 消息通知webhook = "https://oapi.dingtalk.com/robot/send?access_token={机器人TOKEN}"secret = "SEC开头密钥"dd = DingtalkChatbot(webhook, secret=secret, fail_notice=True)private_rpc_url = "http://私有节点ip:8545/"# 公共节点public_rpc_urls = [ "https://bsc-dataseed.binance.org", "https://bsc-dataseed1.defibit.io", "https://bsc-dataseed1.binance.org", "https://bsc-dataseed2.defibit.io", "https://bsc-dataseed3.ninicoin.io"]def get_ethereum_block_height(rpc_url): start_time = time.time() # 记录开始时间 web3 = Web3(Web3.HTTPProvider(rpc_url)) block_height = web3.eth.block_number end_time = time.time() # 记录结束时间 elapsed_time = end_time - start_time logging.info(f"节点 {rpc_url} 当前区块高度: {block_height},查询耗时: {elapsed_time:.3f}秒") return block_heightdef send_dingtalk_message(message): dd.send_text(msg=message)def monitor_block_height(private_rpc_url, public_rpc_urls): inspection_timer = 0 # 初始化巡检计时器,单位为秒 while True: try: my_block_height = get_ethereum_block_height(private_rpc_url) # 获取所有节点的区块高度 public_block_heights = [get_ethereum_block_height(url) for url in public_rpc_urls] logging.info(f"私有节点高度:{my_block_height} 所有节点的区块高度: {public_block_heights}") # 找到最大的区块高度 max_block_height = max(public_block_heights) logging.info(f"最大区块高度: {max_block_height}") # 比对节点是否领先 # 如果私有节点的区块高度小于最大区块高度,并且区块差异大于3 则发送警告 diff_height = max_block_height - my_block_height if my_block_height < max_block_height and diff_height > 3: # if my_block_height < max_block_height: currentTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) message = f"{app_name}私有节点 {my_block_height} 低于于其他节点,最大区块高度为 {max_block_height},差异:{diff_height} 时间 {currentTime}" logging.warning(message) # 使用警告级别的日志 send_dingtalk_message(message) if can_call: stop_event_trigger.pin_point_and_stop_engine_event() # 每隔10分钟发送一次巡检记录 if inspection_timer >= 1800: currentTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) inspection_message = f"{app_name} 巡检记录:私有节点 {my_block_height}," inspection_message += f"公共节点最大区块高度: {max_block_height}," inspection_message += f"时间 {currentTime}" logging.info(inspection_message) send_dingtalk_message(inspection_message) inspection_timer = 0 # 重置计时器 except Exception as e: logging.error(f"发生错误:{e}") time.sleep(5) inspection_timer += 5 # 每次循环增加计时器的时间,单位为秒if __name__ == "__main__": # 读取命令行传入参数 if len(sys.argv) > 1: app_name = sys.argv[1] if app_name is None: app_name = "block_monitor" if len(sys.argv) > 2: canCall = sys.argv[2] if canCall is None: canCall = False if can_call: stop_event_trigger.pin_point_and_stop_engine_event(app_name) logging.info(f"app_name: {app_name}") t = threading.Thread(target=monitor_block_height, args=(private_rpc_url, public_rpc_urls)) t.start() t.join() 启动脚本 1234567891011121314#!/bin/bash# 增加一个启动参数,app_name,如传入则使用传入的app_name,并加入到启动命令中app_name=$1# 设置启动日志文件路径start_log_file="logs/start_script.log"# 启动 Python 脚本,并将输出保存到启动日志文件nohup python3.10 monitor.py $app_name > $start_log_file 2>&1 &# 获取启动的 Python 进程的PID并保存到文件中echo $! > pid_file.txtecho "脚本已在后台运行。PID为:$(cat pid_file.txt)" 停止脚本 12345678910111213141516#!/bin/bash# 获取之前保存的PID文件pid_file="pid_file.txt"pid=$(cat $pid_file 2>/dev/null)if [ -z "$pid" ]; then echo "未找到PID。脚本可能未在运行。"else # 终止Python进程 kill -TERM $pid echo "PID为 $pid 的脚本已停止。" # 删除PID文件 rm $pid_filefi","link":"/2024/11/02/node-monitor/"},{"title":"Carry-Coin 记服务迁移和流量优化","text":"最近Contabo服务器频繁死机,发邮件给官方反应问题,一开始嘴硬说没问题让自查,沟通2天又是截图又是各种开票,最后承认问题说技术团队排查但是不给解决时间 邮件回复 VNC过去看到 sda3硬盘一直挂不上/dev/sda3: recovering journal,猜测不是硬件存储坏了就是虚拟化平台抽风;好在重启5-8次大概能进系统一次,赶紧拷数据闪人; 目前部署架构 原来contabo的机器8u24g,32TTraffic一个月$26, 相同配置国内厂商看了一圈没有羊毛,最后选择tx,但是相同配置明显贵上天,只能调整架构先开台低配2u4g/90ssd轻量服务器,把front、server、db弄回来,worker后面再说; 程序迁移后大问题没有,每种不足就是出口流量太吃紧了,轻量应用流量包只有2T,跑了10个小时流量80多G,一天毛估估200G; 优化过程iftop大概观察下流量去向,其实心里也有数,整机对外一个是通过nginx访问的front 这是给自己看的前端,还有一个就是mysql,几个worker每秒多线程读写,这部分传输过程中流量花费巨大;资源的话cpu的使用率基本维持在50左右,内存40%左右,优化空间还是有的 jeecgboot 前端肿的不行,所以nginx gzip该压的压起来,改了后观察监控发现提升可以忽略不计。 mysql是大头,这块翻了一些优化料大部分都是持久化侧的,表压缩之类的;所以换了个思路,既然是传输过程中的损耗那么大概率是jdbc驱动的事情,这么常规的场景应该有支持; 翻了mysql-connector-j-8.0.33.jar代码发现果然有戏com.mysql.cj.protocol.a.NativeProtocol中有个字段useCompression开启后mysql的传输过程会压缩,但是默认是关闭的,可以在jdbc连接字符串后面加上useCompression=true来开启压缩; CompressedPacketSender开关打开后会使用com.mysql.cj.protocol.a.CompressedPacketSender来发送数据; 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697/** * Packet sender implementation for the compressed MySQL protocol. For compressed transmission of multi-packets, split the packets up in the same way as the * uncompressed protocol. We fit up to MAX_PACKET_SIZE bytes of split uncompressed packet, including the header, into an compressed packet. The first packet * of the multi-packet is 4 bytes of header and MAX_PACKET_SIZE - 4 bytes of the payload. The next packet must send the remaining four bytes of the payload * followed by a new header and payload. If the second split packet is also around MAX_PACKET_SIZE in length, then only MAX_PACKET_SIZE - 4 (from the * previous packet) - 4 (for the new header) can be sent. This means the payload will be limited by 8 bytes and this will continue to increase by 4 at every * iteration. * * @param packet * data bytes * @param packetLen * packet length * @param packetSequence * sequence id * @throws IOException * if i/o exception occurs */ public void send(byte[] packet, int packetLen, byte packetSequence) throws IOException { this.compressedSequenceId = packetSequence; // short-circuit send small packets without compression and return if (packetLen < MIN_COMPRESS_LEN) { writeCompressedHeader(packetLen + NativeConstants.HEADER_LENGTH, this.compressedSequenceId, 0); writeUncompressedHeader(packetLen, packetSequence); this.outputStream.write(packet, 0, packetLen); this.outputStream.flush(); return; } if (packetLen + NativeConstants.HEADER_LENGTH > NativeConstants.MAX_PACKET_SIZE) { this.compressedPacket = new byte[NativeConstants.MAX_PACKET_SIZE]; } else { this.compressedPacket = new byte[NativeConstants.HEADER_LENGTH + packetLen]; } PacketSplitter packetSplitter = new PacketSplitter(packetLen); int unsentPayloadLen = 0; int unsentOffset = 0; // loop over constructing and sending compressed packets while (true) { this.compressedPayloadLen = 0; if (packetSplitter.nextPacket()) { // rest of previous packet if (unsentPayloadLen > 0) { addPayload(packet, unsentOffset, unsentPayloadLen); } // current packet int remaining = NativeConstants.MAX_PACKET_SIZE - unsentPayloadLen; // if remaining is 0 then we are sending a very huge packet such that are 4-byte header-size carryover from last packet accumulated to the size // of a whole packet itself. We don't handle this. Would require 4 million packet segments (64 gigs in one logical packet) int len = Math.min(remaining, NativeConstants.HEADER_LENGTH + packetSplitter.getPacketLen()); int lenNoHdr = len - NativeConstants.HEADER_LENGTH; addUncompressedHeader(packetSequence, packetSplitter.getPacketLen()); addPayload(packet, packetSplitter.getOffset(), lenNoHdr); completeCompression(); // don't send payloads with incompressible data if (this.compressedPayloadLen >= len) { // combine the unsent and current packet in an uncompressed packet writeCompressedHeader(unsentPayloadLen + len, this.compressedSequenceId++, 0); this.outputStream.write(packet, unsentOffset, unsentPayloadLen); writeUncompressedHeader(lenNoHdr, packetSequence); this.outputStream.write(packet, packetSplitter.getOffset(), lenNoHdr); } else { sendCompressedPacket(len + unsentPayloadLen); } packetSequence++; unsentPayloadLen = packetSplitter.getPacketLen() - lenNoHdr; unsentOffset = packetSplitter.getOffset() + lenNoHdr; resetPacket(); } else if (unsentPayloadLen > 0) { // no more packets, send remaining unsent data addPayload(packet, unsentOffset, unsentPayloadLen); completeCompression(); if (this.compressedPayloadLen >= unsentPayloadLen) { writeCompressedHeader(unsentPayloadLen, this.compressedSequenceId, 0); this.outputStream.write(packet, unsentOffset, unsentPayloadLen); } else { sendCompressedPacket(unsentPayloadLen); } resetPacket(); break; } else { // nothing left to send (only happens on boundaries) break; } } this.outputStream.flush(); // release reference to (possibly large) compressed packet buffer this.compressedPacket = null; } 整体思路高效地发送数据包,无论是小包还是大包,同时通过压缩减少传输数据的大小。它通过拆分、压缩和适当的序列管理确保数据完整和顺序发送. 改了以后程序跑起来,超出预期,流量消耗少了近50%,代价是cpu提了10%左右,划算。 先跑着一个月后再看。","link":"/2024/09/02/traffic_optimization_idea/"}],"tags":[{"name":"Java","slug":"Java","link":"/tags/Java/"},{"name":"Python","slug":"Python","link":"/tags/Python/"},{"name":"Architecture","slug":"Architecture","link":"/tags/Architecture/"},{"name":"jasypt","slug":"jasypt","link":"/tags/jasypt/"},{"name":"SpringBoot","slug":"SpringBoot","link":"/tags/SpringBoot/"},{"name":"python","slug":"python","link":"/tags/python/"},{"name":"日志清理","slug":"日志清理","link":"/tags/%E6%97%A5%E5%BF%97%E6%B8%85%E7%90%86/"},{"name":"HummingBot","slug":"HummingBot","link":"/tags/HummingBot/"},{"name":"bsc","slug":"bsc","link":"/tags/bsc/"},{"name":"optimization","slug":"optimization","link":"/tags/optimization/"}],"categories":[{"name":"CarryCoin","slug":"CarryCoin","link":"/categories/CarryCoin/"},{"name":"HummingBot","slug":"HummingBot","link":"/categories/HummingBot/"}],"pages":[{"title":"about me","text":"姓名 Gong Wei性别 男工作经验 10years+学历 本科学校 北京航空航天大学 - 计算机科学与技术邮箱 blackjackhoho@gmail.comTelegram @blackjackhohoGithub https://github.com/konbluesky 14年的老开发,在寻找一个激情、开放、和谐的团队,与志同道合的伙伴共同创造卓越的项目。 基础技能 精通Java,超过10年的企业级Java程序开发经验,对项目中用到的框架包括但不限于(Spring Full Stack,Mybatis,Netty等)有Cover和Hack能力; 熟悉Nodejs、Python、Shell等动态语言,能够灵活运用这些脚本语言解决项目中的碎片化问题,如原型验证、数据处理、自动化脚本等; 擅长OOP&OOD,有复杂系统0-1设计和1-N的重构经验; 擅长Linux下工作和DevOps实践;","link":"/about/index.html"}]} \ No newline at end of file