├── .gitignore ├── README.md ├── config-sample.txt ├── docs ├── PARSING_PATTERN.md └── VERSIONS.md ├── pom.xml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── mbcu │ │ │ └── mmm │ │ │ ├── api │ │ │ └── ApiManager.java │ │ │ ├── helpers │ │ │ └── TAccountOffer.java │ │ │ ├── main │ │ │ ├── Main.java │ │ │ └── WebSocketClient.java │ │ │ ├── models │ │ │ ├── Base.java │ │ │ ├── dataapi │ │ │ │ ├── AccountBalance.java │ │ │ │ └── Balance.java │ │ │ ├── internal │ │ │ │ ├── AgBalance.java │ │ │ │ ├── BefAf.java │ │ │ │ ├── BotConfig.java │ │ │ │ ├── Config.java │ │ │ │ ├── Cpair.java │ │ │ │ ├── Credentials.java │ │ │ │ ├── Intervals.java │ │ │ │ ├── LastAmount.java │ │ │ │ ├── LastBuySellTuple.java │ │ │ │ ├── LedgerEvent.java │ │ │ │ ├── NameIssuer.java │ │ │ │ ├── PartialOrder.java │ │ │ │ ├── RLOrder.java │ │ │ │ └── TRLOrder.java │ │ │ └── request │ │ │ │ ├── AccountInfo.java │ │ │ │ ├── AccountOffers.java │ │ │ │ ├── BookOffers.java │ │ │ │ ├── Request.java │ │ │ │ ├── Submit.java │ │ │ │ ├── Subscribe.java │ │ │ │ └── TeeEx.java │ │ │ ├── notice │ │ │ ├── Content.java │ │ │ └── SenderSES.java │ │ │ ├── rx │ │ │ ├── BusBase.java │ │ │ ├── RxBus.java │ │ │ └── RxBusProvider.java │ │ │ ├── sequences │ │ │ ├── Balancer.java │ │ │ ├── Base.java │ │ │ ├── Common.java │ │ │ ├── Dataapi.java │ │ │ ├── Emailer.java │ │ │ ├── LogNum.java │ │ │ ├── Orderbook.java │ │ │ ├── Starter.java │ │ │ ├── Tester.java │ │ │ ├── Txc.java │ │ │ ├── Txd.java │ │ │ ├── counters │ │ │ │ ├── Counter.java │ │ │ │ └── Yuki.java │ │ │ └── state │ │ │ │ ├── State.java │ │ │ │ └── StateProvider.java │ │ │ └── utils │ │ │ ├── GsonUtils.java │ │ │ ├── Mock.java │ │ │ ├── MyLogger.java │ │ │ └── MyUtils.java │ └── webapp │ │ ├── WEB-INF │ │ └── web.xml │ │ └── index.jsp └── test │ └── java │ └── com │ ├── mbcu │ └── mmm │ │ ├── sequences │ │ ├── CommonTest.java │ │ ├── NotifierTest.java │ │ └── counters │ │ │ └── YukiTest.java │ │ └── utils │ │ └── UtilsTest.java │ └── ripple │ └── core │ ├── QualitySuperLongTest.java │ ├── QualityTest.java │ ├── STObjectTest.java │ └── coretypes │ └── CurrencyTest.java └── target └── .gitignore /.gitignore: -------------------------------------------------------------------------------- 1 | .metadata 2 | bin/ 3 | tmp/ 4 | *.tmp 5 | *.bak 6 | *.swp 7 | *~.nib 8 | local.properties 9 | .settings/ 10 | .loadpath 11 | .recommenders 12 | .classpath 13 | .project 14 | 15 | # External tool builders 16 | .externalToolBuilders/ 17 | 18 | # Locally stored "Eclipse launch configurations" 19 | *.launch 20 | 21 | # PyDev specific (Python IDE for Eclipse) 22 | *.pydevproject 23 | 24 | # CDT-specific (C/C++ Development Tooling) 25 | .cproject 26 | 27 | # Java annotation processor (APT) 28 | .factorypath 29 | 30 | # PDT-specific (PHP Development Tools) 31 | .buildpath 32 | 33 | # sbteclipse plugin 34 | .target 35 | 36 | # Tern plugin 37 | .tern-project 38 | 39 | # TeXlipse plugin 40 | .texlipse 41 | 42 | # STS (Spring Tool Suite) 43 | .springBeans 44 | 45 | # Code Recommenders 46 | .recommenders/ 47 | 48 | # Scala IDE specific (Scala & Java development for Eclipse) 49 | .cache-main 50 | .scala_dependencies 51 | .worksheet 52 | 53 | config.txt 54 | log*.* 55 | orderbook*.txt 56 | /target/ 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java Ripple Market Maker 2 | 3 | An automated program to market make on Ripple network with grid method. 4 | 5 | The bot prioritizes speed: any order will be pushed immediately to the ledger and retried if needed until it's in. 6 | 7 | Requires [ripple-lib-java: ripple-core](https://github.com/ripple/ripple-lib-java) to build. 8 | 9 | 10 | Principal elements: 11 | 12 | **Parser** : parses response from stream and grabs events of interest 13 | 14 | **Counter** : counters each order consumed with a new order 15 | 16 | **Balancer**: maintains the number of orders and their original information in orderbook 17 | 18 | **State** : keeps track of account sequence number which is used to send orders 19 | 20 | 21 | To use: 22 | ``` 23 | java -jar .jar 24 | ``` 25 | 26 | Documentations: 27 | 28 | [Parsing pattern](docs/PARSING_PATTERN.md) 29 | 30 | [Version log](docs/VERSIONS.md) 31 | 32 | 33 | ## Strategies 34 | 35 | In general the principle of grid trading is simple. Seed orderbook with orders spaced by price ("seed"). If an order is consumed, place a new order with a new rate calculated from the consumed rate ("counter"). As we want profit, when a buy order is consumed, sell it back at higher price, when a sell order is consumed, buy it back at lower price. Every a few ledgers, the bot will check the orderbook and add necessary orders on either side. 36 | 37 | The IOU to trade, number of orders, grid space, amount, etc are defined in the config. The following strategies determine order countering: 38 | 39 | 40 | #### Fixed Partial `partial` 41 | 42 | Any order consumed will be immediately countered with new order equals to the amount that consumed it. The new unit price is spaced rigidly by gridSpace, e.g. if a buy order with price X is consumed then a new sell order selling the same quantity with price X + gridSpace will be created. If a sell order with price Y is consumed then a buy order with the same quantity and price Y - gridSpace will be created. 43 | 44 | #### Fixed Full `fullfixed` 45 | 46 | [Youtube Demo](https://www.youtube.com/watch?v=dth4D28p4zk) 47 | 48 | The same as fixed partial but the bot will counter only if the order is fully consumed. 49 | 50 | #### Proportional `ppt` 51 | 52 | In this mode both base quantity and unit price are spaced by percentage point of the previous offerCreate level. 53 | 54 | For sell direction p1 = (1 + gridSpace / 100) * p0 and q1 = q0 / (1 + gridSpace / 100)^0.5 55 | 56 | For buy direction p1 = p0 / (1 + gridSpace / 100) and q1 = q0 * (1 + gridSpace / 100)^0.5 57 | 58 | 59 | #### Attention 60 | The bot uses the initial offerCreate as reference for next order either as seed or counter. Preferably the bot should start when orderbook is empty. If not then it will assume any order in the orderbook as the original offerCreate. If you start the bot this way, **make sure partially filled orders in the orderbook are deleted** because they don't reflect their original offerCreates. 61 | 62 | ## Config 63 | 64 | #### General configuration 65 | 66 | `feeXRP` : *String* 67 | 68 | Base fee in XRP (default "0.000012") 69 | 70 | `datanet` : *String* 71 | 72 | Historical Data API url. 73 | 74 | `emails` : *String* 75 | 76 | Contact emails the bot will send reports to. 77 | 78 | Emails are sent from AWS SES. To use this feature you need to : 79 | - Register the emails in SES Sandbox Mode. These emails will be used as both recipient and sender. 80 | - Set up the SES credentials in your environment. You can put the credentials in ~/.aws/credentials or export them to environment variables. 81 | 82 | #### Intervals (optional) 83 | 84 | Intervals are the numbers of elapsed ledger validated events which will trigger following actions. 85 | 86 | `balancer` : *int* 87 | 88 | Balancer checks our orders and seeds missing orders according to bot configuration (default = 4) 89 | 90 | `accountBalance` : *int* 91 | 92 | Checks how much IOU the account has (default = 10) 93 | 94 | #### Bot configuration 95 | 96 | `pair` : *String* 97 | 98 | Currency or IOU with base.baseIssuer/quote.quoteIssuer format. 99 | 100 | `startMiddlePrice` : *float* 101 | 102 | Starting rate for seeder. 103 | 104 | `gridSpace` : *float* 105 | 106 | Price level between orders, behavior determined by strategy. 107 | 108 | `buyGridLevels` and `sellGridLevels` : *int* 109 | 110 | Number of seed orders for buy and sell sides respectively. 111 | 112 | `buyOrderQuantity` and `sellOrderQuantity` : *float* 113 | 114 | Amount of seed and counter order. This value is used for any strategy beside *partial* 115 | 116 | `strategy` : *String* 117 | 118 | Strategy to be used. Refer to strategy section for valid names. 119 | 120 | ## When Error Happens 121 | 122 | ### Error by Rippled 123 | 124 | As a blockchain rippled does not guarantee each order sent to it to be included in the ledger. The bot will act depending response from the server. In general responses are categorized into : 125 | - Quasi-success: tesSUCCESS, ter_ (i.e. terQUEUED and terPRE_SEQ) are orders that have the chance to get into the ledger. If successful, server will send stream data about the order on the subscribed channel up to the order's maxledger. If maxledger is passed without any such data, the bot will resubmit the same order. 126 | - Immediate-fail : tefPAST_SEQ and errors indicating that the order was ok, but submitted with wrong sequence number or under a circumstance that otherwise would be fine at different time. Such order is immediately resubmitted after bot's sequence number is synchronized. 127 | - Fund-related fail : if the wallet no longer has enough fund to push an order then the order will be discarded. One dangerous error is terINSUF_FEE_B in which the bot no longer has enough XRP. The bot will not stop although no further order can be created. 128 | 129 | ### Error by Network 130 | 131 | Any network error and websocket disconnect will cause system exit with signal 1. A process control system like supervisord may catch this signal and restart the bot. 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /config-sample.txt: -------------------------------------------------------------------------------- 1 | { 2 | "net": "wss://s1.ripple.com", 3 | "datanet" : "https://data.ripple.com/", 4 | "feeXRP" : "0.000015", 5 | "credentials": { 6 | "address": "WALLET ADDRESS", 7 | "secret": "SECRET KEY" 8 | }, 9 | "emails" : [ 10 | "error_report_sent_here@gmail.com", 11 | "error_report_sent_here@test.com" 12 | ], 13 | "intervals" : { 14 | "balance" : 4, 15 | "accountBalance" : 2000 16 | }, 17 | "bots": [ 18 | { 19 | "pair": "XRP/JPY.rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS", 20 | "startMiddlePrice": 100, 21 | "gridSpace": 0.5, 22 | "buyGridLevels": 2, 23 | "sellGridLevels": 2, 24 | "buyOrderQuantity": 1, 25 | "sellOrderQuantity": 1, 26 | "strategy" : "ppt" 27 | }, 28 | { 29 | "pair": "XRP/USD.rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS", 30 | "startMiddlePrice": 0.20, 31 | "gridSpace": 1, 32 | "buyGridLevels": 2, 33 | "sellGridLevels": 2, 34 | "buyOrderQuantity": 2, 35 | "sellOrderQuantity": 1, 36 | "strategy" : "partial" 37 | 38 | }, 39 | { 40 | "pair": "USD.rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS/JPY.rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS", 41 | "startMiddlePrice": 112, 42 | "gridSpace": 1, 43 | "buyGridLevels": 3, 44 | "sellGridLevels": 3, 45 | "buyOrderQuantity": 2, 46 | "sellOrderQuantity": 1, 47 | "strategy" : "fullfixed" 48 | 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /docs/PARSING_PATTERN.md: -------------------------------------------------------------------------------- 1 | # PARSER 2 | 3 | Always use https://xrpcharts.ripple.com/#/transactions to get a clear description of a tx 4 | 5 | The most important element in a response is probably DeletedNode. DeletedNode tells if an order was executed, modified, canceled due to OfferCancel or lack of fund. 6 | 7 | ### The Summary 8 | Any request for transaction history either by websocket's account_tx or Data Api's /transactions command will return all transactions involving owner's account. For the sake of perspective owner will be referred to as "us". 9 | 10 | Basically a typical transaction response consists of **tx** and **meta**. 11 | 12 | tx consists of original [transactions](https://ripple.com/build/transactions/) request. 13 | 14 | meta consists of information of how this transaction affects our and other people's orders, balance, and also blockchain's ledger. 15 | 16 | Since the focus of this bot project is trade, the breakdown is this: 17 | 18 | 1. The only transactions affecting orders are : OfferCreate, OfferCancel, and Payment. 19 | 20 | 2. If we are the creator of the offer then our address will be in tx.Account and tx.TransactionType of interest is either OfferCreate or OfferCancel. 21 | All transactions that happen in meta are important to us. Other people's transactions that are consumed by our offer may appear in DeletedNodes as fully-filled order or ModifiedNodes as partially-filled order. A CreatedNode that contains our account is the original offerCreate (or its left over after it hits some orders) that appears in the ledger. If our address appears in some DeletedNodes after we push an offerCreate it means we no longer have enough fund to maintain all our offers. As a consequence, some of our offers are automatically canceled and reflected in those deletedNodes. 22 | 23 | 3. If we are not the creator then tx.Account does not have our address. We will need to pay attention to tx.TransactionType equals OfferCreate and Payment. In meta, only transactions (in DeletedNodes or ModifiedNodes) with our address are important to us. These are our orders which were consumed. Transactions not with our address affect other people's orders and we don't care about those. 24 | 25 | ## DeletedNodes 26 | - Only pay attention to node with LedgerEntryType="Offer" 27 | - if tx.Account == our Address, all nodes are important. If tx.Account != our address, only consider the ones with our address. 28 | 29 | ### Offer Executed 30 | - DeletedNode has nested.PreviousFields 31 | - if tx.Account == our Address, all DeletedNodes with other people's address are their filled orders. If it has our address read the part about "Offer Canceled Not By OfferCancel". 32 | If tx.Account != our address, then all DeletedNodes with our address are our filled orders. 33 | - check also ModifiedNodes for partially filled orders with the same rules. 34 | 35 | ### Offer Modified 36 | - modifying (or editing) an order will cancel and replace it with a new one 37 | - tx.TransactionType is offerCreate and has key "OfferSequence" containing the old order's sequence 38 | - DeletedNode contains the old pre-modified offer 39 | 40 | ### Offer Canceled Not By OfferCancel 41 | - existing orders may be automatically canceled if no longer funded. This may happen when a new offer is created. 42 | - in this case tx.Account == our address and tx.TransactionType == OfferCreate. deletedNodes are the offers that get canceled 43 | 44 | ## ModifiedNodes 45 | - contain partially filled orders 46 | - contain changes in balance 47 | 48 | ## CreatedNode 49 | - contains created order which appears in the ledger (or orderbook). The amount will be equal to the original offerCreate or equal to the left over after it consumes some orders in the orderbook. 50 | 51 | ### Samples 52 | - partial filled, unfunded E356B7AA6ADBB90CB4BA280AA1FF6E92E6054A76192E40980F95C118629B4E15, E8378AEBC3B1B78CE0AE8219B603DBD6A18420004B615981A99F125332FC3702 53 | - executed : 95674414E0824878892D8B6C518F35CC5667D8B0700B90189C2A01886462DE71 54 | - offercancel : 9641D3F019A5177F9AB93B09250A311D927CD44E3EC8296B3C2EECE0AE377589 55 | - edit : 2227DA34FB27F500F5E04052569D9B345668817FC449FE7BE0E5627D41A26C06 56 | - offercreate, full : no deletednode 57 | - offerCreate, own, fully consumes : 33F4D4FA8564B75C5DD13FE4B32C961DBE6C2009FCE30BB4C18200B598B3E6F7 (no created node) 58 | - offercreate, partial : 7931DC82FB680321C5A3949EC8B81CEE088A7928BBC170BE5816C39EEE716AF7 (txn has original request, one created node has offer and the remaining amount that enters the orderbook) 59 | - offerCreate, other, fully consumed : 568F82829CE9EEE5223ABC22AC63DF8550FA2E1F18A990A880466401D5CF6FB9 60 | 61 | 62 | 63 | 64 | # RANDOM NOTES 65 | 66 | ## RESPONSE 67 | - response contains hash so it can be used as key. This hash should be stored in "deleted" node. 68 | - Final field in stream indicates executed offer. 69 | -- hash -> DeletedNodes.FinalFields.PreviousTxnID (old hash), hash (new hash) 70 | - offer create for RJP/JPY may not have PreviousFields. This causes parse error for FinalFields. It looks creating new offer also cancels some tx which results in this situation. We can safely ignore it and use transaction information instead. 71 | - payment and OC belonging to others will result in many OEs. Find only those belonging to us 72 | - do not instantiate Amount directly. Use RLOrder#amount 73 | - pair in Config determines the unit of rate and gridSpace 74 | - ter errors including terQUEUED and terPRE_SEQ behave like tesSUCCESS https://www.xrpchat.com/topic/2654-transaction-failed-with-terpre_seq-but-still-executed/?page=2 75 | - any error in the code is wrapped in WebSocketClient#handleCallbackError . This can be parsing error (unhandled response type) 76 | - sending counter order that crosses the rate of previous remaining order will cancel it. The counter will prevail. 77 | 78 | 79 | ## ORDERS 80 | When our order consumes 81 | - OC belongs to us 82 | - OE belongs to others. 83 | - If the order is fully consumed in the process then no order is created 84 | - If the order is partially consumed in the process then new order with remainder as quantity is created 85 | - Sequence always increases 86 | - OE with other account and FinalFields.TakerPays and TakerGets == 0 means their order is fully consumed, but we don't care 87 | - Counter : OE belonging to others 88 | 89 | When our order is consumed 90 | - OC belongs to others. Can be payment which belongs to others. 91 | - When consumed no new order is created 92 | - Sequence does not change if taken or partially taken. Only our first OC adds the sequence. 93 | - Counter : OE with our account 94 | - OE with our account and FinalFields.TakerPays and TakerGets == 0 means fully consumed 95 | 96 | ## Autobridge 97 | - autobridge rate (quality) never changes 98 | - autobridge currency is XRP 99 | - created rate and quantity may not be the same as create offer rate and quantity 100 | - ~~Response must have two OEs each one against XRP.~~ 101 | - Response can have many OEs in either direction but the sum of XRP of them all must be equal. 102 | - ~~Real rate = both of OEs' rates multiplied~~ 103 | - Real quantity = look at OE's non-XRP quantity 104 | - OEs have different accounts than ours 105 | - OEs' PreviousTxnID are not the same as OC's hash 106 | - Ignore OC's with other's account. It might consume our order. 107 | - If order is not fully matched then autobridge tx will produce extra tx to sell or buy bridge currency (XRP). 108 | - Rate = sum of all principal quantities / sum of all principal's counter quantities. 109 | 110 | tecUNFUNDED_OFFER 111 | - happens on non-XRP currency 112 | - happens when the balance is 0 113 | 114 | ## Editing Order 115 | - basically sending OfferCreate which has OfferSequence of the offer edited 116 | 117 | ## Balancer 118 | - if both sides in orderbook are empty, seed from midPrice 119 | - if order level is lower than config, seed more orders toward worse rate (buy -> cheaper rate, sell -> higher rate) 120 | - if one side is empty, seed it with rate from the best price of the other side 121 | 122 | 123 | ## Others 124 | - Sequence increases when we have OrderCreate, not necessarily OrderCreated 125 | - Any FinalFields with prices zero means order fully consumed. 126 | - Payment doesn't have our autobridge. But it has other people's autobridge with our order in it. 127 | - OC belonging to other don't have our autobridge either. See payment. 128 | - hash also appears in executedoffer so we can trace it back to orderbook 129 | - if our consuming orders leaves a remainder and is consumed then it is treated like our order is consumed 130 | - total consumed doesn't raise sequence 131 | - Payments can have multiple same account OEs when it consumes. 132 | - OE contains previousTxnId 133 | - It's possible to create new OC with more funds than the account has 134 | -------------------------------------------------------------------------------- /docs/VERSIONS.md: -------------------------------------------------------------------------------- 1 | ## Version History 2 | 3 | v.071 4 | - fixed partial countering on full strategies 5 | - some logic in deletednode (unfunded cancel, etc) was affected regardless of tx.account 6 | 7 | v.07 8 | - Common#filterStream2 all deletedNodes and modifiedNodes are first saved. If txn.account == myAddress then all these are significant (canceled offer due to lack of fund), if txn.account != myAddress, filter out offers which don't belong to myAddress 9 | 10 | v.0613 pre-merged, summary 11 | - parser bug fixed (deletednode, creatednode, modifiednode) 12 | - strategies rearranged 13 | - network or websocket error will do system exit with error signal 14 | - RLORder.fromOfferCreate 15 | 16 | v.0612 17 | - fullfixed refactored, tested 18 | - ppt tested 19 | 20 | v.0611 21 | - working on fullfixed and partial 22 | - RLORder.fromOfferCreate(Transaction txn) seemed to create wrong direction. I flipped it and also flipped yukiPct. 23 | 24 | v.0610 25 | - websocket or network problem will cause system.exit(1) . This way supervisord will catch it and restart the bot. 26 | 27 | v.0609 28 | - found errors in how DeletedNode was interpreted. DeletedNodes may contain orders which are canceled after they became unfunded. These orders are not OfferCancel. This is the cause of orders not removed from orderbook and therefore causing a lot of doubles (06001 to 06008). 29 | - amended 0608. No automatic edit event changes direction. This is side effect from the above error. 30 | 31 | v.0608 32 | - edit can change the direction (buy <-> sell) (false) 33 | - dangling order didn't get removed (log.missed_remove_order.txt.0) 34 | 35 | v.0607 36 | - partial consume removes the order from orderbook 37 | 38 | v.0606 39 | - both ways work 40 | - testing error : log.rhXCRgFH7xdCfDBrpQSjUTfCnoiSUWeTMc.2017_12_27_13_22_45_997.txt 5538 -> 9565 41 | - edit may not find sequence so obsolete sequence is left dangling in orderbook becoming anchor for seed. 42 | - attempt fix : edit on both sides everytime 43 | 44 | v.0605 45 | - fixed orderbook seed generator (tested on both ways) 46 | 47 | v.0604 48 | - total1 = sqrt * q0 / u1 test this 49 | 50 | v.0603 51 | - buy is correct 52 | - sell not correct 53 | - both active not correct 54 | 55 | v.0602 56 | - sell and buy 80% 57 | - every sellGridlevels there's a jump in price 58 | 59 | v.0601 60 | - moving to pure pct strategy 61 | - buy seed, counter 70% ok, check newrate when buys is not empty 62 | - test RLOrder 454 63 | - do the same for sell 64 | 65 | v.060 66 | - (06001) fixed fullrateseedpct counter 67 | - streamed counter 68 | 69 | v.059 70 | - all trade settings should be customizable in botconfig 71 | - describe them in README 72 | - fixed partial counter 73 | - change of config 74 | 75 | 76 | v.058 77 | - Seed amount should be changed by percentage too 78 | - log if one side or both sides of orderbook are empty 79 | - streamed pctseed 80 | 81 | v.057 82 | - fixed percentage mode on counters. 83 | 84 | v.056 85 | - fixed percentage mode. Counter order is created from percentage of the rate consumed. 86 | - added log to drop below zero in buy seed and counter. 87 | 88 | v.055 89 | - added seed by percentage gridspace 90 | - balancer not adding missing orders ? 91 | 92 | v.054 93 | - see in progress tasks 94 | - Befaf now holds txnId for debugging. To be continued after txnId is captured for division by Zero. 95 | - fixed precision for XRP too small error 96 | 97 | v.053 98 | - fixed account_offers for more than 200 orders 99 | 100 | v.052 101 | - fixed email sender. Supervisor runs on sudo so need to place credentials on the same level. 102 | 103 | v.051 104 | - attempt to add maxFee by adding it in Transaction failed 105 | 106 | v.050 107 | - missing order error fix. Txs were inserted with the same sequence number (happens if there's a sequence refresh and counter). Now submission checks if seq exists. 108 | - removed seed in between orders. 109 | - fixed double order issue. When one side in orderbook was empty, balancer would seed new orders from midPrice. This caused counters to fill already filled price level on the other side. Now when one side is empty, seed it from the best price of the other side with one gridlevel gap. 110 | 111 | 112 | v.049 113 | - Account balance is sent periodically to emails. 114 | 115 | v.048 116 | - Attach wallet address on log 117 | 118 | v.047 119 | - Added non-tesSuccess handler in stream causing false OfferCreate. TecUnfunded and other errors will not register as OfferCreate anymore. 120 | - Added warning in Orderbook#generate. Order explosion should only come from this function. 121 | 122 | v.046 123 | - Account balance check. 124 | - Change in `config.txt` (intervals, datanet) 125 | 126 | v.045 127 | - Email notification. Change in `config.txt` 128 | 129 | v.044 130 | - Fixed 04201. Possibly blank entry in wallet page. To avoid this, Common now checks if final node contains Account or not. False edit doesn't have account and has HighLimit, LowLimit although it has prevTxnId 131 | 132 | v.043 133 | v.042 134 | 135 | v.041 136 | - fixed 04101 137 | - unwrap exceptions in all Observables 138 | 139 | v.040 140 | - removed generate seed from midprice 141 | - error severe -1 (forgot to start balancer generate from 0 as I no longer seed from spread) 142 | 143 | v.039 144 | - balancer generate now seeds from startmiddle and between orders. 145 | - probably need to remove generate seed from middleprice (may end up with lots of counter orders attacking the other side). 146 | 147 | v.038 148 | - fixed 03701 149 | 150 | v.037 151 | - commented out balancer trim 152 | - commented out orderbook edit (bug) 153 | 154 | v.036 155 | - fixed orderbook worst rates 156 | 157 | v.035 158 | - balancer interval changed to 4 159 | 160 | v.034 161 | - balancer sell fixed 162 | 163 | v.033 164 | - balancer tested 165 | 166 | v.032 167 | - balancer generate completed 168 | 169 | v.031 170 | - balancer trim completed 171 | - changed String pair to Cpair class 172 | 173 | v.030 174 | - balancer waits until state is clear of pending tx. 175 | 176 | v.029 177 | - Orderbook sum modified for sells. Need to check 178 | 179 | v.028 180 | - Made MaxLedger to call sequence sync, otherwise it will loop into terPreSeqs. 181 | 182 | v.027 183 | - Made ter to behave like tesSuccess 184 | 185 | v.026 186 | - Cleaned up post TakerGets error. 187 | - Disabled Balancer 188 | 189 | v.025 190 | - Fixed no TakerGets error. This offer should be ignored https://github.com/ripple/rippled/issues/2199 191 | 192 | v.024 193 | - Orderbook can be built from stream events, tested on Order canceled, edited, and difference 194 | 195 | v.023 196 | - testing OnDifference 197 | 198 | v.022 199 | - whole order counter is adjusted with gridSpace 200 | - normalized partial and whole counters 201 | 202 | v.021 203 | - orderbook on initiation 204 | 205 | v.020 206 | - balancer from orderbook start 207 | - deactivated seed 208 | 209 | v.019 210 | - alpha replace order 211 | - added isReplaceMode in bot config. 212 | - cleaned up replacement counter 213 | 214 | v.018 215 | - fixed the remainder counter but partial counter for this strategy is not logical because at the same rate it cancels the previous order. 216 | 217 | v.017 218 | - fixed bug in tx submission. XRP should be sent as native without issuer 219 | - limited log files at 20MB with 20 rotations 220 | 221 | v.016 222 | - alpha remainder counter (counter if a percentage of original order is taken) 223 | - tested on consume and consumed 224 | 225 | v.015 226 | - building whole counter and percentage of original order 227 | 228 | v.014 229 | - alpha. double orders are caused by resubmitting terPRE_SEQ. 230 | 231 | v.013 232 | - bug1: Txc OnResponseFail was not matched against own hash or sequence. 233 | - removed synchronized in State 234 | - need to test on preSeq or pastSeq 235 | 236 | v.012 237 | - pre-alpha State. This would be equal to mmm in node.js 238 | - State manages order submission and trial 239 | 240 | v.011 241 | - tested on Arbitrage without autobridge. 242 | - fixed some calculation errors. 243 | 244 | v.010 245 | - privatized NamePair 246 | - used Observer on all sequences (intercept error) 247 | 248 | v.009 249 | - pre-alpha counter 250 | - removed RLAmount. 251 | 252 | v.008 253 | - debugging counter 254 | 255 | v.007 256 | - finalizing alpha counter 257 | 258 | v.006 259 | - completed parser for counter. 260 | 261 | v.005 262 | - parser now parsed Autobridge correctly. 263 | 264 | v.004 265 | - improved parser so OE mimic history text response on wallet app 266 | 267 | v.003 268 | - alpha version of parser 269 | 270 | v.002 271 | - improving getQuality 272 | 273 | v.001 274 | - added missing src/main/resources folder for Maven requirement 275 | - added README.md 276 | 277 | 278 | 279 | ### TODOS 280 | - [x] (done cancel-order.txt) see delete offer response 281 | - (offer-executed-bridged.txt) see executed offer response 282 | - [x] get response for autobridge 283 | - [x] (done) build parser to find previous tx by hash 284 | - (canceled) parser returns currency pair, rate, quantity, and tx hash. 285 | - [x] (done) filter returns onEdited, onCanceled, onNewOrder, onOffers 286 | - the hash replaces previous hash (partial take, full take, cancel) or insert new (create new) 287 | - partial take and reduces current offer in orderbook. Edit changes an order. Cancel and Full Take removes an order. 288 | - [x] (done) autobridge tx should result in original rate. 289 | - our OEs should return FinalFields and hash they modify 290 | - [x] (done) OC should also return sequence 291 | - [x] (done) define a class to hold sequence, update and get it concurrently. 292 | - (canceled, pointless) move all bus elements to the end of parser 293 | - update hash for OC. It looks like OC we send doesn't have meta 294 | - [x] (done)define Counter class 295 | - [x] (fixed) v.009 value precision of 18 is greater than maximum iou precision of 16 296 | - (v.011) test new counter on autobridge 297 | - tefALREADY needs retry ? 298 | - [x] (done) build listener for remaining order after taken and original quantity and rate if it's fully taken 299 | - [x] (done) list all OR in a list 300 | - rearrange log 301 | - [x] fix the remainder counter Check BeAf 302 | - [x] partial remainder counter is not logical. If the partial amount is countered with the same rate then the previous order will be canceled. 303 | - (canceled) continue Txc so it disposes old disposable and turn it into orderbook item 304 | - [x] get Amount from account_offers result 305 | - [x] bus for account_offers should also have currency pair 306 | - [x] intercept pairs on orderbook or balancer level 307 | - [x] sort RLOrder by rate 308 | - [x] write orderbook to files 309 | - [x] BefAf needs sequence 310 | - [x] Orderbook test onDelete 311 | - [x] Orderbook test onEdit 312 | - [x] Orderbook test onRemainder 313 | - [x] get RLORder from State#pending and State#qWait for Orderbook balancer, synchronously 314 | - [x] balancer can sum all orders, compare with settings 315 | - [x] remove if sum is more than config 316 | - [x] create if sum is less than config 317 | - [x] cancel order mechanism 318 | - [x] edit updates sequence 319 | - [x] cancel updates sequence 320 | - [x] compare orderbook sum with setting's sum 321 | - [x] (bug) no TaketGets in PreviousFields . Create null checker to ignore such offer. 322 | - [x] ter in Txc sets isTesSuccess true because they have the same behavior. 323 | - [x] as ter behaves like tesSUCCESS then MaxLedger passed should refresh sequence, otherwise it will go into terPastSeq loop. 324 | - [x] orderbook sum doesn't return the right sum 325 | - [x] Common needs to pass onCreate id from our address only 326 | - [x] Check anything affected by String pair 327 | - [x] (bug) Orderbook buys is empty. 328 | - [x] check balancer generate for sells and both 329 | - [x] turn off seed on balancer 330 | - [x] save worst rates for every orderbook 331 | - [x] any reseed should start from these prices 332 | - [x] (bug) some canceled txs are resubmitted in infinity. 333 | - [x] (bug) worst rate calculation must be done separately for buys and sells otherwise the rate will clock up when one of them is empty. 334 | - [x] (bug) see 03701 sequence wasn't set in Common@271 335 | - [x] find a way to unwrap exception 336 | - [x] may need to cancel generate seed from middleprice 337 | - [] bot creates buy orders down to 0 price 338 | - [x] unknown edit (B078019B57E783C3467419BB4C6ED93770A60CB766ACF6A00A2AABF88D3BE98E). This is rolled into new OfferExecuted logic. 339 | - [x] add warning email system in case of any unhandled errors 340 | - [x] automated balance check 341 | - [x] error may arrive in stream not response. Handle this. 342 | - [x] log files should be named per account 343 | - [x] account balance sends email periodically 344 | - [x] need to trace all retry tx 345 | - [x] missing order error, check response_sample/missing_order.txt 346 | - [x] log files should be named per start ts 347 | - [x] 05001 ledger number contains holes 348 | - [x] need to know if offerCreate comes from seed or counter 349 | - ~~[] get reference market price~~ 350 | - [x] balancer seeder skips a rate if all order consumed. If the gap = 2*levels then it's correct. 351 | - [x] double orders on same price. when restarted, orders will start from startMiddle when empty. This will cause double orders on the other side 352 | - ~~[] need maxFee.~~ 353 | - [x] move fee to config 354 | - [x] test email sender with credentials in env. 355 | - [x] fix division by zero 356 | - [x] capture txnId for BefAf 357 | - [x] fix XRP too small 358 | - [x] fix account_offers for more than 200 orders 359 | - [x] add percentage counter 360 | - [] fix static counter 361 | - [x] test counter. the range between buys and sells != gridspace 362 | - [x] fix 06001 363 | - [] build tx response from rpc tool to json interpreted by Common 364 | - [x] wrong balancer post-delete 365 | - [x] offercancel still results in unfunded offer message 366 | - [] rearrange readme 367 | - [] websocketclient exception check 368 | 369 | 370 | 01701 : sending XRP tx with form /XRP/rrrrrrrrrrrrrrrrrrrrrhoLvTp will not work. It has to be done natively by the lib. 371 | 03701 : 372 | OFFER EDITED 14FDEE8C8EA4408B39FEE0C3A43354B570ABF263939183EA7B67BA3F365AF71D to FDFEB58D863867A841741C02BE2986F66E026A9EEB2D0E5C279214BA13EE0D4B 373 | Aug 23, 2017 3:35:26 PM com.mbcu.mmm.main.WebSocketClient$1 handleCallbackError 374 | SEVERE: null 375 | 04001 : 376 | tefMAX_LEDGER raNDu1gNyZ5hipBTKxm5zx7NovA1rNnNRf 82DC1F52F51E697A32DF3E6D84FA78B4D5A48E65EA33C267A29CE6A857C5D0DE 30142 377 | 8 25, 2017 1:04:51 午前 com.mbcu.mmm.main.WebSocketClient$1 handleCallbackError 378 | SEVERE: JSONObject["validated_ledgers"] not found. 379 | 04201 : Response showing edit although there was no edit, blank entry in history tab. 380 | False edits: 381 | 61D3DC620E3BF2B36B936B8509BB68AB703041B1A80454E9AE39F0AE5F453CFE 382 | E94C67ECAB3483015D545C96FD84DFDED6B2D789A1E6CEBD0B330D0855B3C315 383 | Real edit: 384 | 7DF67EB046C88C49D8D2DFFF4B11339640D0ABFDF26182539E7712DD83530C84 385 | 04601 386 | Unfunded offer arriving in stream: 387 | {"engine_result":"tecUNFUNDED_OFFER","engine_result_code":103,"engine_result_message":"Insufficient balance to fund created offer.","ledger_hash":"1DDC59A766EE8C6EEACBBA7B08E967148FD2B86ED86AC8493F2785796CC06CCE","ledger_index":32729222,"meta":{"AffectedNodes":[{"ModifiedNode":{"FinalFields":{"Account":"raNDu1gNyZ5hipBTKxm5zx7NovA1rNnNRf","Balance":"1256217681","Flags":0,"OwnerCount":30,"Sequence":31392},"LedgerEntryType":"AccountRoot","LedgerIndex":"72A59CDF8FFBF65C20D01D3A3D5DA5BAE3158A3881E7AEE525A01B1CC73D32DD","PreviousFields":{"Balance":"1256217696","Sequence":31391},"PreviousTxnID":"450ACD8995147924A04AB3E10F07F252D0209754B12B4ACD7910387644BD2128","PreviousTxnLgrSeq":32729167}}],"TransactionIndex":5,"TransactionResult":"tecUNFUNDED_OFFER"},"status":"closed","transaction":{"Account":"raNDu1gNyZ5hipBTKxm5zx7NovA1rNnNRf","Fee":"15","Flags":2147483648,"LastLedgerSequence":32729226,"Sequence":31391,"SigningPubKey":"02E2F1208D1715E18B0957FC819546FA7434B4A19EE38321932D2ED28FA090678E","TakerGets":{"currency":"DOG","issuer":"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS","value":"1060"},"TakerPays":"10000000","TransactionType":"OfferCreate","TxnSignature":"304402204A9C478A41102D1BECD3409E9F125FE091B4F4B597DA4965964C1CBCE61BC1EB022051D4FD1ACA89B8E4276FB7A843C0862B96C09254F023E1F2CD2E5EDB131A8F54","date":558603441,"hash":"3E528E50A60CF4E582BE70976D9177CB162E5CAFA493BAF391ACAC68C4DC54D4","owner_funds":"0"},"type":"transaction","validated":true} 388 | 05001 389 | { 390 | "raw": "{\"fee_base\":10,\"fee_ref\":10,\"ledger_hash\":\"0A9103E0AE7724FA2E355A6B906306A97668A3A6955311C3261F7315148B6172\",\"ledger_index\":419711,\"ledger_time\":558842422,\"reserve_base\":20000000,\"reserve_inc\":5000000,\"txn_count\":0,\"type\":\"ledgerClosed\",\"validated_ledgers\":\"158188-419709,419711\"}\n" 391 | } 392 | object-info end 393 | trace start 394 | java.lang.NumberFormatException: For input string: "419709,419711" 395 | at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) 396 | at java.lang.Integer.parseInt(Integer.java:580) 397 | at java.lang.Integer.parseInt(Integer.java:615) 398 | at com.mbcu.mmm.models.internal.LedgerEvent.fromJSON(LedgerEvent.java:28) 399 | at com.mbcu.mmm.sequences.Common$OnLedgerClosed.(Common.java:583) 400 | at com.mbcu.mmm.sequences.Common.filterLedgerClosed(Common.java:96) 401 | at com.mbcu.mmm.sequences.Common.reroute(Common.java:90) 402 | at com.mbcu.mmm.sequences.Common.access$0(Common.java:82) 403 | at com.mbcu.mmm.sequences.Common$1.onNext(Common.java:59) 404 | 06001 405 | Problem with XRP/ETC.MRR: too many orders 406 | { 407 | "pair": "XRP/ETC.rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS", 408 | "startMiddlePrice": 0.0127, 409 | "gridSpace": 1, 410 | "buyGridLevels": 2, 411 | "sellGridLevels": 2, 412 | "buyOrderQuantity": 2, 413 | "sellOrderQuantity": 1, 414 | "strategy" : "fullrateseedpct" 415 | } 416 | 06008 : error_logs 417 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | jp.fi 5 | mmm 6 | war 7 | 0.0.1-SNAPSHOT 8 | mmm Maven Webapp 9 | http://maven.apache.org 10 | 11 | 12 | com.neovisionaries 13 | nv-websocket-client 14 | 2.2 15 | 16 | 17 | 18 | com.google.code.gson 19 | gson 20 | 2.8.0 21 | 22 | 23 | 24 | org.parceler 25 | parceler 26 | 1.1.7 27 | provided 28 | 29 | 30 | 31 | org.parceler 32 | parceler-api 33 | 1.1.7 34 | 35 | 36 | 37 | org.json 38 | json 39 | 20170516 40 | 41 | 42 | 43 | io.reactivex.rxjava2 44 | rxjava 45 | 2.1.0 46 | 47 | 48 | 49 | com.jakewharton.rxrelay2 50 | rxrelay 51 | 2.0.0 52 | 53 | 54 | 55 | com.amazonaws 56 | aws-java-sdk 57 | 1.11.185 58 | 59 | 60 | 61 | com.squareup.retrofit2 62 | retrofit 63 | 2.3.0 64 | 65 | 66 | 67 | 68 | com.squareup.retrofit2 69 | converter-gson 70 | 2.0.0-beta3 71 | 72 | 73 | 74 | junit 75 | junit 76 | 3.8.1 77 | test 78 | 79 | 80 | 81 | ${project.artifactId} 82 | 83 | 84 | maven-compiler-plugin 85 | 3.2 86 | 87 | 1.8 88 | 1.8 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/api/ApiManager.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.api; 2 | 3 | import com.mbcu.mmm.models.dataapi.AccountBalance; 4 | import com.mbcu.mmm.models.internal.Config; 5 | 6 | import retrofit2.Call; 7 | import retrofit2.GsonConverterFactory; 8 | import retrofit2.Retrofit; 9 | import retrofit2.http.GET; 10 | import retrofit2.http.Path; 11 | import retrofit2.http.Query; 12 | 13 | public class ApiManager { 14 | private Retrofit REST_ADAPTER; 15 | private DataApiService DATAAPI_SERVICE; 16 | 17 | public interface DataApiService { 18 | 19 | @GET("/v2/accounts/{address}/balances") 20 | Call getBalances(@Path("address") String address, @Query("date") String date); 21 | 22 | } 23 | 24 | public ApiManager(Config config) { 25 | REST_ADAPTER = new Retrofit.Builder().baseUrl(config.getDatanet()) 26 | .addConverterFactory(GsonConverterFactory.create()).build(); 27 | DATAAPI_SERVICE = REST_ADAPTER.create(DataApiService.class); 28 | } 29 | 30 | public DataApiService getService() { 31 | return DATAAPI_SERVICE; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/helpers/TAccountOffer.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.helpers; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.json.JSONException; 6 | import org.json.JSONObject; 7 | 8 | import com.mbcu.mmm.models.internal.RLOrder; 9 | import com.mbcu.mmm.models.internal.RLOrder.Direction; 10 | import com.ripple.core.coretypes.AccountID; 11 | import com.ripple.core.coretypes.Amount; 12 | import com.ripple.core.coretypes.Currency; 13 | 14 | public class TAccountOffer { 15 | RLOrder order; 16 | int seq; 17 | private static BigDecimal MILLION = new BigDecimal("1000000"); 18 | 19 | public static TAccountOffer of(JSONObject j) { 20 | TAccountOffer res = new TAccountOffer(); 21 | res.seq = j.getInt("seq"); 22 | Amount takerGets = from(j, "taker_gets"); 23 | Amount takerPays = from(j, "taker_pays"); 24 | res.order = RLOrder.rateUnneeded(Direction.BUY, takerPays, takerGets); 25 | return res; 26 | } 27 | 28 | private static Amount from(JSONObject whole, String key) { 29 | Amount res; 30 | try { 31 | JSONObject inside = whole.getJSONObject(key); 32 | BigDecimal value = new BigDecimal(inside.getString("value")); 33 | Currency currency = Currency.fromString(inside.getString("currency")); 34 | AccountID issuer = AccountID.fromAddress(inside.getString("issuer")); 35 | res = RLOrder.amount(value, currency, issuer); 36 | } catch (JSONException e) { 37 | BigDecimal drops = new BigDecimal(whole.getString(key)); 38 | res = RLOrder.amount(drops.divide(MILLION)); 39 | } 40 | return res; 41 | } 42 | 43 | public RLOrder getOrder() { 44 | return order; 45 | } 46 | 47 | public int getSeq() { 48 | return seq; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/main/Main.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.main; 2 | 3 | import java.io.IOException; 4 | 5 | import com.mbcu.mmm.models.internal.Config; 6 | import com.mbcu.mmm.sequences.Starter; 7 | import com.mbcu.mmm.utils.MyLogger; 8 | import com.neovisionaries.ws.client.WebSocketException; 9 | 10 | public class Main { 11 | 12 | public static void main(String[] args) throws IOException, WebSocketException, InterruptedException { 13 | Config config = Config.build(args[0]); 14 | MyLogger.setup(config); 15 | Starter manager = Starter.newInstance(config); 16 | manager.start(); 17 | } 18 | 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/main/WebSocketClient.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.main; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.logging.Logger; 7 | 8 | import com.mbcu.mmm.models.internal.Config; 9 | import com.mbcu.mmm.rx.BusBase; 10 | import com.mbcu.mmm.rx.RxBus; 11 | import com.mbcu.mmm.rx.RxBusProvider; 12 | import com.mbcu.mmm.sequences.Emailer; 13 | import com.mbcu.mmm.utils.MyLogger; 14 | import com.neovisionaries.ws.client.ThreadType; 15 | import com.neovisionaries.ws.client.WebSocket; 16 | import com.neovisionaries.ws.client.WebSocketException; 17 | import com.neovisionaries.ws.client.WebSocketFactory; 18 | import com.neovisionaries.ws.client.WebSocketFrame; 19 | import com.neovisionaries.ws.client.WebSocketListener; 20 | import com.neovisionaries.ws.client.WebSocketState; 21 | 22 | public class WebSocketClient { 23 | private final static Logger LOGGER = Logger.getLogger(WebSocketClient.class.getName()); 24 | private WebSocket ws; 25 | private RxBus bus = RxBusProvider.getInstance(); 26 | 27 | public WebSocketClient(Config config) throws IOException { 28 | 29 | this.ws = new WebSocketFactory().setConnectionTimeout(5000).createSocket(config.getNet()) 30 | .addListener(new WebSocketListener() { 31 | 32 | @Override 33 | public void onUnexpectedError(WebSocket arg0, WebSocketException e) throws Exception { 34 | bus.send(new WSError(e)); 35 | } 36 | 37 | @Override 38 | public void onThreadStopping(WebSocket arg0, ThreadType arg1, Thread arg2) throws Exception { 39 | // TODO Auto-generated method stub 40 | 41 | } 42 | 43 | @Override 44 | public void onThreadStarted(WebSocket arg0, ThreadType arg1, Thread arg2) throws Exception { 45 | // TODO Auto-generated method stub 46 | 47 | } 48 | 49 | @Override 50 | public void onThreadCreated(WebSocket arg0, ThreadType arg1, Thread arg2) throws Exception { 51 | // TODO Auto-generated method stub 52 | 53 | } 54 | 55 | @Override 56 | public void onTextMessageError(WebSocket arg0, WebSocketException e, byte[] arg2) throws Exception { 57 | bus.send(new WSError(e)); 58 | } 59 | 60 | @Override 61 | public void onTextMessage(WebSocket arg0, String raw) throws Exception { 62 | bus.send(new WSGotText(raw)); 63 | } 64 | 65 | @Override 66 | public void onTextFrame(WebSocket arg0, WebSocketFrame arg1) throws Exception { 67 | // TODO Auto-generated method stub 68 | 69 | } 70 | 71 | @Override 72 | public void onStateChanged(WebSocket arg0, WebSocketState arg1) throws Exception { 73 | // TODO Auto-generated method stub 74 | 75 | } 76 | 77 | @Override 78 | public void onSendingHandshake(WebSocket arg0, String arg1, List arg2) throws Exception { 79 | // TODO Auto-generated method stub 80 | 81 | } 82 | 83 | @Override 84 | public void onSendingFrame(WebSocket arg0, WebSocketFrame arg1) throws Exception { 85 | // TODO Auto-generated method stub 86 | 87 | } 88 | 89 | @Override 90 | public void onSendError(WebSocket arg0, WebSocketException e, WebSocketFrame arg2) throws Exception { 91 | bus.send(new WSError(e)); 92 | } 93 | 94 | @Override 95 | public void onPongFrame(WebSocket arg0, WebSocketFrame arg1) throws Exception { 96 | // TODO Auto-generated method stub 97 | 98 | } 99 | 100 | @Override 101 | public void onPingFrame(WebSocket arg0, WebSocketFrame arg1) throws Exception { 102 | // TODO Auto-generated method stub 103 | 104 | } 105 | 106 | @Override 107 | public void onMessageError(WebSocket arg0, WebSocketException e, List arg2) throws Exception { 108 | bus.send(new WSError(e)); 109 | } 110 | 111 | @Override 112 | public void onMessageDecompressionError(WebSocket arg0, WebSocketException e, byte[] arg2) throws Exception { 113 | bus.send(new WSError(e)); 114 | } 115 | 116 | @Override 117 | public void onFrameUnsent(WebSocket arg0, WebSocketFrame arg1) throws Exception { 118 | // TODO Auto-generated method stub 119 | 120 | } 121 | 122 | @Override 123 | public void onFrameSent(WebSocket arg0, WebSocketFrame arg1) throws Exception { 124 | // TODO Auto-generated method stub 125 | 126 | } 127 | 128 | @Override 129 | public void onFrameError(WebSocket arg0, WebSocketException e, WebSocketFrame arg2) throws Exception { 130 | bus.send(new WSError(e)); 131 | } 132 | 133 | @Override 134 | public void onFrame(WebSocket arg0, WebSocketFrame arg1) throws Exception { 135 | // TODO Auto-generated method stub 136 | 137 | } 138 | 139 | @Override 140 | public void onError(WebSocket arg0, WebSocketException e) throws Exception { 141 | bus.send(new WSError(e)); 142 | } 143 | 144 | @Override 145 | public void onDisconnected(WebSocket arg0, WebSocketFrame arg1, WebSocketFrame arg2, boolean arg3) 146 | throws Exception { 147 | bus.send(new WSDisconnected()); 148 | } 149 | 150 | @Override 151 | public void onContinuationFrame(WebSocket arg0, WebSocketFrame arg1) throws Exception { 152 | // TODO Auto-generated method stub 153 | } 154 | 155 | @Override 156 | public void onConnected(WebSocket ws, Map> arg1) throws Exception { 157 | bus.send(new WSConnected()); 158 | } 159 | 160 | @Override 161 | public void onConnectError(WebSocket arg0, WebSocketException e) throws Exception { 162 | bus.send(new WSError(e)); 163 | } 164 | 165 | @Override 166 | public void onCloseFrame(WebSocket arg0, WebSocketFrame arg1) throws Exception { 167 | // TODO Auto-generated method stub 168 | 169 | } 170 | 171 | @Override 172 | public void onBinaryMessage(WebSocket arg0, byte[] arg1) throws Exception { 173 | // TODO Auto-generated method stub 174 | 175 | } 176 | 177 | @Override 178 | public void onBinaryFrame(WebSocket arg0, WebSocketFrame arg1) throws Exception { 179 | // TODO Auto-generated method stub 180 | 181 | } 182 | 183 | @Override 184 | public void handleCallbackError(WebSocket ws, Throwable e) { 185 | LOGGER.severe(e.getMessage()); 186 | System.exit(-1); 187 | } 188 | }); 189 | 190 | initBus(); 191 | } 192 | 193 | private void initBus() { 194 | bus.toObservable().subscribe(o -> { 195 | BusBase base = (BusBase) o; 196 | try { 197 | if (base instanceof WSRequestSendText) { 198 | WSRequestSendText event = (WSRequestSendText) o; 199 | ws.sendText(event.request); 200 | } 201 | else if (base instanceof WSError) { 202 | WSError event = (WSError) base; 203 | bus.send(new Emailer.SendEmailWSError(event)); 204 | } 205 | else if (base instanceof WSRequestShutdown) { 206 | ws.disconnect(1013); // try again later code 207 | WSRequestShutdown event = (WSRequestShutdown) base; 208 | System.exit(event.isUnexpected ? 1 : 0); 209 | } 210 | } catch (Exception e) { 211 | MyLogger.exception(LOGGER, base.toString(), e); 212 | } 213 | 214 | }); 215 | } 216 | 217 | public void start() throws WebSocketException, IOException { 218 | ws.connect(); 219 | } 220 | 221 | public static class WSConnected extends BusBase { 222 | } 223 | 224 | public static class WSDisconnected extends BusBase { 225 | } 226 | 227 | public static class WSError extends BusBase { 228 | public final Exception e; 229 | 230 | public WSError(Exception e) { 231 | super(); 232 | this.e = e; 233 | } 234 | 235 | } 236 | 237 | public static class WSGotText extends BusBase { 238 | public String raw; 239 | 240 | public WSGotText(String raw) { 241 | super(); 242 | this.raw = raw; 243 | } 244 | 245 | } 246 | 247 | public static class WSRequestSendText extends BusBase { 248 | public String request; 249 | 250 | public WSRequestSendText(String request) { 251 | super(); 252 | this.request = request; 253 | } 254 | 255 | } 256 | 257 | public static class WSRequestShutdown extends BusBase { 258 | public final boolean isUnexpected; 259 | 260 | public WSRequestShutdown(boolean isUnexpected) { 261 | this.isUnexpected = isUnexpected; 262 | } 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/Base.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models; 2 | 3 | import com.mbcu.mmm.utils.GsonUtils; 4 | 5 | public abstract class Base { 6 | 7 | protected String stringify(Object object) { 8 | return GsonUtils.toJson(object); 9 | } 10 | 11 | public abstract String stringify(); 12 | } -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/dataapi/AccountBalance.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.dataapi; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public class AccountBalance { 9 | 10 | String result; 11 | int ledger_index; 12 | int limit; 13 | List balances = new ArrayList(); 14 | 15 | public String getResult() { 16 | return result; 17 | } 18 | 19 | public void setResult(String result) { 20 | this.result = result; 21 | } 22 | 23 | public int getLedger_index() { 24 | return ledger_index; 25 | } 26 | 27 | public void setLedger_index(int ledger_index) { 28 | this.ledger_index = ledger_index; 29 | } 30 | 31 | public int getLimit() { 32 | return limit; 33 | } 34 | 35 | public void setLimit(int limit) { 36 | this.limit = limit; 37 | } 38 | 39 | public List getBalances() { 40 | return balances; 41 | } 42 | 43 | public void setBalances(List balances) { 44 | if (balances != null) { 45 | this.balances.addAll(balances); 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/dataapi/Balance.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.dataapi; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import com.mbcu.mmm.models.internal.NameIssuer; 6 | import com.ripple.core.coretypes.AccountID; 7 | import com.ripple.core.coretypes.Amount; 8 | import com.ripple.core.coretypes.Currency; 9 | 10 | public class Balance { 11 | 12 | String currency; 13 | String value; 14 | String counterparty; 15 | 16 | public String getCurrency() { 17 | return currency; 18 | } 19 | 20 | public void setCurrency(String currency) { 21 | this.currency = currency; 22 | } 23 | 24 | public String getValue() { 25 | return value; 26 | } 27 | 28 | public void setValue(String value) { 29 | this.value = value; 30 | } 31 | 32 | public String getCounterparty() { 33 | return counterparty; 34 | } 35 | 36 | public void setCounterparty(String counterparty) { 37 | this.counterparty = counterparty; 38 | } 39 | 40 | public Amount toAmount() { 41 | if (currency.equals(Currency.XRP.toString())) { 42 | return new Amount(new BigDecimal(value)); 43 | } 44 | return new Amount(new BigDecimal(value), Currency.fromString(currency), 45 | AccountID.fromAddress(counterparty)); 46 | } 47 | 48 | public NameIssuer toSignature() { 49 | Amount a; 50 | if (currency.equals(Currency.XRP.toString())) { 51 | a = Amount.ONE_XRP; 52 | } else { 53 | a = new Amount(com.ripple.core.coretypes.Currency.fromString(currency), AccountID.fromAddress(counterparty)); 54 | } 55 | return NameIssuer.from(a); 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return toAmount().stringRepr(); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/AgBalance.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import org.joda.time.DateTime; 7 | import org.joda.time.DateTimeZone; 8 | 9 | import com.mbcu.mmm.models.dataapi.AccountBalance; 10 | import com.mbcu.mmm.models.dataapi.Balance; 11 | 12 | public class AgBalance { 13 | 14 | long ts; 15 | int ledgerIndex; 16 | DateTime dt; 17 | Map data = new HashMap<>(); 18 | 19 | public static AgBalance from(AccountBalance in, long ts) { 20 | AgBalance res = new AgBalance(); 21 | Map data = new HashMap<>(); 22 | in.getBalances().forEach(balance -> { 23 | data.put(balance.toSignature(), balance); 24 | }); 25 | 26 | res.data.putAll(data); 27 | res.ts = ts; 28 | res.dt = new DateTime(ts * 1000, DateTimeZone.forID("Asia/Tokyo")); 29 | return res; 30 | } 31 | 32 | public long getTs() { 33 | return ts; 34 | } 35 | 36 | public int getLedgerIndex() { 37 | return ledgerIndex; 38 | } 39 | 40 | public DateTime getDt() { 41 | return dt; 42 | } 43 | 44 | public Map getData() { 45 | return data; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/BefAf.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | import com.mbcu.mmm.models.Base; 4 | import com.ripple.core.coretypes.hash.Hash256; 5 | import com.ripple.core.coretypes.uint.UInt32; 6 | 7 | public class BefAf extends Base { 8 | public final Hash256 txnId; 9 | public final RLOrder before; 10 | public final RLOrder after; 11 | public final UInt32 befSeq; 12 | public final RLOrder source; 13 | 14 | public BefAf(RLOrder before, RLOrder after, UInt32 befSeq, Hash256 txnId, RLOrder source) { 15 | super(); 16 | this.before = before; 17 | this.after = after; 18 | this.befSeq = befSeq; 19 | this.txnId = txnId; 20 | this.source = source; 21 | } 22 | 23 | @Override 24 | public String stringify() { 25 | StringBuffer res = new StringBuffer("BefAf"); 26 | res.append("\n"); 27 | res.append(txnId.toString()); 28 | res.append("\n"); 29 | res.append(before.stringify()); 30 | if (after != null) { 31 | res.append(after.stringify()); 32 | } else { 33 | res.append("After is null"); 34 | } 35 | return res.toString(); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/BotConfig.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | import java.io.IOException; 4 | import java.math.BigDecimal; 5 | import java.math.MathContext; 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | 10 | import com.mbcu.mmm.models.request.BookOffers; 11 | import com.mbcu.mmm.utils.MyUtils; 12 | import com.ripple.core.coretypes.AccountID; 13 | import com.ripple.core.coretypes.Amount; 14 | import com.ripple.core.coretypes.Currency; 15 | 16 | 17 | public class BotConfig { 18 | 19 | private String pair; 20 | private String startMiddlePrice; 21 | private String gridSpace; 22 | private int buyGridLevels; 23 | private int sellGridLevels; 24 | private String buyOrderQuantity; 25 | private String sellOrderQuantity; 26 | private String strategy; 27 | 28 | transient Amount base; 29 | transient Amount quote; 30 | transient BigDecimal totalBuyQty, totalSelQty; 31 | transient List orderbookReqs; 32 | 33 | 34 | public enum Strategy { 35 | PARTIAL, 36 | FULLFIXED, 37 | PPT; 38 | } 39 | 40 | public static HashMap buildMap(Credentials credentials, ArrayList bots) throws IOException { 41 | HashMap res = new HashMap<>(); 42 | for (BotConfig bot : bots) { 43 | String[] pair = buildBaseAndQuote(bot.getPair()); 44 | bot.base = fromDotForm(pair[0]); 45 | bot.quote = fromDotForm(pair[1]); 46 | bot.orderbookReqs = BookOffers.buildRequest(credentials.address, bot); 47 | bot.totalBuyQty = buildTotalQuantity(bot.buyGridLevels, bot.buyOrderQuantity); 48 | bot.totalSelQty = buildTotalQuantity(bot.sellGridLevels, bot.sellOrderQuantity); 49 | 50 | res.put(bot.getPair(), bot); 51 | if (!MyUtils.isInEnum(bot.strategy, Strategy.class)){ 52 | throw new IOException(String.format("Strategy \"%s\" is not recognized", bot.strategy)); 53 | } 54 | if (bot.getStrategy().equals(BotConfig.Strategy.PPT)) { 55 | BigDecimal pct = new BigDecimal(bot.gridSpace); 56 | pct = BigDecimal.ONE.add(pct.divide(new BigDecimal("100"), MathContext.DECIMAL64)); 57 | bot.gridSpace = pct.toPlainString(); 58 | } 59 | } 60 | return res; 61 | } 62 | 63 | private static BigDecimal buildTotalQuantity(int gridLevels, String orderQuantity) { 64 | BigDecimal a = new BigDecimal(gridLevels); 65 | BigDecimal b = new BigDecimal(orderQuantity); 66 | return a.multiply(b, MathContext.DECIMAL64); 67 | } 68 | 69 | public static Amount fromDotForm(String part) throws IllegalArgumentException { 70 | String currency = null; 71 | String issuer = null; 72 | String[] b = part.split("[.]"); 73 | if (!b[0].equals(Currency.XRP.toString()) && b.length != 2) { 74 | throw new IllegalArgumentException("currency pair not formatted in base.issuer/quote.issuer"); 75 | } else { 76 | currency = b[0]; 77 | if (b.length == 2) { 78 | issuer = b[1]; 79 | } 80 | } 81 | return issuer == null ? new Amount(new BigDecimal("0")) 82 | : new Amount(Currency.fromString(currency), AccountID.fromAddress(issuer)); 83 | } 84 | 85 | public static String[] buildBaseAndQuote(String pair) { 86 | return pair.split("[/]"); 87 | } 88 | 89 | public String getPair() { 90 | return pair; 91 | } 92 | 93 | public String getReversePair() { 94 | String[] els = pair.split("[/]"); 95 | StringBuffer res = new StringBuffer(els[1]); 96 | res.append("/"); 97 | res.append(els[0]); 98 | return res.toString(); 99 | } 100 | 101 | public void setPair(String pair) { 102 | this.pair = pair; 103 | } 104 | 105 | public BigDecimal getStartMiddlePrice() { 106 | return new BigDecimal(startMiddlePrice); 107 | } 108 | 109 | public void setStartMiddlePrice(String startMiddlePrice) { 110 | this.startMiddlePrice = startMiddlePrice; 111 | } 112 | 113 | public BigDecimal getGridSpace() { 114 | return new BigDecimal(this.gridSpace); 115 | } 116 | 117 | public int getBuyGridLevels() { 118 | return buyGridLevels; 119 | } 120 | 121 | public void setBuyGridLevels(int buyGridLevels) { 122 | this.buyGridLevels = buyGridLevels; 123 | } 124 | 125 | public int getSellGridLevels() { 126 | return sellGridLevels; 127 | } 128 | 129 | public void setSellGridLevels(int sellGridLevels) { 130 | this.sellGridLevels = sellGridLevels; 131 | } 132 | 133 | public BigDecimal getBuyOrderQuantity() { 134 | return new BigDecimal(buyOrderQuantity); 135 | } 136 | 137 | public BigDecimal getSellOrderQuantity() { 138 | return new BigDecimal(sellOrderQuantity); 139 | } 140 | 141 | public List getOrderbookRequests() { 142 | return orderbookReqs; 143 | } 144 | 145 | public Amount getBase() { 146 | return base; 147 | } 148 | 149 | public Amount getQuote() { 150 | return quote; 151 | } 152 | 153 | public BigDecimal getTotalBuyQty() { 154 | return totalBuyQty; 155 | } 156 | 157 | public BigDecimal getTotalSelQty() { 158 | return totalSelQty; 159 | } 160 | 161 | 162 | public Strategy getStrategy() { 163 | return Strategy.valueOf(strategy.toUpperCase()); 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/Config.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | import java.io.IOException; 4 | import java.lang.reflect.Type; 5 | import java.math.BigDecimal; 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.stream.Collectors; 9 | 10 | import com.google.gson.JsonDeserializationContext; 11 | import com.google.gson.JsonDeserializer; 12 | import com.google.gson.JsonElement; 13 | import com.google.gson.JsonParseException; 14 | import com.mbcu.mmm.utils.GsonUtils; 15 | import com.mbcu.mmm.utils.MyUtils; 16 | 17 | public class Config { 18 | private static final int DEFAULT_INTERVAL_BALANCER = 4; 19 | private static final int DEFAULT_INTERVAL_ACCOUNT_BALANCE = 2; 20 | public static final int HOUR_ACCOUNT_BALANCER_EMAILER = 6; 21 | private static final String DEFAULT_FEE_XRP = "0.000012"; 22 | 23 | private String net; 24 | private String datanet; 25 | private BigDecimal feeXRP; 26 | private Credentials credentials; 27 | private ArrayList emails; 28 | private Intervals intervals; 29 | private transient HashMap botConfigMap; 30 | private ArrayList bots; 31 | 32 | public void setBotConfigMap(HashMap botConfigMap) { 33 | this.botConfigMap = botConfigMap; 34 | } 35 | 36 | public HashMap getBotConfigMap() { 37 | return botConfigMap; 38 | } 39 | 40 | public Credentials getCredentials() { 41 | return credentials; 42 | } 43 | 44 | public void setCredentials(Credentials credentials) { 45 | this.credentials = credentials; 46 | } 47 | 48 | public BigDecimal getFeeXRP() { 49 | return feeXRP; 50 | } 51 | 52 | public void setDatanet(String datanet) { 53 | this.datanet = datanet; 54 | } 55 | 56 | public String getDatanet() { 57 | return datanet; 58 | } 59 | 60 | public String getNet() { 61 | return net; 62 | } 63 | 64 | public void setNet(String net) { 65 | this.net = net; 66 | } 67 | 68 | public Intervals getIntervals() { 69 | return intervals; 70 | } 71 | 72 | public void setIntervals(Intervals intervals) { 73 | this.intervals = intervals; 74 | } 75 | 76 | public ArrayList getBots() { 77 | return bots; 78 | } 79 | 80 | public void setBots(ArrayList bots) { 81 | this.bots = bots; 82 | } 83 | 84 | public ArrayList getEmails() { 85 | return emails; 86 | } 87 | 88 | public static Config build(String fileName) throws IOException { 89 | String raw = MyUtils.readFile(fileName); 90 | 91 | Config res = GsonUtils.toBean(raw, Config.class); 92 | if (res == null) { 93 | throw new IOException("Failed to parse config"); 94 | } 95 | 96 | if (res.bots.isEmpty()) { 97 | throw new IOException("Bots are empty"); 98 | } 99 | 100 | if (res.feeXRP == null) { 101 | res.feeXRP = new BigDecimal(DEFAULT_FEE_XRP); 102 | } 103 | 104 | if (res.intervals == null) { 105 | Intervals intervals = new Intervals(); 106 | intervals.accountBalance = DEFAULT_INTERVAL_ACCOUNT_BALANCE; 107 | intervals.balancer = DEFAULT_INTERVAL_BALANCER; 108 | res.setIntervals(intervals); 109 | } else { 110 | if (res.intervals.accountBalance < DEFAULT_INTERVAL_ACCOUNT_BALANCE) { 111 | res.intervals.accountBalance = DEFAULT_INTERVAL_ACCOUNT_BALANCE; 112 | } 113 | if (res.intervals.balancer < DEFAULT_INTERVAL_BALANCER) { 114 | res.intervals.balancer = DEFAULT_INTERVAL_BALANCER; 115 | } 116 | } 117 | 118 | int s = res.emails.stream().filter(MyUtils::isEmail).collect(Collectors.toList()).size(); 119 | 120 | if (s != res.emails.size()) { 121 | throw new IOException("Wrong email format"); 122 | } 123 | 124 | res.setBotConfigMap(BotConfig.buildMap(res.getCredentials(), res.getBots())); 125 | res.setBots(null); 126 | return res; 127 | } 128 | 129 | class BigDecimalDeserializer implements JsonDeserializer { 130 | 131 | @Override 132 | public BigDecimal deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext c) 133 | throws JsonParseException { 134 | 135 | return json.getAsBigDecimal(); 136 | } 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/Cpair.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | import java.util.Optional; 4 | 5 | import com.ripple.core.coretypes.Amount; 6 | import com.ripple.core.coretypes.Currency; 7 | import com.ripple.core.coretypes.STObject; 8 | import com.ripple.core.types.known.sle.entries.Offer; 9 | 10 | public class Cpair { 11 | 12 | private final String fw; 13 | private final String rv; 14 | 15 | private Cpair(String pair) { 16 | super(); 17 | this.fw = pair; 18 | this.rv = getReversePair(); 19 | } 20 | 21 | private String getReversePair() { 22 | String[] p = this.fw.split("[/]"); 23 | StringBuffer res = new StringBuffer(p[1]); 24 | res.append("/"); 25 | res.append(p[0]); 26 | return res.toString(); 27 | } 28 | 29 | public Optional isMatch(String test) { 30 | if (test.equals(fw)) { 31 | return Optional.of(true); 32 | } else if (test.equals(rv)) { 33 | return Optional.of(false); 34 | } 35 | return Optional.empty(); 36 | } 37 | 38 | public String getFw() { 39 | return fw; 40 | } 41 | 42 | public String getRv() { 43 | return rv; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return fw; 49 | } 50 | 51 | public static Cpair newInstance(String pair) { 52 | return new Cpair(pair); 53 | } 54 | 55 | public static Cpair newInstance(String base, String baseIssuer, String quote, String quoteIssuer) { 56 | StringBuffer sb = new StringBuffer(base); 57 | if (!base.equals(Currency.XRP.toString())) { 58 | sb.append("."); 59 | sb.append(baseIssuer); 60 | } 61 | sb.append("/"); 62 | sb.append(quote); 63 | if (!quote.equals(Currency.XRP.toString())) { 64 | sb.append("."); 65 | sb.append(quoteIssuer); 66 | } 67 | Cpair res = new Cpair(sb.toString()); 68 | return res; 69 | } 70 | 71 | public static Cpair newInstance(Offer offer) { 72 | STObject executed = offer.executed(offer.get(STObject.FinalFields)); 73 | Amount paid = executed.get(Amount.TakerPays); 74 | Amount got = executed.get(Amount.TakerGets); 75 | Cpair res = newInstance(paid, got); 76 | return res; 77 | } 78 | 79 | public static Cpair newInstance(Amount pay, Amount get) { 80 | return newInstance(get.currencyString(), get.issuerString(), pay.currencyString(), pay.issuerString()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/Credentials.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | import org.parceler.Parcel; 4 | 5 | import com.ripple.core.coretypes.AccountID; 6 | 7 | @Parcel 8 | public class Credentials { 9 | String address; 10 | String secret; 11 | 12 | public Credentials(String address, String secret) { 13 | super(); 14 | this.address = address; 15 | this.secret = secret; 16 | } 17 | 18 | public String getAddress() { 19 | return address; 20 | } 21 | 22 | public void setAddress(String address) { 23 | this.address = address; 24 | } 25 | 26 | public String getSecret() { 27 | return secret; 28 | } 29 | 30 | public void setSecret(String secret) { 31 | this.secret = secret; 32 | } 33 | 34 | public boolean addressEquals(AccountID accountID) { 35 | return accountID.address.equals(this.address); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/Intervals.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | public class Intervals { 4 | 5 | int balancer; 6 | int accountBalance; 7 | 8 | public int getBalancer() { 9 | return balancer; 10 | } 11 | 12 | public void setBalancer(int balancer) { 13 | this.balancer = balancer; 14 | } 15 | 16 | public int getAccountBalance() { 17 | return accountBalance; 18 | } 19 | 20 | public void setAccountBalance(int accountBalance) { 21 | this.accountBalance = accountBalance; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/LastAmount.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public class LastAmount { 6 | 7 | public final BigDecimal unitPrice; 8 | public final BigDecimal qty; 9 | 10 | public LastAmount(BigDecimal unitPrice, BigDecimal qty) { 11 | super(); 12 | this.unitPrice = unitPrice; 13 | this.qty = qty; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/LastBuySellTuple.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public class LastBuySellTuple { 6 | 7 | public final LastAmount buy, sel; 8 | public final boolean isBuyPulledFromSel; 9 | public final boolean isSelPulledFromBuy; 10 | 11 | 12 | public LastBuySellTuple(BigDecimal buyUnitPrice, BigDecimal buyQty, BigDecimal selUnitPrice, BigDecimal selQty, boolean isBuyPulledFromSel, boolean isSelPulledFromBuy){ 13 | LastAmount buy = new LastAmount(buyUnitPrice, buyQty); 14 | LastAmount sel = new LastAmount(selUnitPrice, selQty); 15 | this.buy = buy; 16 | this.sel = sel; 17 | this.isBuyPulledFromSel = isBuyPulledFromSel; 18 | this.isSelPulledFromBuy = isSelPulledFromBuy; 19 | } 20 | 21 | 22 | 23 | 24 | // public Amount getBuyAmount() { 25 | // return buyAmount; 26 | // } 27 | // 28 | // public void setBuyAmount(Amount buyAmount) { 29 | // this.buyAmount = buyAmount; 30 | // } 31 | // 32 | // public Amount getSellAmount() { 33 | // return sellAmount; 34 | // } 35 | // 36 | // public void setSellAmount(Amount sellAmount) { 37 | // this.sellAmount = sellAmount; 38 | // } 39 | // 40 | // public BigDecimal getBuyRate() { 41 | // return buyRate; 42 | // } 43 | // 44 | // public void setBuyRate(BigDecimal buyRate) { 45 | // this.buyRate = buyRate; 46 | // } 47 | // 48 | // public BigDecimal getSelRate() { 49 | // return selRate; 50 | // } 51 | // 52 | // public void setSelRate(BigDecimal selRate) { 53 | // this.selRate = selRate; 54 | // } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/LedgerEvent.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | import org.json.JSONObject; 4 | 5 | public class LedgerEvent { 6 | final int closed; 7 | final int validated; 8 | 9 | private LedgerEvent(int closed, int validated) { 10 | super(); 11 | this.closed = closed; 12 | this.validated = validated; 13 | } 14 | 15 | public int getClosed() { 16 | return closed; 17 | } 18 | 19 | public int getValidated() { 20 | return validated; 21 | } 22 | 23 | public static LedgerEvent fromJSON(JSONObject root) { 24 | if (!root.has("validated_ledgers")) { 25 | return new LedgerEvent(-1, -1); 26 | } 27 | // 158188-419709,419711 28 | String a = root.getString("validated_ledgers"); 29 | String els[] = a.split(","); 30 | String raw = els[els.length - 1]; 31 | String[] rangeValids = raw.split("-"); 32 | int validated = Integer.parseInt(rangeValids[rangeValids.length - 1]); 33 | int closed = root.getInt("ledger_index"); 34 | return new LedgerEvent(closed, validated); 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return String.format("Ledger closed: %d, Ledger validated: %d", this.closed, this.validated); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/NameIssuer.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | import com.ripple.core.coretypes.Amount; 4 | import com.ripple.core.coretypes.Currency; 5 | 6 | public class NameIssuer { 7 | String currency; 8 | String issuer; 9 | 10 | private NameIssuer() { 11 | super(); 12 | } 13 | 14 | public static NameIssuer from(Amount amount) { 15 | NameIssuer res = new NameIssuer(); 16 | res.currency = amount.currencyString(); 17 | res.issuer = (!amount.isNative() || !amount.currencyString().equals("XRP")) ? res.issuer = amount.issuerString() 18 | : null; 19 | return res; 20 | } 21 | 22 | @Override 23 | public boolean equals(Object o) { 24 | if (o == null) { 25 | return false; 26 | } 27 | if (!(o instanceof NameIssuer)) { 28 | return false; 29 | } 30 | NameIssuer test = (NameIssuer) o; 31 | if (test.currency.equals(this.currency)) { 32 | if (test.issuer == null && this.issuer == null) { 33 | return true; 34 | } 35 | if (test.issuer != null && this.issuer != null && test.issuer.equals(this.issuer)) { 36 | return true; 37 | } 38 | } 39 | return false; 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | int result = 17; 45 | result = 31 * result + currency.hashCode(); 46 | result = issuer != null ? 31 * result + issuer.hashCode() : result; 47 | return result; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | StringBuilder res = new StringBuilder(currency); 53 | if (!currency.equals(Currency.XRP.toString())) { 54 | res.append("."); 55 | res.append(issuer); 56 | } 57 | return res.toString(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/PartialOrder.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import com.ripple.core.coretypes.Amount; 7 | 8 | public class PartialOrder { 9 | 10 | private final List parts = new ArrayList<>(); 11 | 12 | 13 | public void add (RLOrder part){ 14 | parts.add(part); 15 | } 16 | 17 | public Amount sum(){ 18 | return parts.stream() 19 | .map(RLOrder::getQuantity) 20 | .reduce((x, y) -> x.add(y)) 21 | .get(); 22 | } 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/RLOrder.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.math.RoundingMode; 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.Comparator; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.Map.Entry; 12 | import java.util.Set; 13 | import java.util.concurrent.ConcurrentMap; 14 | import java.util.logging.Logger; 15 | import java.util.stream.Collectors; 16 | import java.util.stream.IntStream; 17 | 18 | import com.mbcu.mmm.models.Base; 19 | import com.mbcu.mmm.utils.MyUtils; 20 | import com.ripple.core.coretypes.AccountID; 21 | import com.ripple.core.coretypes.Amount; 22 | import com.ripple.core.coretypes.Currency; 23 | import com.ripple.core.coretypes.STObject; 24 | import com.ripple.core.coretypes.hash.Hash256; 25 | import com.ripple.core.coretypes.uint.UInt32; 26 | import com.ripple.core.fields.Field; 27 | import com.ripple.core.types.known.sle.entries.Offer; 28 | import com.ripple.core.types.known.tx.Transaction; 29 | import com.ripple.core.types.known.tx.signed.SignedTransaction; 30 | import com.ripple.core.types.known.tx.txns.OfferCancel; 31 | import com.ripple.core.types.known.tx.txns.OfferCreate; 32 | 33 | import io.reactivex.annotations.Nullable; 34 | 35 | public final class RLOrder extends Base { 36 | 37 | public enum Direction { 38 | BUY("buy"), SELL("sell"); 39 | 40 | private String text; 41 | 42 | Direction(String text) { 43 | this.text = text; 44 | } 45 | 46 | public String text() { 47 | return text; 48 | } 49 | } 50 | 51 | private final String direction; 52 | private final Amount quantity; 53 | private final Amount totalPrice; 54 | private final boolean passive; 55 | private final boolean fillOrKill; 56 | 57 | private final BigDecimal rate; 58 | private final Cpair cpair; 59 | 60 | private RLOrder(Direction direction, Amount quantity, Amount totalPrice, BigDecimal rate, Cpair cpair) { 61 | this.direction = direction.text; 62 | this.quantity = amount(quantity); 63 | this.totalPrice = amount(totalPrice); 64 | this.passive = false; 65 | this.fillOrKill = false; 66 | this.rate = rate; 67 | this.cpair = cpair; 68 | } 69 | 70 | public String getDirection() { 71 | return direction; 72 | } 73 | 74 | public Amount getQuantity() { 75 | return quantity; 76 | } 77 | 78 | public Amount getTotalPrice() { 79 | return totalPrice; 80 | } 81 | 82 | public boolean isPassive() { 83 | return passive; 84 | } 85 | 86 | public boolean isFillOrKill() { 87 | return fillOrKill; 88 | } 89 | 90 | public Cpair getCpair() { 91 | return cpair; 92 | } 93 | 94 | public RLOrder reverse() { 95 | Direction newDirection = this.direction.equals(Direction.BUY.text()) ? Direction.SELL : Direction.BUY; 96 | String newPair = cpair.toString(); 97 | Amount newQuantity = totalPrice; 98 | Amount newTotalPrice = quantity; 99 | BigDecimal rate = newTotalPrice.value().divide(newQuantity.value(), MathContext.DECIMAL64); 100 | RLOrder res = new RLOrder(newDirection, newQuantity, newTotalPrice, rate, Cpair.newInstance(newPair)); 101 | return res; 102 | } 103 | 104 | @Nullable 105 | public BigDecimal getRate() { 106 | if (rate != null) { 107 | return rate; 108 | } 109 | if (quantity.value().compareTo(BigDecimal.ZERO) == 0) { 110 | return null; 111 | } 112 | return totalPrice.value().divide(quantity.value(), MathContext.DECIMAL128); 113 | } 114 | 115 | public static Amount amount(BigDecimal value, Currency currency, AccountID issuer) { 116 | if (currency.isNative()) { 117 | value = value.round(new MathContext(6, RoundingMode.HALF_DOWN)).setScale(6, RoundingMode.HALF_DOWN); 118 | return new Amount(value); 119 | } 120 | value = value.round(new MathContext(16, RoundingMode.HALF_DOWN)).setScale(16, RoundingMode.HALF_DOWN); 121 | return new Amount(value, currency, issuer); 122 | } 123 | 124 | public static Amount amount(Amount amount) { 125 | return amount(amount.value(), amount.currency(), amount.issuer()); 126 | } 127 | 128 | public static Amount amount(BigDecimal value) { 129 | return amount(value, Currency.XRP, null); 130 | } 131 | 132 | /** 133 | * Instantiate RLORder where ask rate is not needed or used for log. This 134 | * object typically goes to submit or test. 135 | * 136 | * @param direction 137 | * @param quantity 138 | * @param totalPrice 139 | * @return 140 | */ 141 | public static RLOrder rateUnneeded(Direction direction, Amount quantity, Amount totalPrice) { 142 | Cpair cpair = direction == Direction.BUY ? Cpair.newInstance(totalPrice, quantity) : Cpair.newInstance(quantity, totalPrice); 143 | return new RLOrder(direction, quantity, totalPrice, null, cpair); 144 | } 145 | 146 | public static RLOrder fromWholeConsumed(Direction direction, Amount quantity, Amount totalPrice, BigDecimal rate) { 147 | Cpair cpair = direction == Direction.BUY ? Cpair.newInstance(totalPrice, quantity) 148 | : Cpair.newInstance(quantity, totalPrice); 149 | return new RLOrder(direction, quantity, totalPrice, rate, cpair); 150 | } 151 | 152 | public static RLOrder fromOfferCreate(Transaction txn) { 153 | Amount gets = txn.get(Amount.TakerPays); 154 | Amount pays = txn.get(Amount.TakerGets); 155 | Cpair cpair = Cpair.newInstance(pays, gets); // flipped 156 | RLOrder res = new RLOrder(Direction.BUY, gets, pays, null, cpair); 157 | return res; 158 | } 159 | 160 | public static RLOrder fromOfferCreated(Offer offer) { 161 | BigDecimal ask = askFrom(offer); 162 | Amount pays = offer.takerPays(); 163 | Amount gets = offer.takerGets(); 164 | Cpair cpair = Cpair.newInstance(gets, pays); 165 | RLOrder res = new RLOrder(Direction.BUY, gets, pays, ask, cpair); 166 | return res; 167 | } 168 | 169 | public static RLOrder fromOfferExecuted(Offer offer, boolean isOCOwn) { 170 | // All OE's paid and got are negative 171 | BigDecimal ask = isOCOwn ? BigDecimal.ONE.divide(askFrom(offer), MathContext.DECIMAL64) : askFrom(offer); 172 | STObject executed = offer.executed(offer.get(STObject.FinalFields)); 173 | Amount paid = executed.get(Amount.TakerPays); 174 | Amount got = executed.get(Amount.TakerGets); 175 | Amount rlGot = isOCOwn ? amount(paid.value(), paid.currency(), paid.issuer()) 176 | : amount(got.value(), got.currency(), got.issuer()); 177 | Amount rlPaid = isOCOwn ? amount(got.value(), got.currency(), got.issuer()) 178 | : amount(paid.value(), paid.currency(), paid.issuer()); 179 | Cpair cpair = Cpair.newInstance(isOCOwn ? got : paid, isOCOwn ? paid : got); 180 | RLOrder res = new RLOrder(Direction.BUY, rlGot, rlPaid, ask, cpair); 181 | return res; 182 | } 183 | 184 | public static RLOrder toPartial(BefAf ba) { 185 | Direction d = ba.after.direction.equals(Direction.BUY.text) ? Direction.BUY : Direction.SELL; 186 | Amount q = ba.before.quantity.abs().subtract(ba.after.quantity.abs()); 187 | Amount t = ba.before.totalPrice.abs().subtract(ba.after.totalPrice.abs()); 188 | return rateUnneeded(d, q, t); 189 | } 190 | 191 | public static BefAf toBA(Amount bTakerPays, Amount bTakerGets, Amount aTakerPays, Amount aTakerGets, UInt32 seq, 192 | Hash256 txnId, RLOrder source) { 193 | Cpair bPair = Cpair.newInstance(bTakerGets, bTakerPays); 194 | BigDecimal bAsk = bTakerGets.value().divide(bTakerPays.value(), MathContext.DECIMAL64); 195 | RLOrder before = new RLOrder(Direction.BUY, bTakerPays, bTakerGets, bAsk, bPair); 196 | if (aTakerPays == null) { 197 | aTakerPays = new Amount(new BigDecimal("0"), bTakerPays.currency(), bTakerPays.issuer()); 198 | aTakerGets = new Amount(new BigDecimal("0"), bTakerGets.currency(), bTakerGets.issuer()); 199 | } 200 | RLOrder after = RLOrder.rateUnneeded(Direction.BUY, aTakerPays, aTakerGets); 201 | return new BefAf(before, after, seq, txnId, source); 202 | } 203 | 204 | public static List fromAutobridge(Map> map) { 205 | List res = new ArrayList<>(); 206 | 207 | ArrayList majorities = null; 208 | ArrayList minorities = null; 209 | 210 | for (ArrayList offers : map.values()) { 211 | if (majorities == null && minorities == null) { 212 | majorities = offers; 213 | minorities = offers; 214 | } else { 215 | if (offers.size() > majorities.size()) { 216 | majorities = offers; 217 | } else { 218 | minorities = offers; 219 | } 220 | } 221 | } 222 | 223 | BigDecimal refAsk = oeAvg(minorities); 224 | STObject oeExecutedMinor = minorities.get(0).executed(minorities.get(0).get(STObject.FinalFields)); 225 | boolean isXRPGotInMajority = majorities.get(0).getPayCurrencyPair().startsWith(Currency.XRP.toString()); 226 | 227 | Direction direction = Direction.BUY; 228 | for (Offer oe : majorities) { 229 | STObject oeExecuted = oe.executed(oe.get(STObject.FinalFields)); 230 | BigDecimal newAsk = refAsk.multiply(oe.directoryAskQuality(), MathContext.DECIMAL64); 231 | Amount oePaid = oeExecuted.get(Amount.TakerPays); 232 | Amount oeGot = oeExecuted.get(Amount.TakerGets); 233 | if (!isXRPGotInMajority) { 234 | Amount oePaidRef = oeExecutedMinor.get(Amount.TakerPays); 235 | Amount newPaid = new Amount(oePaid.value().multiply(refAsk, MathContext.DECIMAL64), oePaidRef.currency(), oePaidRef.issuer()); 236 | Cpair cpair = Cpair.newInstance(newPaid, oeGot); 237 | Amount oeGotPositive = new Amount(oeGot.value(), oeGot.currency(), oeGot.issuer()); 238 | res.add(new RLOrder(direction, oeGotPositive, newPaid, newAsk, cpair)); 239 | } else { 240 | Amount oeGotRef = oeExecutedMinor.get(Amount.TakerGets); 241 | Amount newGot = new Amount(oeGot.value().divide(refAsk, MathContext.DECIMAL64), oeGotRef.currency(), oeGotRef.issuer()); 242 | Amount oePaidPositive = new Amount(oePaid.value(), oePaid.currency(), oePaid.issuer()); 243 | Cpair cpair = Cpair.newInstance(oePaid, newGot); 244 | res.add(new RLOrder(direction, newGot, oePaidPositive, newAsk, cpair)); 245 | } 246 | } 247 | return res; 248 | } 249 | 250 | private static BigDecimal oeAvg(ArrayList offers) { 251 | BigDecimal paids = new BigDecimal(0); 252 | BigDecimal gots = new BigDecimal(0); 253 | for (Offer oe : offers) { 254 | STObject executed = oe.executed(oe.get(STObject.FinalFields)); 255 | paids = paids.add(executed.get(Amount.TakerPays).value(), MathContext.DECIMAL128); 256 | gots = gots.add(executed.get(Amount.TakerGets).value(), MathContext.DECIMAL128); 257 | } 258 | return paids.divide(gots, MathContext.DECIMAL64); 259 | } 260 | 261 | private static BigDecimal askFrom(Offer offer) { 262 | return offer.directoryAskQuality().stripTrailingZeros(); 263 | } 264 | 265 | public SignedTransaction signOfferCreate(Config config, int sequence, int maxLedger, BigDecimal fees) { 266 | OfferCreate offerCreate = new OfferCreate(); 267 | if (this.direction.equals(Direction.BUY.text())) { 268 | offerCreate.takerGets(clearXRPIssuer(totalPrice)); 269 | offerCreate.takerPays(clearXRPIssuer(quantity)); 270 | } else if (this.direction.equals(Direction.SELL.text())) { 271 | offerCreate.takerGets(clearXRPIssuer(quantity)); 272 | offerCreate.takerPays(clearXRPIssuer(totalPrice)); 273 | } else { 274 | throw new IllegalArgumentException("Direction not valid"); 275 | } 276 | offerCreate.sequence(new UInt32(String.valueOf(sequence))); 277 | offerCreate.fee(new Amount(fees)); 278 | offerCreate.lastLedgerSequence(new UInt32(String.valueOf(maxLedger))); 279 | offerCreate.account(AccountID.fromAddress(config.getCredentials().getAddress())); 280 | SignedTransaction signed = offerCreate.sign(config.getCredentials().getSecret()); 281 | return signed; 282 | } 283 | 284 | public static SignedTransaction signOfferCancel(Config config, int seq, int newSeq, int maxLedger, BigDecimal fees) { 285 | OfferCancel res = new OfferCancel(); 286 | res.put(Field.OfferSequence, new UInt32(String.valueOf(seq))); 287 | res.sequence(new UInt32(String.valueOf(seq))); 288 | res.fee(new Amount(fees)); 289 | res.lastLedgerSequence(new UInt32(String.valueOf(maxLedger))); 290 | res.account(AccountID.fromAddress(config.getCredentials().getAddress())); 291 | 292 | SignedTransaction signed = res.sign(config.getCredentials().getSecret()); 293 | return signed; 294 | } 295 | 296 | private static Amount clearXRPIssuer(Amount in) { 297 | if (in.issuer() != null && in.issuer().address.equals("rrrrrrrrrrrrrrrrrrrrrhoLvTp")) { 298 | return new Amount(in.value()); 299 | } 300 | return in; 301 | } 302 | 303 | public static List buildSeed(boolean isBuySeed, LastBuySellTuple last, int levels, BotConfig bot, Logger log) { 304 | MathContext mc = MathContext.DECIMAL64; 305 | BigDecimal startRate = isBuySeed ? last.buy.unitPrice : last.sel.unitPrice; 306 | BigDecimal rate = bot.getGridSpace(); 307 | int range = isBuySeed ? last.isBuyPulledFromSel ? 2 : 1 : last.isSelPulledFromBuy ? 2 : 1; 308 | 309 | List res = IntStream 310 | .range(range, levels + range) 311 | .mapToObj(n -> { 312 | BigDecimal f = rate.multiply(new BigDecimal(n), mc); 313 | BigDecimal newRate = isBuySeed ? startRate.subtract(f, mc) : startRate.add(f, mc); 314 | if (newRate.compareTo(BigDecimal.ZERO) <=0) { 315 | log.severe("RLOrder.buildBuySeedPct rate below zero. Check config for the pair " + bot.getPair()); 316 | } 317 | return newRate; 318 | }) 319 | .filter(o -> o.compareTo(BigDecimal.ZERO) > 0) 320 | .map(newRate -> { 321 | Amount quantity = bot.base.add(isBuySeed ? bot.getBuyOrderQuantity() : bot.getSellOrderQuantity()); 322 | BigDecimal totalAmount1 = quantity.value().multiply(newRate, mc); 323 | Amount total1 = RLOrder.amount(totalAmount1, Currency.fromString(bot.quote.currencyString()), AccountID.fromAddress(bot.quote.issuerString())); 324 | RLOrder order = RLOrder.rateUnneeded(isBuySeed ? Direction.BUY : Direction.SELL, quantity, total1); 325 | return order; 326 | }) 327 | .filter(o -> o.getQuantity().value().compareTo(BigDecimal.ZERO) > 0) 328 | .filter(o -> o.getTotalPrice().value().compareTo(BigDecimal.ZERO) > 0) 329 | .collect(Collectors.toList()); 330 | return res; 331 | } 332 | 333 | public static List buildSeedPct(boolean isBuySeed, LastBuySellTuple last, int levels, BotConfig bot, Logger log) { 334 | MathContext mc = MathContext.DECIMAL64; 335 | BigDecimal mtp = bot.getGridSpace(); 336 | BigDecimal unitPrice0 = isBuySeed ? last.buy.unitPrice : last.sel.unitPrice; 337 | BigDecimal qty0 = isBuySeed ? last.buy.qty : last.sel.qty; 338 | 339 | int range = isBuySeed ? last.isBuyPulledFromSel ? 3 : 2 : last.isSelPulledFromBuy ? 3 : 2; 340 | List res = IntStream 341 | .range(range, levels + range) 342 | .mapToObj(n -> { 343 | BigDecimal rate = Collections.nCopies(n, BigDecimal.ONE).stream().reduce((x, y) -> x.multiply(mtp, mc)).get(); 344 | BigDecimal unitPrice1 = isBuySeed ? unitPrice0.divide(rate, mc) : unitPrice0.multiply(rate, mc); 345 | BigDecimal sqrt = MyUtils.bigSqrt(rate); 346 | BigDecimal qty1 = isBuySeed ? qty0.multiply(sqrt, mc): qty0.divide(sqrt, mc); 347 | if (unitPrice1.compareTo(BigDecimal.ZERO) <= 0) { 348 | log.severe("RLOrder.buildBuySeedPct rate below zero. Check config for the pair " + bot.getPair()); 349 | } 350 | BigDecimal total1 = qty1.multiply(unitPrice1, mc); 351 | Amount qtyAmount1 = bot.base.add(qty1); 352 | Amount totalAmount1 = RLOrder.amount(total1, Currency.fromString(bot.quote.currencyString()), AccountID.fromAddress(bot.quote.issuerString())); 353 | Direction direction1 = isBuySeed ? Direction.BUY: Direction.SELL; 354 | RLOrder buy = RLOrder.rateUnneeded(direction1, qtyAmount1, totalAmount1); 355 | return buy; 356 | }) 357 | .filter(o -> o.getQuantity().value().compareTo(BigDecimal.ZERO) > 0) 358 | .filter(o -> o.getTotalPrice().value().compareTo(BigDecimal.ZERO) > 0) 359 | .collect(Collectors.toList()); 360 | return res; 361 | } 362 | 363 | public static LastBuySellTuple nextTRates(ConcurrentMap buys, ConcurrentMap sels, BigDecimal worstBuy, BigDecimal worstSel, BotConfig botConfig) { 364 | return nextRates(TRLOrder.origins(buys), TRLOrder.origins(sels), worstBuy, worstSel, botConfig); 365 | } 366 | 367 | /** 368 | * Routes last orders in orderbook or config to quantity and unit price 369 | */ 370 | public static LastBuySellTuple nextRates(ConcurrentMap buys, ConcurrentMap sels, BigDecimal lastBuy, BigDecimal lastSel, BotConfig botConfig) { 371 | MathContext mc = MathContext.DECIMAL64; 372 | BigDecimal selAm, buyAm; 373 | BigDecimal selUnPr = lastSel; 374 | BigDecimal buyUnPr = lastBuy; 375 | boolean isBuyPulledFromSel = false; 376 | boolean isSelPulledFromBuy = false; 377 | if (buys.isEmpty() && sels.isEmpty()) { 378 | selAm = botConfig.getSellOrderQuantity(); 379 | buyAm = botConfig.getBuyOrderQuantity(); 380 | } 381 | else { 382 | List> sorted = new ArrayList<>(); 383 | RLOrder last; 384 | if (buys.isEmpty()) { 385 | last = sortSels(sels, true).get(0).getValue(); 386 | buyAm = last.getTotalPrice().value(); 387 | buyUnPr = last.getQuantity().value().divide(buyAm, mc); 388 | isBuyPulledFromSel = true; 389 | } 390 | else { 391 | sorted.addAll(sortBuys(buys, false)); 392 | Collections.reverse(sorted); 393 | last = sorted.get(0).getValue(); 394 | buyAm = last.getQuantity().value(); 395 | buyUnPr = last.getTotalPrice().value().divide(buyAm, mc); 396 | } 397 | sorted.clear(); 398 | if (sels.isEmpty()) { 399 | last = sortBuys(buys, false).get(0).getValue(); 400 | selAm = last.quantity.value(); 401 | selUnPr = last.totalPrice.value().divide(selAm, mc); 402 | isSelPulledFromBuy = true; 403 | } 404 | else { 405 | sorted.addAll(sortSels(sels, false)); 406 | last = sorted.get(0).getValue(); 407 | // lastSel = BigDecimal.ONE.divide(last.getRate(), MathContext.DECIMAL64); 408 | selAm = last.getTotalPrice().value(); 409 | selUnPr = last.quantity.value().divide(selAm, mc); 410 | } 411 | } 412 | return new LastBuySellTuple(buyUnPr, buyAm, selUnPr, selAm, isBuyPulledFromSel, isSelPulledFromBuy); 413 | } 414 | 415 | public static List> sortTBuys(ConcurrentMap buys, boolean isReversed) { 416 | return sortBuys(TRLOrder.origins(buys), isReversed); 417 | } 418 | 419 | private static List> sortBuys(ConcurrentMap buys, boolean isReversed) { 420 | Set> entries = buys.entrySet(); 421 | List> res = new ArrayList>(entries); 422 | Collections.sort(res, !isReversed ? Collections.reverseOrder(obMapComparator) : obMapComparator); 423 | return res; 424 | } 425 | 426 | public static List> sortTSels(ConcurrentMap sels, boolean isReversed) { 427 | return sortSels(TRLOrder.origins(sels), isReversed); 428 | } 429 | 430 | private static List> sortSels(ConcurrentMap sels, boolean isReversed) { 431 | Set> entries = sels.entrySet(); 432 | List> res = new ArrayList>(entries); 433 | Collections.sort(res, !isReversed ? obMapComparator : Collections.reverseOrder(obMapComparator)); 434 | return res; 435 | } 436 | 437 | private static Comparator> obMapComparator = new Comparator>() { 438 | 439 | @Override 440 | public int compare(Entry e1, Entry e2) { 441 | return e1.getValue().getRate().compareTo(e2.getValue().getRate()); 442 | } 443 | }; 444 | 445 | @Override 446 | public String stringify() { 447 | StringBuffer sb = new StringBuffer(direction); 448 | sb.append("\n"); 449 | sb.append("quantity:"); 450 | sb.append(quantity.toTextFull()); 451 | sb.append("\n"); 452 | sb.append("totalPrice:"); 453 | sb.append(totalPrice.toTextFull()); 454 | sb.append("\n"); 455 | sb.append("rate:"); 456 | BigDecimal rate = getRate(); 457 | sb.append(rate != null ? rate.toPlainString() : "rate na"); 458 | sb.append("\n"); 459 | sb.append("pair:"); 460 | sb.append(getCpair()); 461 | sb.append("\n"); 462 | return sb.toString(); 463 | } 464 | 465 | } 466 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/internal/TRLOrder.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.internal; 2 | 3 | import java.util.concurrent.ConcurrentMap; 4 | import java.util.stream.Collectors; 5 | 6 | public class TRLOrder { 7 | 8 | private final RLOrder origin; 9 | private final RLOrder now; 10 | 11 | public TRLOrder(RLOrder origin, RLOrder now){ 12 | this.origin = origin; 13 | this.now = now; 14 | } 15 | 16 | public RLOrder getNow() { 17 | return now; 18 | } 19 | 20 | 21 | public RLOrder getOrigin() { 22 | return origin; 23 | } 24 | 25 | public static ConcurrentMap origins(ConcurrentMap in){ 26 | return in.entrySet().stream() 27 | .collect(Collectors.toConcurrentMap( 28 | e -> e.getKey(), 29 | e -> e.getValue().getOrigin() 30 | )); 31 | } 32 | 33 | public TRLOrder updatedWith(RLOrder now){ 34 | return new TRLOrder(origin, now); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/request/AccountInfo.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.request; 2 | 3 | import com.mbcu.mmm.models.internal.Config; 4 | 5 | public class AccountInfo extends Request { 6 | 7 | final String account; 8 | 9 | public AccountInfo(String account) { 10 | super(Command.ACCOUNT_INFO); 11 | this.account = account; 12 | } 13 | 14 | public static final AccountInfo of(Config config) { 15 | return new AccountInfo(config.getCredentials().getAddress()); 16 | } 17 | 18 | @Override 19 | public String stringify() { 20 | return super.stringify(this); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/request/AccountOffers.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.request; 2 | 3 | import com.mbcu.mmm.models.internal.Config; 4 | 5 | public class AccountOffers extends Request { 6 | 7 | String account, ledger, marker; 8 | int limit; // max 400 9 | 10 | private AccountOffers() { 11 | super(Command.ACCOUNT_OFFERS); 12 | } 13 | 14 | @Override 15 | public String stringify() { 16 | return super.stringify(this); 17 | } 18 | 19 | public static AccountOffers of(Config config) { 20 | AccountOffers res = new AccountOffers(); 21 | res.account = config.getCredentials().getAddress(); 22 | res.limit = 200; 23 | return res; 24 | } 25 | 26 | public AccountOffers withMarker(String marker) { 27 | this.marker = marker; 28 | return this; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/request/BookOffers.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.request; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import com.mbcu.mmm.models.internal.BotConfig; 7 | import com.mbcu.mmm.models.internal.NameIssuer; 8 | 9 | public class BookOffers extends Request { 10 | 11 | String taker; 12 | Integer limit; 13 | NameIssuer taker_gets; 14 | NameIssuer taker_pays; 15 | 16 | private BookOffers() { 17 | super(Command.BOOK_OFFERS); 18 | } 19 | 20 | @Override 21 | public String stringify() { 22 | return super.stringify(this); 23 | } 24 | 25 | public static List buildRequest(String taker, BotConfig botConfig) { 26 | List res = new ArrayList(); 27 | BookOffers one = new BookOffers(); 28 | one.taker_gets = NameIssuer.from(botConfig.getBase()); 29 | one.taker_pays = NameIssuer.from(botConfig.getQuote()); 30 | one.taker = taker; 31 | 32 | BookOffers two = new BookOffers(); 33 | two.taker_gets = one.taker_pays; 34 | two.taker_pays = one.taker_gets; 35 | two.taker = taker; 36 | res.add(one.stringify()); 37 | res.add(two.stringify()); 38 | return res; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/request/Request.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.request; 2 | 3 | import com.mbcu.mmm.models.Base; 4 | 5 | public abstract class Request extends Base { 6 | 7 | public enum Command { 8 | SUBSCRIBE("subscribe"), SUBMIT("submit"), ACCOUNT_INFO("account_info"), LEDGER_CLOSED("ledger_closed"), TX( 9 | "tx"), BOOK_OFFERS("book_offers"), ACCOUNT_OFFERS("account_offers"); 10 | 11 | private String text; 12 | 13 | Command(String text) { 14 | this.text = text; 15 | } 16 | 17 | public String text() { 18 | return text; 19 | } 20 | } 21 | 22 | String command; 23 | 24 | public Request(Command command) { 25 | this.command = command.text; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/request/Submit.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.request; 2 | 3 | public class Submit extends Request { 4 | 5 | String tx_blob; 6 | 7 | public String getCommand() { 8 | return command; 9 | } 10 | 11 | public void setCommand(String command) { 12 | this.command = command; 13 | } 14 | 15 | public String getTx_blob() { 16 | return tx_blob; 17 | } 18 | 19 | public void setTx_blob(String tx_blob) { 20 | this.tx_blob = tx_blob; 21 | } 22 | 23 | public Submit(Command command) { 24 | super(command); 25 | } 26 | 27 | public static Submit build(String tx_blob) { 28 | Submit submit = new Submit(Request.Command.SUBMIT); 29 | submit.setTx_blob(tx_blob); 30 | return submit; 31 | } 32 | 33 | @Override 34 | public String stringify() { 35 | return super.stringify(this); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/request/Subscribe.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.request; 2 | 3 | import java.util.ArrayList; 4 | 5 | import com.mbcu.mmm.models.internal.BotConfig; 6 | import com.mbcu.mmm.models.internal.NameIssuer; 7 | import com.ripple.core.coretypes.Amount; 8 | 9 | public class Subscribe extends Request { 10 | 11 | private Subscribe(Command command) { 12 | super(command); 13 | } 14 | 15 | public enum Stream { 16 | LEDGER("ledger"), SERVER("server"); 17 | 18 | private String text; 19 | 20 | Stream(String text) { 21 | this.text = text; 22 | } 23 | 24 | public String text() { 25 | return text; 26 | } 27 | } 28 | 29 | ArrayList accounts; 30 | ArrayList streams; 31 | Book[] books; 32 | 33 | public void addAccount(String account) { 34 | if (accounts == null) { 35 | accounts = new ArrayList<>(); 36 | } 37 | accounts.add(account); 38 | } 39 | 40 | public void addStream(Stream stream) { 41 | if (streams == null) { 42 | streams = new ArrayList<>(); 43 | } 44 | streams.add(stream.text); 45 | } 46 | 47 | public static Subscribe build(Command command) { 48 | return new Subscribe(command); 49 | } 50 | 51 | public Subscribe withAccount(String account) { 52 | this.addAccount(account); 53 | return this; 54 | } 55 | 56 | public Subscribe withStream(Stream stream) { 57 | this.addStream(stream); 58 | return this; 59 | } 60 | 61 | public Subscribe withOrderbook(BotConfig botConfig) { 62 | this.books = new Book[1]; 63 | this.books[0] = new Book(botConfig.getQuote(), botConfig.getBase()); 64 | return this; 65 | } 66 | 67 | public static class Book { 68 | NameIssuer taker_gets, taker_pays; 69 | boolean snapshot = true; 70 | boolean both = true; 71 | 72 | public Book(Amount taker_gets, Amount taker_pays) { 73 | super(); 74 | this.taker_gets = NameIssuer.from(taker_gets); 75 | this.taker_pays = NameIssuer.from(taker_pays); 76 | } 77 | } 78 | 79 | @Override 80 | public String stringify() { 81 | return super.stringify(this); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/models/request/TeeEx.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.models.request; 2 | 3 | public class TeeEx extends Request { 4 | 5 | private final String transaction; 6 | 7 | public TeeEx(String transaction) { 8 | super(Command.TX); 9 | this.transaction = transaction; 10 | } 11 | 12 | @Override 13 | public String stringify() { 14 | return super.stringify(this); 15 | } 16 | 17 | public static TeeEx newInstance(String hash) { 18 | return new TeeEx(hash); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/notice/Content.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.notice; 2 | 3 | public class Content { 4 | 5 | public static String body(String account, String pair, String error) { 6 | StringBuilder sb = new StringBuilder(account); 7 | sb.append("\n"); 8 | sb.append(pair); 9 | sb.append("\n"); 10 | sb.append(error); 11 | return sb.toString(); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/notice/SenderSES.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.notice; 2 | 3 | import java.util.logging.Level; 4 | import java.util.logging.Logger; 5 | 6 | import com.amazonaws.regions.Regions; 7 | import com.amazonaws.services.simpleemail.AmazonSimpleEmailService; 8 | import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClientBuilder; 9 | import com.amazonaws.services.simpleemail.model.Body; 10 | import com.amazonaws.services.simpleemail.model.Content; 11 | import com.amazonaws.services.simpleemail.model.Destination; 12 | import com.amazonaws.services.simpleemail.model.Message; 13 | import com.amazonaws.services.simpleemail.model.SendEmailRequest; 14 | import com.mbcu.mmm.models.internal.Config; 15 | import com.mbcu.mmm.sequences.Emailer; 16 | 17 | public class SenderSES { 18 | 19 | private static Regions region = Regions.US_EAST_1; 20 | private Config config; 21 | private Logger logger; 22 | 23 | public SenderSES(Config config, Logger logger) { 24 | this.config = config; 25 | this.logger = logger; 26 | } 27 | 28 | public void sendAccBalance(String body) { 29 | String title = titleAccBalance(config); 30 | config.getEmails().forEach(to -> { 31 | send(logger, title, body, to); 32 | }); 33 | 34 | } 35 | 36 | public void sendBotError(Emailer.SendEmailBotError e) { 37 | String body = bodyBotError(e); 38 | String title = titleBotError(config); 39 | config.getEmails().forEach(to -> { 40 | send(logger, title, body, to); 41 | }); 42 | } 43 | 44 | public void sendWSError(Exception e) { 45 | String title = "Websocket error"; 46 | String body = e.getMessage(); 47 | config.getEmails().forEach(to -> { 48 | send(logger, title, body, to); 49 | }); 50 | 51 | } 52 | 53 | private static final void send(Logger logger, String title, String body, String to) { 54 | try { 55 | AmazonSimpleEmailService client = AmazonSimpleEmailServiceClientBuilder.standard().withRegion(region).build(); 56 | SendEmailRequest request = new SendEmailRequest().withDestination(new Destination().withToAddresses(to)) 57 | .withMessage(new Message().withBody(new Body().withText(new Content().withCharset("UTF-8").withData(body))) 58 | .withSubject(new Content().withCharset("UTF-8").withData(title))) 59 | .withSource(to); 60 | client.sendEmail(request); 61 | if (logger != null) { 62 | logger.log(Level.FINER, "Email sent!"); 63 | } 64 | } catch (Exception ex) { 65 | logger.log(Level.SEVERE, "The email was not sent. Error message: " + ex.getMessage()); 66 | } 67 | } 68 | 69 | private static String bodyBotError(Emailer.SendEmailBotError e) { 70 | StringBuilder sb = new StringBuilder("Currency Pair : "); 71 | sb.append(e.pair); 72 | sb.append("\n"); 73 | sb.append("Error : "); 74 | sb.append(e.error); 75 | return sb.toString(); 76 | } 77 | 78 | private static String titleBotError(Config config) { 79 | StringBuilder sb = new StringBuilder("Bot Error, Account "); 80 | sb.append(config.getCredentials().getAddress()); 81 | return sb.toString(); 82 | } 83 | 84 | private static String titleAccBalance(Config config) { 85 | StringBuilder sb = new StringBuilder("Account Balance of "); 86 | sb.append(config.getCredentials().getAddress()); 87 | return sb.toString(); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/rx/BusBase.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.rx; 2 | 3 | import com.mbcu.mmm.utils.GsonUtils; 4 | 5 | public class BusBase { 6 | 7 | @Override 8 | public String toString() { 9 | StringBuilder sb = new StringBuilder(this.getClass().getName()); 10 | sb.append("\n"); 11 | sb.append(GsonUtils.toJson(this)); 12 | return sb.toString(); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/rx/RxBus.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.rx; 2 | 3 | import com.jakewharton.rxrelay2.PublishRelay; 4 | import com.jakewharton.rxrelay2.Relay; 5 | 6 | import io.reactivex.Observable; 7 | 8 | public final class RxBus { 9 | private final Relay bus = PublishRelay.create().toSerialized(); 10 | 11 | public void send(Object event) { 12 | bus.accept(event); 13 | } 14 | 15 | public Observable toObservable() { 16 | return bus; 17 | } 18 | 19 | public boolean hasObservers() { 20 | return bus.hasObservers(); 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/rx/RxBusProvider.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.rx; 2 | 3 | public class RxBusProvider { 4 | private static RxBus BUS = null; 5 | 6 | private RxBusProvider() { 7 | // No instances. 8 | } 9 | 10 | public static RxBus getInstance() { 11 | if (BUS == null) { 12 | BUS = new RxBus(); 13 | } 14 | return BUS; 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/Balancer.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | import com.mbcu.mmm.main.WebSocketClient; 8 | import com.mbcu.mmm.models.internal.Config; 9 | import com.mbcu.mmm.models.request.AccountOffers; 10 | import com.mbcu.mmm.rx.BusBase; 11 | import com.mbcu.mmm.sequences.Common.OnAccountOffers; 12 | import com.mbcu.mmm.sequences.Orderbook.OnAccOffersDone; 13 | import com.mbcu.mmm.utils.MyLogger; 14 | 15 | import io.reactivex.Observer; 16 | import io.reactivex.disposables.CompositeDisposable; 17 | import io.reactivex.disposables.Disposable; 18 | import io.reactivex.observers.DisposableObserver; 19 | import io.reactivex.schedulers.Schedulers; 20 | 21 | public class Balancer extends Base { 22 | 23 | private final List perm = new ArrayList<>(); 24 | private AtomicInteger existingOrderSize = new AtomicInteger(0); 25 | 26 | public Balancer(Config config) { 27 | super(MyLogger.getLogger(Starter.class.getName()), config); 28 | buildOrderbooks(); 29 | CompositeDisposable accOfferDis = new CompositeDisposable(); 30 | accOfferDis 31 | .add(bus.toObservable().subscribeOn(Schedulers.newThread()).subscribeWith(new DisposableObserver() { 32 | 33 | @Override 34 | public void onNext(Object o) { 35 | BusBase base = (BusBase) o; 36 | if (base instanceof Common.OnAccountOffers) { 37 | OnAccountOffers event = (OnAccountOffers) base; 38 | existingOrderSize.addAndGet(event.accOffs.size()); 39 | if (event.marker != null) { 40 | bus.send(new WebSocketClient.WSRequestSendText( 41 | AccountOffers.of(config).withMarker(event.marker).stringify())); 42 | } else { 43 | bus.send(new OnAccOffersDone()); 44 | log("Account offers : " + existingOrderSize.get()); 45 | accOfferDis.clear(); 46 | } 47 | } 48 | 49 | } 50 | 51 | @Override 52 | public void onError(Throwable e) { 53 | // TODO Auto-generated method stub 54 | 55 | } 56 | 57 | @Override 58 | public void onComplete() { 59 | } 60 | })); 61 | 62 | bus.toObservable().subscribe(new Observer() { 63 | 64 | @Override 65 | public void onSubscribe(Disposable d) { 66 | // TODO Auto-generated method stub 67 | } 68 | 69 | @Override 70 | public void onNext(Object o) { 71 | 72 | } 73 | 74 | @Override 75 | public void onError(Throwable e) { 76 | // TODO Auto-generated method stub 77 | } 78 | 79 | @Override 80 | public void onComplete() { 81 | // TODO Auto-generated method stub 82 | } 83 | }); 84 | } 85 | 86 | private void buildOrderbooks() { 87 | super.config.getBotConfigMap().values().stream().forEach(botConfig -> { 88 | perm.add(Orderbook.newInstance(botConfig, config)); 89 | }); 90 | } 91 | 92 | public static Balancer newInstance(Config config) { 93 | return new Balancer(config); 94 | } 95 | 96 | public static class OnRequestNonOrderbookRLOrder extends BusBase { 97 | public String pair; 98 | 99 | public OnRequestNonOrderbookRLOrder(String pair) { 100 | super(); 101 | this.pair = pair; 102 | } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/Base.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences; 2 | 3 | import java.util.logging.Level; 4 | import java.util.logging.Logger; 5 | 6 | import com.mbcu.mmm.models.internal.Config; 7 | import com.mbcu.mmm.rx.RxBus; 8 | import com.mbcu.mmm.rx.RxBusProvider; 9 | 10 | public class Base { 11 | protected final RxBus bus = RxBusProvider.getInstance(); 12 | protected Config config; 13 | protected Logger LOGGER; 14 | 15 | public Base(Logger logger, Config config) { 16 | this.LOGGER = logger; 17 | this.config = config; 18 | } 19 | 20 | protected void log(String message, Level... level) { 21 | Level l = level.length > 0 ? level[0] : Level.FINE; 22 | this.LOGGER.log(l, message); 23 | System.out.println(message); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/Dataapi.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.time.ZoneOffset; 6 | import java.time.ZonedDateTime; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.concurrent.Callable; 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.ScheduledExecutorService; 12 | import java.util.concurrent.ScheduledFuture; 13 | import java.util.concurrent.TimeUnit; 14 | import java.util.concurrent.atomic.AtomicInteger; 15 | import java.util.concurrent.atomic.AtomicLong; 16 | import java.util.logging.Level; 17 | 18 | import com.mbcu.mmm.api.ApiManager; 19 | import com.mbcu.mmm.models.dataapi.AccountBalance; 20 | import com.mbcu.mmm.models.dataapi.Balance; 21 | import com.mbcu.mmm.models.internal.AgBalance; 22 | import com.mbcu.mmm.models.internal.Config; 23 | import com.mbcu.mmm.models.internal.NameIssuer; 24 | import com.mbcu.mmm.notice.SenderSES; 25 | import com.mbcu.mmm.rx.BusBase; 26 | import com.mbcu.mmm.sequences.Common.OnLedgerClosed; 27 | import com.mbcu.mmm.utils.MyLogger; 28 | 29 | import io.reactivex.disposables.CompositeDisposable; 30 | import io.reactivex.observers.DisposableObserver; 31 | import io.reactivex.schedulers.Schedulers; 32 | import retrofit2.Call; 33 | import retrofit2.Callback; 34 | import retrofit2.Response; 35 | 36 | public class Dataapi extends Base { 37 | private CompositeDisposable disposable = new CompositeDisposable(); 38 | private ApiManager apiManager; 39 | private AgBalance lastAgBalance = new AgBalance(); 40 | private AtomicInteger ledgerValidated = new AtomicInteger(-1); 41 | private ScheduledExecutorService accBalanceMailService = Executors.newSingleThreadScheduledExecutor(); 42 | private StringBuilder cachedAccBalanceMail = new StringBuilder(); 43 | private AtomicLong startTs; 44 | private SenderSES senderSES; 45 | 46 | public Dataapi(Config config) { 47 | super(MyLogger.getLogger(Dataapi.class.getName()), config); 48 | senderSES = new SenderSES(config, LOGGER); 49 | apiManager = new ApiManager(config); 50 | startTs = new AtomicLong(System.currentTimeMillis()); 51 | disposable 52 | .add(bus.toObservable().subscribeOn(Schedulers.newThread()).subscribeWith(new DisposableObserver() { 53 | 54 | @Override 55 | public void onNext(Object o) { 56 | BusBase base = (BusBase) o; 57 | try { 58 | if (base instanceof Common.OnLedgerClosed) { 59 | OnLedgerClosed event = (OnLedgerClosed) base; 60 | if (ledgerValidated.get() == -1 || event.ledgerEvent.getValidated() - ledgerValidated.get() >= config 61 | .getIntervals().getAccountBalance()) { 62 | ledgerValidated.set(event.ledgerEvent.getValidated()); 63 | callBalances(); 64 | } 65 | } 66 | } catch (Exception e) { 67 | MyLogger.exception(LOGGER, base.toString(), e); 68 | throw e; 69 | } 70 | } 71 | 72 | @Override 73 | public void onError(Throwable e) { 74 | log(e.getMessage(), Level.SEVERE); 75 | } 76 | 77 | @Override 78 | public void onComplete() { 79 | } 80 | 81 | }) 82 | 83 | ); 84 | 85 | accBalanceMailService.schedule(new Runnable() { 86 | 87 | @Override 88 | public void run() { 89 | if (cachedAccBalanceMail.length() > 0) { 90 | senderSES.sendAccBalance(cachedAccBalanceMail.toString()); 91 | cachedAccBalanceMail.setLength(0); 92 | } 93 | } 94 | }, Config.HOUR_ACCOUNT_BALANCER_EMAILER, TimeUnit.HOURS); 95 | 96 | } 97 | 98 | private void callBalances() { 99 | ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); 100 | final long nowTs = now.toEpochSecond(); 101 | Call callBalances = apiManager.getService().getBalances(config.getCredentials().getAddress(), 102 | now.toString()); 103 | callBalances.enqueue(new Callback() { 104 | 105 | @Override 106 | public void onResponse(Call arg0, Response response) { 107 | AccountBalance ab = response.body(); 108 | if (ab == null) { 109 | return; 110 | } 111 | AgBalance newAgBalance = AgBalance.from(ab, nowTs); 112 | StringBuilder agBalanceString = buildAccBalance(newAgBalance); 113 | log(agBalanceString.toString(), Level.FINE); 114 | cachedAccBalanceMail.append(agBalanceString); 115 | lastAgBalance = newAgBalance; 116 | } 117 | 118 | @Override 119 | public void onFailure(Call arg0, Throwable arg1) { 120 | // TODO Auto-generated method stub 121 | 122 | } 123 | }); 124 | 125 | } 126 | 127 | private Map deltaBalance(AgBalance newAg) { 128 | final Map res = new HashMap<>(); 129 | Map lastData = lastAgBalance.getData(); 130 | 131 | newAg.getData().entrySet().stream().forEach(e -> { 132 | BigDecimal newValue = new BigDecimal(e.getValue().getValue()); 133 | Balance lastBalance = lastData.get(e.getKey()); 134 | if (lastBalance != null) { 135 | BigDecimal oldValue = new BigDecimal(lastBalance.getValue()); 136 | BigDecimal d = newValue.subtract(oldValue, MathContext.DECIMAL32); 137 | res.put(e.getKey(), d); 138 | } 139 | }); 140 | return res; 141 | } 142 | 143 | private StringBuilder buildAccBalance(AgBalance newAg) { 144 | Map deltas = deltaBalance(newAg); 145 | StringBuilder t = new StringBuilder("\n"); 146 | t.append("Balance "); 147 | t.append(newAg.getDt().toString()); 148 | t.append("\n"); 149 | t.append("pair | now | change"); 150 | t.append("\n"); 151 | newAg.getData().entrySet().stream().forEach(e -> { 152 | t.append(e.getKey()); 153 | t.append(" "); 154 | t.append(e.getValue().getValue()); 155 | t.append(" "); 156 | BigDecimal delta = deltas.get(e.getKey()); 157 | t.append(delta != null ? delta.stripTrailingZeros() : "-"); 158 | t.append("\n"); 159 | 160 | }); 161 | t.append("\n"); 162 | return t; 163 | } 164 | 165 | public static Dataapi newInstance(Config config) { 166 | Dataapi res = new Dataapi(config); 167 | return res; 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/Emailer.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | 5 | import com.mbcu.mmm.main.WebSocketClient; 6 | import com.mbcu.mmm.main.WebSocketClient.WSError; 7 | import com.mbcu.mmm.models.internal.Config; 8 | import com.mbcu.mmm.notice.SenderSES; 9 | import com.mbcu.mmm.rx.BusBase; 10 | import com.mbcu.mmm.utils.MyLogger; 11 | 12 | import io.reactivex.disposables.CompositeDisposable; 13 | import io.reactivex.observers.DisposableObserver; 14 | import io.reactivex.schedulers.Schedulers; 15 | 16 | public class Emailer extends Base { 17 | private final CompositeDisposable disposables = new CompositeDisposable(); 18 | private final ConcurrentHashMap notices = new ConcurrentHashMap<>(); 19 | private final long GAP_MS = 1000 * 60 * 30; // 30 mins 20 | private SenderSES sender; 21 | 22 | public Emailer(Config config) { 23 | super(MyLogger.getLogger(Emailer.class.getName()), config); 24 | sender = new SenderSES(config, LOGGER); 25 | 26 | disposables 27 | .add(bus.toObservable().subscribeOn(Schedulers.newThread()).subscribeWith(new DisposableObserver() { 28 | 29 | @Override 30 | public void onNext(Object o) { 31 | BusBase base = (BusBase) o; 32 | 33 | if (base instanceof SendEmailBotError) { 34 | check((SendEmailBotError) base); 35 | } 36 | else if (base instanceof SendEmailWSError) { 37 | SendEmailWSError event = (SendEmailWSError) base; 38 | sender.sendWSError(event.e.e); 39 | bus.send(new WebSocketClient.WSRequestShutdown(true)); 40 | } 41 | } 42 | 43 | @Override 44 | public void onError(Throwable e) { 45 | // TODO Auto-generated method stub 46 | 47 | } 48 | 49 | @Override 50 | public void onComplete() { 51 | // TODO Auto-generated method stub 52 | 53 | } 54 | 55 | })); 56 | } 57 | 58 | private void check(SendEmailBotError r) { 59 | long now = System.currentTimeMillis(); 60 | Long lastInserted = notices.get(r); 61 | if (lastInserted != null && lastInserted + GAP_MS > now) { 62 | return; 63 | } 64 | notices.put(r, now); 65 | sender.sendBotError(r); 66 | } 67 | 68 | public static Emailer newInstance(Config config) { 69 | Emailer res = new Emailer(config); 70 | return res; 71 | } 72 | 73 | public static class SendEmailBotError extends BusBase { 74 | public final String error; 75 | public final String pair; 76 | public final long millis; 77 | public final boolean shutDown; 78 | 79 | public SendEmailBotError(String error, String pair, long millis, boolean shutDown) { 80 | this.error = error; 81 | this.pair = pair; 82 | this.millis = millis; 83 | this.shutDown = shutDown; 84 | } 85 | 86 | @Override 87 | public int hashCode() { 88 | return (pair.hashCode() + error.hashCode()) / 17; 89 | } 90 | 91 | @Override 92 | public boolean equals(Object o) { 93 | if (o == null) 94 | return false; 95 | if (!(o instanceof SendEmailBotError)) 96 | return false; 97 | SendEmailBotError t = (SendEmailBotError) o; 98 | if (t.pair.equals(pair) && t.error.equals(error)) { 99 | return true; 100 | } 101 | return false; 102 | } 103 | } 104 | 105 | public static class SendEmailWSError extends BusBase{ 106 | public final WSError e; 107 | 108 | public SendEmailWSError(WSError e) { 109 | super(); 110 | this.e = e; 111 | } 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/LogNum.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences; 2 | 3 | public enum LogNum { 4 | 5 | CANCELED_UNFUNDED ("Offer canceled, no longer funded"), 6 | CANCELED_OFFERCANCEL("Canceled by OfferCancel"); 7 | 8 | private final String text; 9 | 10 | private LogNum(final String text) { 11 | this.text = text; 12 | } 13 | 14 | @Override 15 | public String toString() { 16 | return text; 17 | } 18 | 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/Orderbook.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Map.Entry; 9 | import java.util.Optional; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | import java.util.function.Function; 13 | import java.util.logging.Level; 14 | import java.util.stream.Stream; 15 | 16 | import com.mbcu.mmm.helpers.TAccountOffer; 17 | import com.mbcu.mmm.models.internal.BefAf; 18 | import com.mbcu.mmm.models.internal.BotConfig; 19 | import com.mbcu.mmm.models.internal.BotConfig.Strategy; 20 | import com.mbcu.mmm.models.internal.Config; 21 | import com.mbcu.mmm.models.internal.LastBuySellTuple; 22 | import com.mbcu.mmm.models.internal.RLOrder; 23 | import com.mbcu.mmm.models.internal.RLOrder.Direction; 24 | import com.mbcu.mmm.models.internal.TRLOrder; 25 | import com.mbcu.mmm.rx.BusBase; 26 | import com.mbcu.mmm.rx.RxBus; 27 | import com.mbcu.mmm.rx.RxBusProvider; 28 | import com.mbcu.mmm.sequences.state.State; 29 | import com.mbcu.mmm.sequences.state.State.BroadcastPendings; 30 | import com.mbcu.mmm.sequences.state.State.OnOrderReady; 31 | import com.mbcu.mmm.utils.MyLogger; 32 | 33 | import io.reactivex.disposables.CompositeDisposable; 34 | import io.reactivex.observers.DisposableObserver; 35 | import io.reactivex.schedulers.Schedulers; 36 | 37 | public class Orderbook extends Base { 38 | 39 | private final BotConfig botConfig; 40 | private final ConcurrentHashMap buys = new ConcurrentHashMap<>(); 41 | private final ConcurrentHashMap sels = new ConcurrentHashMap<>(); 42 | 43 | private final RxBus bus = RxBusProvider.getInstance(); 44 | private final CompositeDisposable disposables = new CompositeDisposable(); 45 | private final CompositeDisposable accountOffersDispo = new CompositeDisposable(); 46 | private final AtomicInteger lastBalanced = new AtomicInteger(0); 47 | private final AtomicInteger ledgerValidated = new AtomicInteger(0); 48 | private final AtomicInteger ledgerClosed = new AtomicInteger(0); 49 | private LastBuySellTuple start; 50 | 51 | private Orderbook(Config config, BotConfig botConfig) { 52 | super(MyLogger.getLogger(String.format(Txc.class.getName())), config); 53 | this.botConfig = botConfig; 54 | this.start = new LastBuySellTuple(botConfig.getStartMiddlePrice(), botConfig.getBuyOrderQuantity(), botConfig.getStartMiddlePrice(), botConfig.getSellOrderQuantity(), false, false); 55 | 56 | accountOffersDispo 57 | .add(bus.toObservable().subscribeOn(Schedulers.newThread()).subscribeWith(new DisposableObserver() { 58 | 59 | @Override 60 | public void onNext(Object o) { 61 | BusBase base = (BusBase) o; 62 | if (base instanceof Common.OnAccountOffers) { 63 | Common.OnAccountOffers event = (Common.OnAccountOffers) o; 64 | boolean buyMatched = false; 65 | boolean selMatched = false; 66 | Optional pairMatched = null; 67 | 68 | for (TAccountOffer t : event.accOffs) { 69 | pairMatched = pairMatched(t.getOrder()); 70 | if (pairMatched.isPresent()) { 71 | if (pairMatched.get()) { 72 | buyMatched = true; 73 | } else { 74 | selMatched = true; 75 | } 76 | insert(t.getOrder(), t.getOrder(), t.getSeq(), pairMatched.get()); 77 | } 78 | } 79 | if (buyMatched || selMatched) { 80 | LastBuySellTuple worstRates = RLOrder.nextTRates(buys, sels, start.buy.unitPrice, start.sel.unitPrice, botConfig); 81 | start = worstRates; 82 | } 83 | } 84 | } 85 | 86 | @Override 87 | public void onError(Throwable e) { 88 | log(e.getMessage(), Level.SEVERE); 89 | } 90 | 91 | @Override 92 | public void onComplete() { 93 | } 94 | })); 95 | 96 | disposables.add(bus.toObservable().subscribeOn(Schedulers.newThread()).subscribeWith(new DisposableObserver() { 97 | 98 | @Override 99 | public void onNext(Object o) { 100 | BusBase base = (BusBase) o; 101 | try { 102 | if (base instanceof Common.OnLedgerClosed) { 103 | lastBalanced.incrementAndGet(); 104 | Common.OnLedgerClosed event = (Common.OnLedgerClosed) o; 105 | ledgerClosed.set(event.ledgerEvent.getClosed()); 106 | ledgerValidated.set(event.ledgerEvent.getValidated()); 107 | requestPendings(); 108 | } 109 | else if (base instanceof Common.OnDifference) { 110 | Common.OnDifference event = (Common.OnDifference) o; 111 | boolean isBelongToThisOrderbook = false; 112 | 113 | List preCounters = new ArrayList<>(); 114 | for (BefAf ba : event.bas) { 115 | Optional pairMatched = pairMatched(ba.after); 116 | if (pairMatched.isPresent()) { 117 | isBelongToThisOrderbook = true; 118 | if (ba.source != null){ 119 | if (ba.after.getQuantity().value().compareTo(BigDecimal.ZERO) == 0){ 120 | preCounters.add(ba.source); 121 | } 122 | else { 123 | insert(ba.before, ba.after, ba.befSeq.intValue(), pairMatched.get()); 124 | } 125 | } 126 | else if (ba.source == null){ 127 | if(ba.after.getQuantity().value().compareTo(BigDecimal.ZERO) == 0) { // fully consumed 128 | TRLOrder entry = pairMatched.get() ? buys.get(ba.befSeq.intValue()) : sels.get(ba.befSeq.intValue()); 129 | if (entry != null){ 130 | preCounters.add(botConfig.getStrategy() == Strategy.PARTIAL ? RLOrder.toPartial(ba) : entry.getOrigin()); 131 | remove(ba.befSeq.intValue()); 132 | } 133 | else{ 134 | log("Orderbook Trying to remove already gone :" + ba.befSeq.intValue(), Level.SEVERE); 135 | } 136 | } 137 | else { 138 | if (botConfig.getStrategy() == Strategy.PARTIAL) { 139 | preCounters.add(RLOrder.toPartial(ba)); 140 | } 141 | update(ba.after, ba.befSeq.intValue(), ba.befSeq.intValue(), pairMatched.get()); // partially consumed 142 | } 143 | } 144 | } 145 | } 146 | 147 | if (isBelongToThisOrderbook) { 148 | LastBuySellTuple worstRates = RLOrder.nextTRates(buys, sels, start.buy.unitPrice, start.sel.unitPrice, botConfig); 149 | start = worstRates; 150 | } 151 | 152 | if (!preCounters.isEmpty()){ 153 | bus.send(new OnOrderConsumed(preCounters)); 154 | } 155 | } 156 | else if (base instanceof Common.OnOfferEdited) { 157 | Common.OnOfferEdited event = (Common.OnOfferEdited) o; 158 | Optional pairMatched = pairMatched(event.ba.after); 159 | if (pairMatched.isPresent()) { 160 | update(event.ba.after, event.ba.befSeq.intValue(), event.newSeq.intValue(), pairMatched.get()); 161 | LastBuySellTuple worstRates = RLOrder.nextTRates(buys, sels, start.buy.unitPrice, start.sel.unitPrice, botConfig); 162 | start = worstRates; 163 | } 164 | } 165 | else if (base instanceof Common.OnOfferCanceled) { 166 | Common.OnOfferCanceled event = (Common.OnOfferCanceled) o; 167 | Boolean pairMatched = remove(event.prevSeq.intValue()); 168 | if (pairMatched != null) { 169 | if (event.lognum == LogNum.CANCELED_UNFUNDED) { 170 | bus.send(new Emailer.SendEmailBotError("Order " + event.previousTxnId + " " + event.prevSeq + " canceled, unfunded", botConfig.getPair(), System.currentTimeMillis(), false)); 171 | } 172 | LastBuySellTuple worstRates = RLOrder.nextTRates(buys, sels, start.buy.unitPrice, start.sel.unitPrice, botConfig); 173 | start = worstRates; 174 | } 175 | } else if (base instanceof State.BroadcastPendings) { 176 | BroadcastPendings event = (BroadcastPendings) o; 177 | if (event.pair.equals(botConfig.getPair())) { 178 | if (event.creates.isEmpty() && event.cancels.isEmpty() && lastBalanced.get() >= config.getIntervals().getBalancer()) { 179 | lastBalanced.set(0); 180 | List> sortedSels, sortedBuys; 181 | sortedSels = RLOrder.sortTSels(sels, false); 182 | sortedBuys = RLOrder.sortTBuys(buys, false); 183 | warnEmptyOrderbook(sortedSels, sortedBuys); 184 | printOrderbook(sortedSels, sortedBuys); 185 | balancer(sortedSels, sortedBuys); 186 | } 187 | } 188 | } 189 | } catch (Exception e) { 190 | MyLogger.exception(LOGGER, base.toString(), e); 191 | throw e; 192 | } 193 | } 194 | 195 | @Override 196 | public void onError(Throwable e) { 197 | log(e.getMessage(), Level.SEVERE); 198 | } 199 | 200 | @Override 201 | public void onComplete() { 202 | // TODO Auto-generated method stub 203 | } 204 | 205 | })); 206 | } 207 | 208 | private void printOrderbook(List> sortedSels, List> sortedBuys) { 209 | String ob = dump(ledgerClosed.get(), ledgerValidated.get(), sortedSels, sortedBuys); 210 | // MyUtils.toFile(ob, path); 211 | log(ob, Level.FINER); 212 | } 213 | 214 | private void warnEmptyOrderbook(List> sortedSels, List> sortedBuys){ 215 | String warning = sortedSels.isEmpty() ? "Warning. Orderbook empty : sell " : null; 216 | warning = sortedBuys.isEmpty() ? "Warning. Orderbook empty : buy" : null; 217 | warning = sortedBuys.isEmpty() && sortedSels.isEmpty() ? "Warning. Orderbook empty : both" : null; 218 | if (warning != null){ 219 | log(warning + botConfig.getPair(), Level.WARNING); 220 | // bus.send(new Notifier.RequestEmailNotice(warning, botConfig.getPair(), System.currentTimeMillis())); 221 | } 222 | } 223 | 224 | private void requestPendings() { 225 | bus.send(new Balancer.OnRequestNonOrderbookRLOrder(botConfig.getPair())); 226 | } 227 | 228 | private void balancer(List> sortedSels, List> sortedBuys) { 229 | // using pendings is unrealiable. 230 | BigDecimal sumBuys = sum(Direction.BUY); 231 | BigDecimal sumSels = sum(Direction.SELL); 232 | log(printLog(sumBuys, sumSels, count(Direction.BUY), count(Direction.SELL))); 233 | List gens = new ArrayList<>(); 234 | 235 | BigDecimal buysGap = margin(sumBuys, Direction.BUY); 236 | if (buysGap.compareTo(BigDecimal.ZERO) > 0) { 237 | gens.addAll(generate(sortedBuys, buysGap, Direction.BUY)); 238 | } 239 | 240 | BigDecimal selsGap = margin(sumSels, Direction.SELL); 241 | if (selsGap.compareTo(BigDecimal.ZERO) > 0) { 242 | gens.addAll(generate(sortedSels, selsGap, Direction.SELL)); 243 | } 244 | gens.forEach(rlo -> bus.send(new State.OnOrderReady(rlo, OnOrderReady.Source.BALANCER))); 245 | } 246 | 247 | private List generate(List> sorteds, BigDecimal margin, Direction direction) { 248 | List res = new ArrayList<>(); 249 | margin = margin.abs(); 250 | int levels = margin 251 | .divide(direction == Direction.BUY ? botConfig.getBuyOrderQuantity() : botConfig.getSellOrderQuantity(), MathContext.DECIMAL32) 252 | .intValue(); 253 | if (direction == Direction.SELL) { 254 | Collections.reverse(sorteds); 255 | } 256 | 257 | if (levels > 0) { 258 | if (botConfig.getStrategy() == Strategy.PPT ) { 259 | res.addAll(RLOrder.buildSeedPct(direction == Direction.BUY, start, levels, botConfig, LOGGER)); 260 | } 261 | else { 262 | res.addAll(RLOrder.buildSeed(direction == Direction.BUY, start, levels, botConfig, LOGGER)); 263 | } 264 | } 265 | return res; 266 | } 267 | 268 | /** 269 | * 270 | * @param sum 271 | * @param direction 272 | * @return negative if orderbook is bigger than config, positive if smaller 273 | */ 274 | private BigDecimal margin(BigDecimal sum, Direction direction) { 275 | BigDecimal configTotalQuantity = direction == Direction.BUY ? botConfig.getTotalBuyQty() : botConfig.getTotalSelQty(); 276 | return configTotalQuantity.subtract(sum); 277 | } 278 | 279 | private String printLog(BigDecimal sumBuys, BigDecimal sumSels, int countBuys, int countSels) { 280 | StringBuffer res = new StringBuffer("\nOrderbook "); 281 | res.append(botConfig.getPair()); 282 | res.append("\nBUYS : "); 283 | res.append("n: "); 284 | res.append(countBuys); 285 | res.append(" totalQty "); 286 | res.append(sumBuys.toPlainString()); 287 | res.append("\nSELLS : "); 288 | res.append("n: "); 289 | res.append(countSels); 290 | res.append(" totalQty "); 291 | res.append(sumSels.toPlainString()); 292 | res.append("\n"); 293 | return res.toString(); 294 | } 295 | 296 | private int count(Direction direction) { 297 | int res = direction == Direction.BUY ? buys.size() : sels.size(); 298 | return res; 299 | } 300 | 301 | private BigDecimal sum(Direction direction) { 302 | Stream orderbook; 303 | Function fun; 304 | if (direction == Direction.BUY) { 305 | orderbook = buys.values().stream(); 306 | fun = tRlOrder -> tRlOrder.getNow().getQuantity().value(); 307 | } else { 308 | orderbook = sels.values().stream(); 309 | fun = rlOrder -> rlOrder.getNow().getTotalPrice().value(); 310 | } 311 | return orderbook.map(fun).reduce(BigDecimal.ZERO, BigDecimal::add); 312 | } 313 | 314 | private void update(RLOrder now, int oldSeq, int newSeq, Boolean isAligned){ 315 | ConcurrentHashMap orders = isAligned ? buys : sels; 316 | if (orders.containsKey(oldSeq)){ 317 | TRLOrder oldOrder = orders.get(oldSeq); 318 | remove(oldSeq); // remove comes first then put ! 319 | orders.put(newSeq, oldOrder.updatedWith(now)); 320 | } 321 | else { 322 | log("Orderbook " + botConfig.getPair() + " update failed cannot find , seq " + oldSeq , Level.WARNING); 323 | } 324 | } 325 | 326 | private void insert(RLOrder source, RLOrder now, int seq, Boolean isAligned) { 327 | ConcurrentHashMap orders = isAligned ? buys : sels; 328 | if (orders.contains(seq)){ 329 | orders.put(seq, orders.get(seq).updatedWith(now)); 330 | } 331 | else{ 332 | orders.put(seq, new TRLOrder(source, now)); 333 | } 334 | } 335 | 336 | // Because it's map, it's O(1) and idempotent 337 | private Boolean remove(int seq) { 338 | TRLOrder a = buys.remove(seq); 339 | if (a != null) { 340 | return true; 341 | } 342 | TRLOrder b = sels.remove(seq); 343 | if (b != null) { 344 | return false; 345 | } 346 | return null; 347 | } 348 | 349 | // private void shelve(RLOrder after, UInt32 seq, boolean isAligned) { 350 | // shelve(after, seq.intValue(), isAligned); 351 | // } 352 | 353 | /** 354 | * @param in 355 | * @return null if not matched, true if aligned, false if reversed 356 | */ 357 | private Optional pairMatched(RLOrder in) { 358 | return in.getCpair().isMatch(this.botConfig.getPair()); 359 | } 360 | 361 | private String dump(int ledgerClosed, int ledgerValidated, List> sortedSels, 362 | List> sortedBuys) { 363 | StringBuffer sb = new StringBuffer("Orderbook"); 364 | sb.append(botConfig.getPair()); 365 | sb.append("\n"); 366 | sb.append("Ledger closed : "); 367 | sb.append(ledgerClosed); 368 | sb.append(" "); 369 | sb.append("Ledger validated : "); 370 | sb.append(ledgerValidated); 371 | sb.append("\n\n"); 372 | sb.append("SELLS\n"); 373 | sortedSels.stream().forEach(entry -> { 374 | sb.append(entry.getValue().getTotalPrice().toText()); 375 | sb.append(" "); 376 | sb.append(entry.getValue().getQuantity()); 377 | sb.append(" "); 378 | sb.append(BigDecimal.ONE.divide(entry.getValue().getRate(), MathContext.DECIMAL32).toPlainString()); 379 | sb.append(" "); 380 | sb.append(" Seq:"); 381 | sb.append(entry.getKey()); 382 | sb.append("\n"); 383 | }); 384 | sb.append("\n"); 385 | sb.append("BUYS\n"); 386 | sortedBuys.stream().forEach(entry -> { 387 | sb.append(entry.getValue().getQuantity().toText()); 388 | sb.append(" "); 389 | sb.append(entry.getValue().getTotalPrice()); 390 | sb.append(" "); 391 | sb.append(entry.getValue().getRate().toPlainString()); 392 | sb.append(" "); 393 | sb.append(" Seq:"); 394 | sb.append(entry.getKey()); 395 | sb.append("\n"); 396 | }); 397 | 398 | return sb.toString(); 399 | } 400 | 401 | public static class OnOrderConsumed extends BusBase { 402 | public final List origins; 403 | 404 | public OnOrderConsumed(List origins) { 405 | this.origins = origins; 406 | } 407 | } 408 | 409 | // public static class OnOrderPartialConsumed extends BusBase { 410 | // public final List orders; 411 | // 412 | // public OnOrderPartialConsumed(List orders) { 413 | // this.orders = orders; 414 | // } 415 | // } 416 | 417 | public static Orderbook newInstance(BotConfig botConfig, Config config) { 418 | Orderbook res = new Orderbook(config, botConfig); 419 | return res; 420 | } 421 | 422 | public static class OnAccOffersDone extends BusBase { 423 | } 424 | 425 | } 426 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/Starter.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.concurrent.CountDownLatch; 7 | import java.util.logging.Level; 8 | 9 | import com.mbcu.mmm.main.WebSocketClient; 10 | import com.mbcu.mmm.models.internal.Config; 11 | import com.mbcu.mmm.models.request.AccountInfo; 12 | import com.mbcu.mmm.models.request.AccountOffers; 13 | import com.mbcu.mmm.models.request.Request.Command; 14 | import com.mbcu.mmm.models.request.Subscribe; 15 | import com.mbcu.mmm.models.request.Subscribe.Stream; 16 | import com.mbcu.mmm.rx.BusBase; 17 | import com.mbcu.mmm.sequences.counters.Yuki; 18 | import com.mbcu.mmm.sequences.state.StateProvider; 19 | import com.mbcu.mmm.utils.MyLogger; 20 | import com.neovisionaries.ws.client.WebSocketException; 21 | 22 | import io.reactivex.disposables.CompositeDisposable; 23 | import io.reactivex.observers.DisposableObserver; 24 | import io.reactivex.schedulers.Schedulers; 25 | 26 | public class Starter extends Base { 27 | private CountDownLatch latch; 28 | 29 | private Starter(Config config) { 30 | super(MyLogger.getLogger(Starter.class.getName()), config); 31 | List dispos = new ArrayList<>(); 32 | 33 | CompositeDisposable disSubscribeLedger = new CompositeDisposable(); 34 | disSubscribeLedger 35 | .add(bus.toObservable().subscribeOn(Schedulers.newThread()).subscribeWith(new DisposableObserver() { 36 | 37 | @Override 38 | public void onNext(Object o) { 39 | BusBase base = (BusBase) o; 40 | try { 41 | if (base instanceof Common.OnAccountInfoSequence) { 42 | latch.countDown(); 43 | disSubscribeLedger.dispose(); 44 | } 45 | } catch (Exception e) { 46 | MyLogger.exception(LOGGER, base.toString(), e); 47 | throw e; 48 | } 49 | } 50 | 51 | @Override 52 | public void onError(Throwable e) { 53 | } 54 | 55 | @Override 56 | public void onComplete() { 57 | } 58 | 59 | })); 60 | dispos.add(disSubscribeLedger); 61 | 62 | CompositeDisposable disAccInfo = new CompositeDisposable(); 63 | disAccInfo 64 | .add(bus.toObservable().subscribeOn(Schedulers.newThread()).subscribeWith(new DisposableObserver() { 65 | 66 | @Override 67 | public void onNext(Object o) { 68 | BusBase base = (BusBase) o; 69 | try { 70 | if (o instanceof Common.OnAccountInfoSequence) { 71 | latch.countDown(); 72 | disAccInfo.dispose(); 73 | } 74 | } catch (Exception e) { 75 | MyLogger.exception(LOGGER, base.toString(), e); 76 | throw e; 77 | } 78 | } 79 | 80 | @Override 81 | public void onError(Throwable e) { 82 | } 83 | 84 | @Override 85 | public void onComplete() { 86 | } 87 | 88 | })); 89 | dispos.add(disAccInfo); 90 | 91 | CompositeDisposable disAccountOffers = new CompositeDisposable(); 92 | disAccountOffers 93 | .add(bus.toObservable().subscribeOn(Schedulers.newThread()).subscribeWith(new DisposableObserver() { 94 | 95 | @Override 96 | public void onNext(Object o) { 97 | BusBase base = (BusBase) o; 98 | try { 99 | if (base instanceof Orderbook.OnAccOffersDone) { 100 | latch.countDown(); 101 | disAccountOffers.dispose(); 102 | } 103 | } catch (Exception e) { 104 | MyLogger.exception(LOGGER, base.toString(), e); 105 | throw e; 106 | } 107 | } 108 | 109 | @Override 110 | public void onError(Throwable e) { 111 | } 112 | 113 | @Override 114 | public void onComplete() { 115 | } 116 | 117 | })); 118 | dispos.add(disAccountOffers); 119 | 120 | CompositeDisposable disLedgerClosed = new CompositeDisposable(); 121 | disLedgerClosed 122 | .add(bus.toObservable().subscribeOn(Schedulers.newThread()).subscribeWith(new DisposableObserver() { 123 | 124 | @Override 125 | public void onNext(Object o) { 126 | BusBase base = (BusBase) o; 127 | try { 128 | if (base instanceof Common.OnLedgerClosed) { 129 | latch.countDown(); 130 | disLedgerClosed.dispose(); 131 | } 132 | } catch (Exception e) { 133 | MyLogger.exception(LOGGER, base.toString(), e); 134 | throw e; 135 | } 136 | } 137 | 138 | @Override 139 | public void onError(Throwable e) { 140 | } 141 | 142 | @Override 143 | public void onComplete() { 144 | } 145 | 146 | })); 147 | dispos.add(disLedgerClosed); 148 | 149 | CompositeDisposable disOnWSConnected = new CompositeDisposable(); 150 | disOnWSConnected 151 | .add(bus.toObservable().subscribeOn(Schedulers.newThread()).subscribeWith(new DisposableObserver() { 152 | 153 | @Override 154 | public void onNext(Object o) { 155 | BusBase base = (BusBase) o; 156 | try { 157 | if (base instanceof WebSocketClient.WSConnected) { 158 | log("connected", Level.FINER); 159 | latch.countDown(); 160 | sendInitRequests(); 161 | disOnWSConnected.dispose(); 162 | } 163 | } catch (Exception e) { 164 | MyLogger.exception(LOGGER, base.toString(), e); 165 | throw e; 166 | } 167 | } 168 | 169 | @Override 170 | public void onError(Throwable e) { 171 | } 172 | 173 | @Override 174 | public void onComplete() { 175 | } 176 | 177 | })); 178 | dispos.add(disOnWSConnected); 179 | 180 | CompositeDisposable disOnAccountOffersDone = new CompositeDisposable(); 181 | disOnAccountOffersDone 182 | .add(bus.toObservable().subscribeOn(Schedulers.newThread()).subscribeWith(new DisposableObserver() { 183 | 184 | @Override 185 | public void onNext(Object o) { 186 | if (o instanceof Orderbook.OnAccOffersDone) { 187 | latch.countDown(); 188 | } 189 | } 190 | 191 | @Override 192 | public void onError(Throwable e) { 193 | // TODO Auto-generated method stub 194 | 195 | } 196 | 197 | @Override 198 | public void onComplete() { 199 | // TODO Auto-generated method stub 200 | } 201 | 202 | })); 203 | dispos.add(disOnAccountOffersDone); 204 | latch = new CountDownLatch(dispos.size()); 205 | } 206 | 207 | private void sendInitRequests() { 208 | bus.send(new WebSocketClient.WSRequestSendText(AccountInfo.of(config).stringify())); 209 | bus.send(new WebSocketClient.WSRequestSendText(AccountOffers.of(config).stringify())); 210 | String subscribeRequest = Subscribe.build(Command.SUBSCRIBE).withStream(Stream.LEDGER) 211 | .withAccount(config.getCredentials().getAddress()).stringify(); 212 | bus.send(new WebSocketClient.WSRequestSendText(subscribeRequest)); 213 | } 214 | 215 | public static Starter newInstance(Config config) { 216 | Starter res = new Starter(config); 217 | StateProvider.getInstance(config); 218 | return res; 219 | } 220 | 221 | public void start() throws IOException, WebSocketException, InterruptedException { 222 | log("Initiating ..."); 223 | Common.newInstance(config); 224 | Balancer.newInstance(config); 225 | Emailer.newInstance(config); 226 | WebSocketClient webSocketClient = new WebSocketClient(super.config); 227 | webSocketClient.start(); 228 | latch.await(); 229 | postInit(); 230 | } 231 | 232 | private void postInit() { 233 | log("Initiation complete"); 234 | Yuki.newInstance(config); 235 | Dataapi.newInstance(config); 236 | bus.send(new OnInitiated()); 237 | } 238 | 239 | public static class OnInitiated extends BusBase { 240 | } 241 | 242 | } 243 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/Tester.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences; 2 | 3 | import java.util.concurrent.Executors; 4 | import java.util.concurrent.ScheduledExecutorService; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import com.mbcu.mmm.sequences.state.State; 8 | 9 | public class Tester { 10 | 11 | private State state; 12 | ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); 13 | Runnable periodicTask = new Runnable() { 14 | public void run() { 15 | // Invoke method(s) to do the work 16 | doPeriodicWork(); 17 | } 18 | }; 19 | 20 | private Tester(State state) { 21 | this.state = state; 22 | } 23 | 24 | public static Tester newInstance(State state) { 25 | return new Tester(state); 26 | } 27 | 28 | public void loop() { 29 | executor.scheduleAtFixedRate(periodicTask, 0, 1, TimeUnit.SECONDS); 30 | } 31 | 32 | private void doPeriodicWork() { 33 | // System.out.println("Current sequence " + state.getSequence()); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/Txc.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences; 2 | 3 | import com.mbcu.mmm.models.internal.RLOrder; 4 | import com.mbcu.mmm.rx.BusBase; 5 | import com.mbcu.mmm.rx.RxBus; 6 | import com.mbcu.mmm.rx.RxBusProvider; 7 | import com.mbcu.mmm.sequences.Common.OnLedgerClosed; 8 | import com.mbcu.mmm.sequences.Common.OnOfferCreate; 9 | import com.mbcu.mmm.sequences.Common.OnRPCTesFail; 10 | import com.mbcu.mmm.sequences.Common.OnRPCTesSuccess; 11 | import com.mbcu.mmm.sequences.state.State; 12 | import com.mbcu.mmm.sequences.state.State.OnOrderReady; 13 | import com.mbcu.mmm.utils.MyLogger; 14 | import com.ripple.core.coretypes.hash.Hash256; 15 | import com.ripple.core.serialized.enums.EngineResult; 16 | 17 | import io.reactivex.disposables.CompositeDisposable; 18 | import io.reactivex.observers.DisposableObserver; 19 | import io.reactivex.schedulers.Schedulers; 20 | 21 | public class Txc extends Base { 22 | 23 | private final RLOrder outbound; 24 | private final Hash256 hash; 25 | private final int seq; 26 | private final int maxLedger; 27 | private RxBus bus = RxBusProvider.getInstance(); 28 | private final CompositeDisposable disposables = new CompositeDisposable(); 29 | private boolean isTesSuccess; 30 | private OnOrderReady.Source source; 31 | 32 | private Txc(RLOrder outbound, OnOrderReady.Source source, Hash256 hash, int seq, int maxLedger) { 33 | super(MyLogger.getLogger(String.format(Txc.class.getName())), null); 34 | this.outbound = outbound; 35 | this.hash = hash; 36 | this.seq = seq; 37 | this.maxLedger = maxLedger; 38 | this.source = source; 39 | } 40 | 41 | private void initBus() { 42 | disposables 43 | .add(bus.toObservable().subscribeOn(Schedulers.newThread()).subscribeWith(new DisposableObserver() { 44 | 45 | @Override 46 | public void onNext(Object o) { 47 | BusBase base = (BusBase) o; 48 | try { 49 | if (base instanceof Common.OnOfferCreate) { 50 | OnOfferCreate event = (OnOfferCreate) o; 51 | if (event.sequence.intValue() == seq) { 52 | disposables.dispose(); 53 | bus.send(new State.RequestRemoveCreate(seq)); 54 | } 55 | } else if (base instanceof Common.OnLedgerClosed) { 56 | OnLedgerClosed event = (OnLedgerClosed) o; 57 | if (isTesSuccess && event.ledgerEvent.getValidated() > maxLedger) { 58 | disposables.dispose(); 59 | bus.send(new State.RequestSequenceSync()); 60 | bus.send(new State.RequestRemoveCreate(seq)); 61 | bus.send(new State.OnOrderReady(outbound, source, hash, " MaxLedger passed")); 62 | return; 63 | } 64 | } else if (base instanceof Common.OnRPCTesSuccess) { 65 | OnRPCTesSuccess event = (OnRPCTesSuccess) o; 66 | if (event.hash.compareTo(hash) == 0) { 67 | isTesSuccess = true; 68 | } 69 | } else if (base instanceof Common.OnRPCTesFail) { 70 | OnRPCTesFail event = (OnRPCTesFail) o; 71 | if (event.sequence.intValue() != seq || event.hash.compareTo(hash) != 0) { 72 | return; 73 | } 74 | String er = event.engineResult; 75 | 76 | if (er.equals(EngineResult.terINSUF_FEE_B.toString())) { 77 | // no fund 78 | bus.send(new Emailer.SendEmailBotError(EngineResult.terINSUF_FEE_B.human, outbound.getCpair().getFw(), System.currentTimeMillis(), true)); 79 | return; 80 | } 81 | 82 | if (er.startsWith("ter")) { 83 | // this includes terQUEUED and terPRE_SEQ and behave like 84 | // tesSUCCESS 85 | // https://www.xrpchat.com/topic/2654-transaction-failed-with-terpre_seq-but-still-executed/?page=2 86 | // disposables.dispose(); 87 | // bus.send(new RequestRemove(seq)); 88 | isTesSuccess = true; 89 | return; 90 | } 91 | // if (er.equals(EngineResult.tefALREADY.toString())){ 92 | // // This means a tx with the same sequence number is already 93 | // queued. 94 | // disposables.dispose(); 95 | // bus.send(new RequestRemove(seq )); 96 | // return; 97 | // } 98 | if (er.equals(EngineResult.tefPAST_SEQ.toString())) { 99 | disposables.dispose(); 100 | bus.send(new State.RequestSequenceSync()); 101 | bus.send(new State.RequestRemoveCreate(seq)); 102 | bus.send(new State.OnOrderReady(outbound, source, hash, " was tefPAST_SEQ")); 103 | return; 104 | } 105 | 106 | if (er.equals(EngineResult.telINSUF_FEE_P.toString())) { 107 | // retry next ledger 108 | disposables.dispose(); 109 | bus.send(new State.RequestWaitNextLedger()); 110 | bus.send(new State.RequestSequenceSync()); 111 | bus.send(new State.RequestRemoveCreate(seq)); 112 | bus.send(new State.OnOrderReady(outbound, source, hash, " was telINSUF_FEE_P")); 113 | return; 114 | } 115 | 116 | disposables.dispose(); 117 | bus.send(new State.RequestRemoveCreate(seq)); 118 | bus.send(new Emailer.SendEmailBotError(er, outbound.getCpair().getFw(), System.currentTimeMillis(), false)); 119 | // bus.send(new State.OnOrderReady(outbound, hash, "retry " + 120 | // er)); 121 | } 122 | } catch (Exception e) { 123 | MyLogger.exception(LOGGER, base.toString(), e); 124 | throw e; 125 | } 126 | } 127 | 128 | @Override 129 | public void onError(Throwable e) { 130 | } 131 | 132 | @Override 133 | public void onComplete() { 134 | } 135 | })); 136 | 137 | } 138 | 139 | public int getSeq() { 140 | return seq; 141 | } 142 | 143 | public RLOrder getOutbound() { 144 | return outbound; 145 | } 146 | 147 | public static Txc newInstance(RLOrder outbound, OnOrderReady.Source source, Hash256 hash, int seq, int maxLedger) { 148 | Txc res = new Txc(outbound, source, hash, seq, maxLedger); 149 | res.initBus(); 150 | return res; 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/Txd.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences; 2 | 3 | import com.mbcu.mmm.models.internal.Cpair; 4 | import com.mbcu.mmm.rx.BusBase; 5 | import com.mbcu.mmm.rx.RxBus; 6 | import com.mbcu.mmm.rx.RxBusProvider; 7 | import com.mbcu.mmm.sequences.Common.OnLedgerClosed; 8 | import com.mbcu.mmm.sequences.Common.OnOfferCanceled; 9 | import com.mbcu.mmm.sequences.Common.OnRPCTesFail; 10 | import com.mbcu.mmm.sequences.Common.OnRPCTesSuccess; 11 | import com.mbcu.mmm.sequences.state.State; 12 | import com.mbcu.mmm.utils.MyLogger; 13 | import com.ripple.core.serialized.enums.EngineResult; 14 | 15 | import io.reactivex.disposables.CompositeDisposable; 16 | import io.reactivex.observers.DisposableObserver; 17 | import io.reactivex.schedulers.Schedulers; 18 | 19 | public class Txd extends Base { 20 | 21 | private final Cpair cpair; 22 | private final int canSeq; 23 | private final int newSeq; 24 | private final int maxLedger; 25 | private final RxBus bus = RxBusProvider.getInstance(); 26 | private boolean isTesSuccess; 27 | 28 | private final CompositeDisposable disposables = new CompositeDisposable(); 29 | 30 | private Txd(Cpair cpair, int canSeq, int newSeq, int maxLedger) { 31 | super(MyLogger.getLogger(String.format(Txd.class.getName())), null); 32 | this.cpair = cpair; 33 | this.canSeq = canSeq; 34 | this.newSeq = newSeq; 35 | this.maxLedger = maxLedger; 36 | } 37 | 38 | private void initBus() { 39 | disposables 40 | .add(bus.toObservable().subscribeOn(Schedulers.newThread()).subscribeWith(new DisposableObserver() { 41 | 42 | @Override 43 | public void onNext(Object o) { 44 | BusBase base = (BusBase) o; 45 | try { 46 | if (base instanceof Common.OnOfferCanceled) { 47 | OnOfferCanceled event = (OnOfferCanceled) o; 48 | if (event.prevSeq.intValue() == canSeq) { 49 | disposables.dispose(); 50 | bus.send(new State.RequestRemoveCancel(canSeq)); 51 | } 52 | } else if (base instanceof Common.OnLedgerClosed) { 53 | OnLedgerClosed event = (OnLedgerClosed) o; 54 | if (isTesSuccess && event.ledgerEvent.getValidated() > maxLedger) { // failed 55 | // to 56 | // enter 57 | // ledger 58 | disposables.dispose(); 59 | bus.send(new State.RequestSequenceSync()); 60 | bus.send(new State.RequestRemoveCancel(canSeq)); 61 | bus.send(new State.OnCancelReady(cpair.toString(), canSeq)); 62 | return; 63 | } 64 | } else if (base instanceof Common.OnRPCTesSuccess) { 65 | OnRPCTesSuccess event = (OnRPCTesSuccess) o; 66 | if (event.sequence.intValue() == newSeq) { 67 | isTesSuccess = true; 68 | } 69 | } else if (base instanceof Common.OnRPCTesFail) { 70 | OnRPCTesFail event = (OnRPCTesFail) o; 71 | if (event.sequence.intValue() != newSeq) { 72 | return; 73 | } 74 | String er = event.engineResult; 75 | 76 | if (er.equals(EngineResult.terINSUF_FEE_B.toString())) { 77 | // no fund 78 | bus.send(new Emailer.SendEmailBotError(EngineResult.terINSUF_FEE_B.human, cpair.getFw(), System.currentTimeMillis(), false)); 79 | return; 80 | } 81 | 82 | if (er.startsWith("ter")) { 83 | isTesSuccess = true; 84 | return; 85 | } 86 | if (er.equals(EngineResult.tefPAST_SEQ.toString())) { 87 | disposables.dispose(); 88 | bus.send(new State.RequestSequenceSync()); 89 | bus.send(new State.RequestRemoveCancel(canSeq)); 90 | bus.send(new State.OnCancelReady(cpair.toString(), canSeq)); 91 | return; 92 | } 93 | if (er.equals(EngineResult.telINSUF_FEE_P.toString())) { 94 | disposables.dispose(); 95 | bus.send(new State.RequestWaitNextLedger()); 96 | bus.send(new State.RequestSequenceSync()); 97 | bus.send(new State.RequestRemoveCancel(canSeq)); 98 | bus.send(new State.OnCancelReady(cpair.toString(), canSeq)); 99 | return; 100 | } 101 | disposables.dispose(); 102 | bus.send(new State.RequestRemoveCancel(canSeq)); 103 | // bus.send(new State.OnOrderReady(outbound, hash, "retry " + 104 | // er)); 105 | } 106 | } catch (Exception e) { 107 | MyLogger.exception(LOGGER, base.toString(), e); 108 | throw e; 109 | } 110 | } 111 | 112 | @Override 113 | public void onError(Throwable e) { 114 | } 115 | 116 | @Override 117 | public void onComplete() { 118 | // TODO Auto-generated method stub 119 | 120 | } 121 | })); 122 | 123 | } 124 | 125 | public int getCanSeq() { 126 | return canSeq; 127 | } 128 | 129 | public Cpair getPair() { 130 | return cpair; 131 | } 132 | 133 | public static Txd newInstance(Cpair cpair, int canSeq, int newSeq, int maxLedger) { 134 | Txd res = new Txd(cpair, canSeq, newSeq, maxLedger); 135 | res.initBus(); 136 | return res; 137 | } 138 | 139 | public static class TxdMini { 140 | public final Cpair cpair; 141 | public final int canSeq; 142 | 143 | public TxdMini(Cpair cpair, int canSeq) { 144 | super(); 145 | this.cpair = cpair; 146 | this.canSeq = canSeq; 147 | } 148 | 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/counters/Counter.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences.counters; 2 | 3 | import com.mbcu.mmm.models.internal.RLOrder; 4 | 5 | public interface Counter { 6 | 7 | public void onCounterReady(RLOrder counter); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/counters/Yuki.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences.counters; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.util.List; 6 | import java.util.Optional; 7 | import java.util.logging.Level; 8 | 9 | import com.mbcu.mmm.models.internal.BotConfig; 10 | import com.mbcu.mmm.models.internal.BotConfig.Strategy; 11 | import com.mbcu.mmm.models.internal.Config; 12 | import com.mbcu.mmm.models.internal.RLOrder; 13 | import com.mbcu.mmm.models.internal.RLOrder.Direction; 14 | import com.mbcu.mmm.rx.BusBase; 15 | import com.mbcu.mmm.rx.RxBus; 16 | import com.mbcu.mmm.rx.RxBusProvider; 17 | import com.mbcu.mmm.sequences.Base; 18 | import com.mbcu.mmm.sequences.Orderbook; 19 | import com.mbcu.mmm.sequences.Orderbook.OnOrderConsumed; 20 | import com.mbcu.mmm.sequences.state.State; 21 | import com.mbcu.mmm.sequences.state.State.OnOrderReady.Source; 22 | import com.mbcu.mmm.utils.MyLogger; 23 | import com.mbcu.mmm.utils.MyUtils; 24 | import com.ripple.core.coretypes.Amount; 25 | 26 | import io.reactivex.Observer; 27 | import io.reactivex.disposables.Disposable; 28 | import io.reactivex.schedulers.Schedulers; 29 | 30 | public class Yuki extends Base implements Counter { 31 | private RxBus bus = RxBusProvider.getInstance(); 32 | private Config config; 33 | int count; 34 | 35 | public Yuki(Config config) { 36 | super(MyLogger.getLogger(Yuki.class.getName()), config); 37 | this.config = config; 38 | 39 | bus.toObservable().subscribeOn(Schedulers.newThread()).subscribe(new Observer() { 40 | 41 | @Override 42 | public void onSubscribe(Disposable d) { 43 | } 44 | 45 | @Override 46 | public void onNext(Object o) { 47 | BusBase base = (BusBase) o; 48 | try { 49 | if (base instanceof Orderbook.OnOrderConsumed){ 50 | OnOrderConsumed event = (OnOrderConsumed) o; 51 | counter(event.origins); 52 | } 53 | } catch (Exception e) { 54 | MyLogger.exception(LOGGER, base.toString(), e); 55 | throw e; 56 | } 57 | } 58 | 59 | @Override 60 | public void onError(Throwable e) {} 61 | 62 | @Override 63 | public void onComplete() {} 64 | }); 65 | 66 | } 67 | 68 | public static Yuki newInstance(Config config) { 69 | Yuki counter = new Yuki(config); 70 | return counter; 71 | } 72 | 73 | 74 | // change this to List for incoming original orders 75 | public void counter(List origins) { 76 | origins.stream() 77 | .map(o -> {return new FullCounterWrap(o, config);}) 78 | .filter(wrap -> wrap.bcd.botConfig != null) 79 | // .filter(wrap -> wrap.bcd.botConfig.getStrategy() == Strategy.FULLFIXED 80 | // || wrap.bcd.botConfig.getStrategy() == Strategy.PPT ) 81 | .map(this::buildCounter) 82 | .filter(Optional::isPresent) 83 | .map(Optional::get) 84 | .forEach(this::onCounterReady); 85 | } 86 | 87 | // public void counterPartial(List origins) { 88 | // origins.stream() 89 | // .map(o -> {return new FullCounterWrap(o, config);}) 90 | // .filter(wrap -> wrap.bcd.botConfig != null) 91 | // .map(this::buildCounter) 92 | // .filter(Optional::isPresent) 93 | // .map(Optional::get) 94 | // .forEach(this::onCounterReady); 95 | // 96 | // 97 | //// oes.stream() 98 | //// .map(this::buildBotDirection) 99 | //// .filter(Optional::isPresent) 100 | //// .filter(optBcd -> optBcd.get().botConfig.getStrategy() == Strategy.PARTIAL) 101 | //// .map(optBcd -> yuki(optBcd.get())) 102 | //// .forEach(this::onCounterReady); 103 | // } 104 | 105 | /* 106 | * This part can be configured so instead of countering, the bot will replace 107 | * taken order. 108 | */ 109 | public Optional buildCounter(FullCounterWrap wrap) { 110 | RLOrder res = null; 111 | BotConfigDirection bcd = wrap.bcd; 112 | RLOrder src = wrap.origin; 113 | 114 | switch (bcd.botConfig.getStrategy()){ 115 | case PPT : 116 | res = yukiPct(src, bcd.botConfig, bcd.isDirectionMatch); 117 | break; 118 | case FULLFIXED : 119 | case PARTIAL: 120 | res = yuki(src, bcd.botConfig, bcd.isDirectionMatch); 121 | break; 122 | // rate = ba.before.getRate(); 123 | // if (bcd.isDirectionMatch) { 124 | // BigDecimal botQuantity = bcd.botConfig.getSellOrderQuantity(); 125 | // Amount quantity = ba.after.getQuantity().subtract(botQuantity); 126 | // origin = RLOrder.fromWholeConsumed(Direction.BUY, quantity, ba.after.getTotalPrice(), rate); 127 | // } 128 | // else { 129 | // BigDecimal botQuantity = bcd.botConfig.getBuyOrderQuantity(); 130 | // Amount totalPrice = ba.after.getTotalPrice().subtract(botQuantity); 131 | // origin = RLOrder.fromWholeConsumed(Direction.BUY, ba.after.getQuantity(), totalPrice, rate); 132 | // } 133 | // bcd.rlOrder = origin; 134 | // res = yuki(bcd); 135 | // break; 136 | default : 137 | } 138 | if (res != null && (res.getQuantity().value().compareTo(BigDecimal.ZERO) <= 0 || res.getTotalPrice().value().compareTo(BigDecimal.ZERO) <= 0)) { 139 | log("Counter anomaly\n" + res.stringify()); 140 | } 141 | log("Full Counter"); 142 | return Optional.of(res); 143 | } 144 | 145 | private RLOrder yuki(RLOrder source, BotConfig botConfig, boolean isCounteringSell) { 146 | MathContext mc = MathContext.DECIMAL64; 147 | // BigDecimal mtp = botConfig.getGridSpace(); 148 | // BigDecimal sqrt = MyUtils.bigSqrt(botConfig.getGridSpace()); 149 | // Amount srcQuantity = source.getQuantity(); 150 | // Amount srcTotalPrice = source.getTotalPrice(); 151 | // BigDecimal newRate = null; 152 | // RLOrder res = null; 153 | // 154 | // if (isCounteringSell){ 155 | // newRate = source.getRate().multiply(mtp, MathContext.DECIMAL64); 156 | // Amount quantity = srcQuantity.divide(sqrt); 157 | // Amount totalPrice = RLOrder.amount(quantity.value().multiply(newRate), srcTotalPrice.currency(), srcTotalPrice.issuer()); 158 | // res = RLOrder.rateUnneeded(Direction.SELL, quantity, totalPrice); 159 | // } 160 | // else { 161 | // newRate = BigDecimal.ONE.divide(source.getRate(), MathContext.DECIMAL64).divide(mtp, MathContext.DECIMAL64); 162 | // Amount quantity = srcTotalPrice.multiply(sqrt); 163 | // Amount totalPrice = RLOrder.amount(quantity.value().multiply(newRate, mc), srcQuantity.currency(), srcQuantity.issuer()); 164 | // res = RLOrder.rateUnneeded(Direction.BUY, quantity, totalPrice); 165 | // } 166 | // return res; 167 | 168 | // RLOrder origin = bcd.rlOrder; 169 | // Amount oldQuantity = origin.getQuantity().multiply(new BigDecimal("-1")); 170 | // Amount oldTotalPrice = origin.getTotalPrice().multiply(new BigDecimal("-1")); 171 | Amount srcQuantity = source.getQuantity(); 172 | Amount srcTotalPrice = source.getTotalPrice(); 173 | BigDecimal newRate = null; 174 | RLOrder res = null; 175 | 176 | if (isCounteringSell) { 177 | newRate = source.getRate().add(botConfig.getGridSpace()); 178 | Amount totalPrice = RLOrder.amount(srcQuantity.value().multiply(newRate), srcTotalPrice.currency(), srcTotalPrice.issuer()); 179 | res = RLOrder.rateUnneeded(Direction.SELL, srcQuantity, totalPrice); 180 | } 181 | else { 182 | System.out.println(source.getRate().toPlainString()); 183 | newRate = BigDecimal.ONE.divide(source.getRate(), mc).subtract(botConfig.getGridSpace()); 184 | if (newRate.compareTo(BigDecimal.ZERO) <= 0) { 185 | log("counter rate below zero " + newRate + " " + source.getCpair(), Level.SEVERE); 186 | return null; 187 | } 188 | Amount totalPrice = RLOrder.amount(srcTotalPrice.value().multiply(newRate), srcQuantity.currency(), srcQuantity.issuer()); 189 | res = RLOrder.rateUnneeded(Direction.BUY, srcTotalPrice, totalPrice); 190 | } 191 | return res; 192 | } 193 | 194 | private static class FullCounterWrap { 195 | RLOrder origin; 196 | BotConfigDirection bcd; 197 | 198 | public FullCounterWrap(RLOrder origin, Config config) { 199 | this.origin = origin; 200 | this.bcd = new BotConfigDirection(config, origin); 201 | } 202 | } 203 | 204 | /** 205 | * 206 | * @param base 207 | * RLOrder reflecting consumed offer. Both totalPrice and quantity 208 | * have to be negative 209 | * @param botConfig 210 | * @param isDirectionMatch 211 | * Direction reflecting counter direction (not consumed order 212 | * direction) 213 | * @return 214 | */ 215 | private RLOrder yukiPct(RLOrder source, BotConfig botConfig, boolean isCounteringSell) { 216 | MathContext mc = MathContext.DECIMAL64; 217 | BigDecimal mtp = botConfig.getGridSpace(); 218 | BigDecimal sqrt = MyUtils.bigSqrt(botConfig.getGridSpace()); 219 | Amount srcQuantity = source.getQuantity(); 220 | Amount srcTotalPrice = source.getTotalPrice(); 221 | BigDecimal newRate = null; 222 | RLOrder res = null; 223 | 224 | if (!isCounteringSell){ 225 | newRate = source.getRate().multiply(mtp, MathContext.DECIMAL64); 226 | Amount quantity = srcQuantity.divide(sqrt); 227 | Amount totalPrice = RLOrder.amount(quantity.value().multiply(newRate), srcTotalPrice.currency(), srcTotalPrice.issuer()); 228 | res = RLOrder.rateUnneeded(Direction.SELL, quantity, totalPrice); 229 | } 230 | else { 231 | newRate = BigDecimal.ONE.divide(source.getRate(), MathContext.DECIMAL64).divide(mtp, MathContext.DECIMAL64); 232 | Amount quantity = srcTotalPrice.multiply(sqrt); 233 | Amount totalPrice = RLOrder.amount(quantity.value().multiply(newRate, mc), srcQuantity.currency(), srcQuantity.issuer()); 234 | res = RLOrder.rateUnneeded(Direction.BUY, quantity, totalPrice); 235 | } 236 | return res; 237 | } 238 | 239 | 240 | 241 | private static class BotConfigDirection { 242 | boolean isDirectionMatch = true; 243 | BotConfig botConfig; 244 | RLOrder rlOrder; 245 | 246 | BotConfigDirection(Config config, RLOrder offer) { 247 | this.rlOrder = offer; 248 | botConfig = config.getBotConfigMap().get(offer.getCpair().getFw()); 249 | if (botConfig == null) { 250 | botConfig = config.getBotConfigMap().get(offer.getCpair().getRv()); 251 | isDirectionMatch = false; 252 | } 253 | } 254 | } 255 | 256 | private Optional buildBotDirection(RLOrder origin){ 257 | BotConfigDirection res = new BotConfigDirection(config, origin); 258 | if (res.botConfig == null) { 259 | return Optional.empty(); 260 | } 261 | return Optional.of(res); 262 | } 263 | 264 | 265 | 266 | @Override 267 | public void onCounterReady(RLOrder counter) { 268 | bus.send(new State.OnOrderReady(counter, Source.COUNTER)); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/state/State.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences.state; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.List; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.concurrent.ConcurrentLinkedQueue; 7 | import java.util.concurrent.atomic.AtomicBoolean; 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | import java.util.logging.Level; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.Stream; 12 | 13 | import com.mbcu.mmm.main.WebSocketClient; 14 | import com.mbcu.mmm.models.internal.Config; 15 | import com.mbcu.mmm.models.internal.Cpair; 16 | import com.mbcu.mmm.models.internal.LedgerEvent; 17 | import com.mbcu.mmm.models.internal.RLOrder; 18 | import com.mbcu.mmm.models.request.AccountInfo; 19 | import com.mbcu.mmm.models.request.Submit; 20 | import com.mbcu.mmm.rx.BusBase; 21 | import com.mbcu.mmm.rx.RxBus; 22 | import com.mbcu.mmm.rx.RxBusProvider; 23 | import com.mbcu.mmm.sequences.Balancer; 24 | import com.mbcu.mmm.sequences.Balancer.OnRequestNonOrderbookRLOrder; 25 | import com.mbcu.mmm.sequences.Base; 26 | import com.mbcu.mmm.sequences.Common; 27 | import com.mbcu.mmm.sequences.Common.OnAccountInfoSequence; 28 | import com.mbcu.mmm.sequences.Common.OnLedgerClosed; 29 | import com.mbcu.mmm.sequences.Common.OnOfferCanceled; 30 | import com.mbcu.mmm.sequences.Common.OnOfferCreate; 31 | import com.mbcu.mmm.sequences.Common.OnOfferEdited; 32 | import com.mbcu.mmm.sequences.Txc; 33 | import com.mbcu.mmm.sequences.Txd; 34 | import com.mbcu.mmm.sequences.Txd.TxdMini; 35 | import com.mbcu.mmm.utils.MyLogger; 36 | import com.ripple.core.coretypes.AccountID; 37 | import com.ripple.core.coretypes.Amount; 38 | import com.ripple.core.coretypes.hash.Hash256; 39 | import com.ripple.core.coretypes.uint.UInt32; 40 | import com.ripple.core.types.known.tx.signed.SignedTransaction; 41 | import com.ripple.core.types.known.tx.txns.OfferCancel; 42 | 43 | import io.reactivex.Observer; 44 | import io.reactivex.disposables.Disposable; 45 | import io.reactivex.schedulers.Schedulers; 46 | import io.reactivex.subjects.PublishSubject; 47 | import io.reactivex.subjects.Subject; 48 | 49 | public class State extends Base { 50 | private static final int DEFAULT_MAX_LEDGER_GAP = 5; 51 | 52 | private final AtomicInteger ledgerClosed = new AtomicInteger(0); 53 | private final AtomicInteger ledgerValidated = new AtomicInteger(0); 54 | private final AtomicInteger sequence = new AtomicInteger(0); 55 | private final ConcurrentHashMap pending = new ConcurrentHashMap<>(); 56 | private final ConcurrentLinkedQueue qPens = new ConcurrentLinkedQueue<>(); 57 | private final ConcurrentHashMap cancels = new ConcurrentHashMap<>(); 58 | private final ConcurrentLinkedQueue qCans = new ConcurrentLinkedQueue<>(); 59 | private final AtomicBoolean flagWaitSeq = new AtomicBoolean(false); 60 | private final AtomicBoolean flagWaitLedger = new AtomicBoolean(false); 61 | private RxBus bus = RxBusProvider.getInstance(); 62 | 63 | private Subject sequenceRefreshObs = PublishSubject.create(); 64 | 65 | public State(Config config) { 66 | super(MyLogger.getLogger(Common.class.getName()), config); 67 | 68 | bus.toObservable().subscribeOn(Schedulers.newThread()).subscribe(new Observer() { 69 | 70 | @Override 71 | public void onSubscribe(Disposable d) { 72 | } 73 | 74 | @Override 75 | public void onNext(Object o) { 76 | BusBase base = (BusBase) o; 77 | try { 78 | if (base instanceof Common.OnOfferCreate) { 79 | OnOfferCreate event = (OnOfferCreate) o; 80 | setSequence(event.sequence); 81 | pending.remove(event.sequence.intValue()); 82 | 83 | } else if (base instanceof Common.OnOfferEdited) { 84 | OnOfferEdited event = (OnOfferEdited) o; 85 | setSequence(event.newSeq); 86 | } else if (base instanceof Common.OnOfferCanceled) { 87 | OnOfferCanceled event = (OnOfferCanceled) o; 88 | cancels.remove(event.prevSeq.intValue()); 89 | setSequence(event.newSeq); 90 | } else if (base instanceof OnOrderReady) { 91 | OnOrderReady event = (OnOrderReady) o; 92 | synchronized (State.this) { 93 | if (flagWaitSeq.get() || flagWaitLedger.get()) { 94 | qPens.add(event); 95 | } else { 96 | ocreate(event); 97 | } 98 | } 99 | } else if (base instanceof OnCancelReady) { 100 | OnCancelReady event = (OnCancelReady) o; 101 | synchronized (State.this) { 102 | if (flagWaitSeq.get() || flagWaitLedger.get()) { 103 | qCans.add(new TxdMini(Cpair.newInstance(event.pair), event.seq)); 104 | } else { 105 | ocancel(Cpair.newInstance(event.pair), event.seq); 106 | } 107 | } 108 | } else if (base instanceof Common.OnLedgerClosed) { 109 | OnLedgerClosed event = (OnLedgerClosed) o; 110 | log(event.ledgerEvent.toString() + " seq : " + sequence); 111 | setLedgerIndex(event.ledgerEvent); 112 | flagWaitLedger.set(false); 113 | if (!flagWaitLedger.get() && !flagWaitSeq.get()) { 114 | drain(); 115 | } 116 | 117 | } else if (base instanceof Common.OnAccountInfoSequence) { 118 | OnAccountInfoSequence event = (OnAccountInfoSequence) o; 119 | log(String.format("New Sequence %d", event.sequence.intValue())); 120 | setSequence(event.sequence); 121 | flagWaitSeq.set(false); 122 | if (!flagWaitLedger.get() && !flagWaitSeq.get()) { 123 | drain(); 124 | } 125 | } else if (base instanceof RequestRemoveCreate) { 126 | RequestRemoveCreate event = (RequestRemoveCreate) o; 127 | pending.remove(event.seq); 128 | } else if (base instanceof RequestRemoveCancel) { 129 | RequestRemoveCancel event = (RequestRemoveCancel) o; 130 | System.out.println("Removing from cancels " + event.seq); 131 | cancels.remove(event.seq); 132 | 133 | } else if (base instanceof RequestSequenceSync) { 134 | sequenceRefreshObs.onNext(flagWaitSeq.compareAndSet(false, true)); 135 | } else if (base instanceof RequestWaitNextLedger) { 136 | flagWaitLedger.set(true); 137 | } else if (base instanceof Balancer.OnRequestNonOrderbookRLOrder) { 138 | OnRequestNonOrderbookRLOrder event = (OnRequestNonOrderbookRLOrder) o; 139 | List crts = Stream.concat(pending.values().stream().map(txc -> { 140 | return txc.getOutbound(); 141 | }), qPens.stream().map(t -> { 142 | return t.outbound; 143 | })).filter(rlo -> rlo.getCpair().isMatch(event.pair) != null).collect(Collectors.toList()); 144 | 145 | List cans = Stream 146 | .concat(cancels.values().stream().filter(txd -> txd.getPair().isMatch(event.pair) != null).map(txd -> { 147 | return txd.getCanSeq(); 148 | }), qCans.stream().filter(mini -> mini.cpair.isMatch(event.pair) != null).map(mini -> { 149 | return mini.canSeq; 150 | })).collect(Collectors.toList()); 151 | 152 | bus.send(new BroadcastPendings(event.pair, crts, cans)); 153 | } 154 | } catch (Exception e) { 155 | MyLogger.exception(LOGGER, base.toString(), e); 156 | throw e; 157 | } 158 | } 159 | 160 | @Override 161 | public void onError(Throwable e) { 162 | log(e.getMessage(), Level.SEVERE); 163 | } 164 | 165 | @Override 166 | public void onComplete() { 167 | } 168 | }); 169 | 170 | sequenceRefreshObs.subscribe(flag -> { 171 | if (flag) { 172 | bus.send(new WebSocketClient.WSRequestSendText(AccountInfo.of(config).stringify())); 173 | } 174 | }); 175 | 176 | } 177 | 178 | public void setLedgerIndex(LedgerEvent event) { 179 | synchronized (ledgerValidated) { 180 | this.ledgerClosed.set(event.getClosed()); 181 | this.ledgerValidated.set(event.getValidated()); 182 | } 183 | } 184 | 185 | public static State newInstance(Config config) { 186 | return new State(config); 187 | } 188 | 189 | private void setSequence(UInt32 seq) { 190 | sequence.set(seq.intValue()); 191 | } 192 | 193 | private int getIncrementSequence() { 194 | return this.sequence.getAndIncrement(); 195 | } 196 | 197 | private void drain() { 198 | while (!qPens.isEmpty()) { 199 | ocreate(qPens.poll()); 200 | } 201 | while (!qCans.isEmpty()) { 202 | TxdMini load = qCans.poll(); 203 | ocancel(load.cpair, load.canSeq); 204 | } 205 | } 206 | 207 | private synchronized void ocreate(OnOrderReady ready) { 208 | int seq = getIncrementSequence(); 209 | while (pending.containsKey(seq)) { 210 | seq = getIncrementSequence(); 211 | } 212 | int maxLedger = ledgerValidated.get() + DEFAULT_MAX_LEDGER_GAP; 213 | SignedTransaction signed = signCreate(ready.outbound, seq, maxLedger, config.getFeeXRP()); 214 | Txc txc = Txc.newInstance(ready.outbound, ready.source, signed.hash, seq, maxLedger); 215 | pending.put(txc.getSeq(), txc); 216 | 217 | StringBuilder sb = new StringBuilder("submitting offerCreate: "); 218 | sb.append(ready.source.text); 219 | sb.append(" "); 220 | sb.append(signed.hash); 221 | sb.append(" "); 222 | sb.append(seq); 223 | if (ready.from != null) { 224 | sb.append(" retry from "); 225 | sb.append(ready.from); 226 | sb.append(" "); 227 | sb.append(ready.memo); 228 | } 229 | log(sb.toString()); 230 | submit(signed.tx_blob); 231 | 232 | } 233 | 234 | private synchronized void ocancel(Cpair cpair, int canSeq) { 235 | int newSeq = getIncrementSequence(); 236 | while (cancels.containsKey(newSeq)) { 237 | newSeq = getIncrementSequence(); 238 | } 239 | int maxLedger = ledgerValidated.get() + DEFAULT_MAX_LEDGER_GAP; 240 | SignedTransaction signed = signCancel(newSeq, canSeq, maxLedger); 241 | Txd txd = Txd.newInstance(cpair, canSeq, newSeq, maxLedger); 242 | cancels.put(canSeq, txd); 243 | log("submitting offerCancel: " + signed.hash + " " + canSeq + " " + newSeq); 244 | submit(signed.tx_blob); 245 | } 246 | 247 | private SignedTransaction signCreate(RLOrder order, int seq, int maxLedger, BigDecimal fees) { 248 | SignedTransaction signed = order.signOfferCreate(config, seq, maxLedger, fees); 249 | return signed; 250 | } 251 | 252 | private SignedTransaction signCancel(int seq, int canSeq, int maxLedger) { 253 | OfferCancel offerCancel = new OfferCancel(); 254 | offerCancel.put(UInt32.OfferSequence, new UInt32(String.valueOf(canSeq))); 255 | offerCancel.fee(new Amount(config.getFeeXRP())); 256 | offerCancel.sequence(new UInt32(String.valueOf(seq))); 257 | offerCancel.lastLedgerSequence(new UInt32(String.valueOf(maxLedger))); 258 | offerCancel.account(AccountID.fromAddress(config.getCredentials().getAddress())); 259 | SignedTransaction signed = offerCancel.sign(config.getCredentials().getSecret()); 260 | return signed; 261 | 262 | } 263 | 264 | private void submit(String txBlob) { 265 | bus.send(new WebSocketClient.WSRequestSendText(Submit.build(txBlob).stringify())); 266 | } 267 | 268 | public static class OnOrderValidated extends BusBase { 269 | public int seq; 270 | 271 | public OnOrderValidated(int seq) { 272 | super(); 273 | this.seq = seq; 274 | } 275 | } 276 | 277 | public static class OnOrderReady extends BusBase { 278 | public enum Source { 279 | BALANCER("balancer"), COUNTER("counter"); 280 | 281 | private String text; 282 | 283 | Source(String text) { 284 | this.text = text; 285 | } 286 | 287 | public String text() { 288 | return text; 289 | } 290 | } 291 | 292 | public RLOrder outbound; 293 | public Hash256 from; 294 | public String memo; 295 | public Source source; 296 | 297 | public OnOrderReady(RLOrder outbound, Source source) { 298 | super(); 299 | this.outbound = outbound; 300 | this.source = source; 301 | } 302 | 303 | public OnOrderReady(RLOrder outbound, Source source, Hash256 from, String memo) { 304 | this(outbound, source); 305 | this.from = from; 306 | this.memo = memo; 307 | } 308 | } 309 | 310 | public static class OnCancelReady extends BusBase { 311 | public final int seq; 312 | public final String pair; 313 | 314 | public OnCancelReady(String pair, int seq) { 315 | super(); 316 | this.pair = pair; 317 | this.seq = seq; 318 | } 319 | 320 | } 321 | 322 | public static class RequestRemoveCreate extends BusBase { 323 | public int seq; 324 | 325 | public RequestRemoveCreate(int seq) { 326 | super(); 327 | this.seq = seq; 328 | } 329 | } 330 | 331 | public static class RequestRemoveCancel extends BusBase { 332 | public int seq; 333 | 334 | public RequestRemoveCancel(int seq) { 335 | super(); 336 | this.seq = seq; 337 | } 338 | 339 | } 340 | 341 | public static class RequestSequenceSync extends BusBase { 342 | } 343 | 344 | public static class RequestWaitNextLedger extends BusBase { 345 | } 346 | 347 | public static class BroadcastPendings extends BusBase { 348 | public final String pair; 349 | public final List creates; 350 | public final List cancels; 351 | 352 | public BroadcastPendings(String pair, List creates, List cancels) { 353 | this.pair = pair; 354 | this.creates = creates; 355 | this.cancels = cancels; 356 | } 357 | } 358 | 359 | public static class RequestOrderCancel extends BusBase { 360 | public final int seq; 361 | 362 | public RequestOrderCancel(int seq) { 363 | super(); 364 | this.seq = seq; 365 | } 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/sequences/state/StateProvider.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences.state; 2 | 3 | import com.mbcu.mmm.models.internal.Config; 4 | 5 | public class StateProvider { 6 | 7 | private static State STATE = null; 8 | 9 | private StateProvider() { 10 | // No instances. 11 | } 12 | 13 | public static State getInstance(Config config) { 14 | if (STATE == null) { 15 | STATE = State.newInstance(config); 16 | } 17 | return STATE; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/utils/GsonUtils.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.utils; 2 | 3 | import java.lang.reflect.Type; 4 | 5 | import com.google.gson.Gson; 6 | import com.google.gson.GsonBuilder; 7 | import com.google.gson.JsonSyntaxException; 8 | 9 | public class GsonUtils { 10 | public static T toBean(String response, Class classOfT) { 11 | GsonBuilder builder = new GsonBuilder(); 12 | // builder.excludeFieldsWithoutExposeAnnotation(); 13 | Gson gson = builder.create(); 14 | try { 15 | return gson.fromJson(response, classOfT); 16 | } catch (JsonSyntaxException e) { 17 | return null; 18 | } 19 | } 20 | 21 | public static String toJson(Object object) { 22 | GsonBuilder builder = new GsonBuilder(); 23 | builder.setPrettyPrinting(); 24 | Gson gson = builder.create(); 25 | String json = gson.toJson(object); 26 | return json; 27 | } 28 | 29 | public static T toBean(String response, Type t) { 30 | GsonBuilder builder = new GsonBuilder(); 31 | // builder.excludeFieldsWithoutExposeAnnotation(); 32 | Gson gson = builder.create(); 33 | try { 34 | return gson.fromJson(response, t); 35 | } catch (JsonSyntaxException e) { 36 | return null; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/utils/Mock.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.utils; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | import java.util.stream.IntStream; 10 | import java.util.stream.Stream; 11 | 12 | import org.json.JSONObject; 13 | 14 | import com.mbcu.mmm.models.internal.Config; 15 | import com.mbcu.mmm.models.internal.Credentials; 16 | import com.mbcu.mmm.models.internal.RLOrder; 17 | import com.mbcu.mmm.models.internal.RLOrder.Direction; 18 | import com.mbcu.mmm.sequences.Common; 19 | import com.mbcu.mmm.sequences.counters.Yuki; 20 | import com.ripple.core.coretypes.AccountID; 21 | import com.ripple.core.coretypes.Amount; 22 | import com.ripple.core.coretypes.Currency; 23 | import com.ripple.core.coretypes.STObject; 24 | import com.ripple.core.coretypes.hash.Hash256; 25 | import com.ripple.core.coretypes.uint.UInt32; 26 | import com.ripple.core.fields.Field; 27 | import com.ripple.core.types.known.sle.LedgerEntry; 28 | import com.ripple.core.types.known.sle.entries.Offer; 29 | import com.ripple.core.types.known.tx.Transaction; 30 | import com.ripple.core.types.known.tx.result.AffectedNode; 31 | import com.ripple.core.types.known.tx.result.TransactionMeta; 32 | import com.ripple.core.types.known.tx.signed.SignedTransaction; 33 | import com.ripple.core.types.known.tx.txns.OfferCreate; 34 | 35 | import io.reactivex.Observable; 36 | 37 | public class Mock { 38 | 39 | public static void main(String[] args) { 40 | BigDecimal a = new BigDecimal("1.01"); 41 | 42 | BigDecimal b = Collections.nCopies(4, BigDecimal.ONE).stream().reduce((x, y) -> x.divide(a, MathContext.DECIMAL32)).get(); 43 | System.out.println(b); 44 | 45 | BigDecimal c = Collections.nCopies(5, BigDecimal.ONE).stream().reduce((x, y) -> x.multiply(a, MathContext.DECIMAL32)).get(); 46 | System.out.println(c); 47 | 48 | 49 | 50 | // Amount a = new Amount(new BigDecimal(-20)); 51 | // Amount b = new Amount(new BigDecimal(-10)); 52 | // 53 | // Amount p = new Amount(new BigDecimal(3)); 54 | // Amount q = new Amount(new BigDecimal(-3)); 55 | // 56 | // RLOrder m = RLOrder.rateUnneeded(Direction.BUY, a, b); 57 | // RLOrder n = RLOrder.rateUnneeded(Direction.BUY, p, q); 58 | // 59 | // 60 | // List t = new ArrayList<>(); 61 | // t.add(m); 62 | // t.add(n); 63 | // List res = t 64 | // .stream() 65 | // .filter(e -> e.getQuantity().value().compareTo(BigDecimal.ONE) > 0) 66 | // .filter(e -> e.getTotalPrice().value().compareTo(BigDecimal.ONE) > 0) 67 | // .collect(Collectors.toList()); 68 | //// res.forEach(System.out::println); 69 | // System.out.println(res.isEmpty()); 70 | // 71 | ; 72 | 73 | 74 | 75 | 76 | 77 | // RLOrder buy = RLOrder.rateUnneeded(Direction.BUY, newAmt, totalPrice); 78 | 79 | 80 | 81 | // BigDecimal test = new BigDecimal("0.99"); 82 | // BigDecimal res = Collections.nCopies(1, test).stream().reduce((x, y) -> x.multiply(y, MathContext.DECIMAL64)).get(); 83 | // System.out.println(res); 84 | // int a = IntStream.range(1, 4). 85 | // 86 | // BigDecimal result = bdList.stream() 87 | // .reduce(BigDecimal.ZERO, BigDecimal::add); 88 | // System.out.println(a); 89 | 90 | // IntStream.range(1, 5).mapToObj(l -> { 91 | // int a = IntStream.range(1, l).reduce((x,y) -> x+y).getAsInt(); 92 | // return a; 93 | // 94 | // }).forEach(System.out::println); 95 | 96 | // IntStream a = IntStream.range(1, 5); 97 | // IntStream b = IntStream.range(90, 100); 98 | 99 | // 100 | // IntStream.concat(a, b).forEach(c -> { 101 | // System.out.println(c); 102 | // 103 | // }); 104 | 105 | // Observable d = Observable.range(1, 5); 106 | // Observable e = Observable.range(90, 110); 107 | 108 | // Observable.range(1, 5) 109 | // .mergeWith(Observable.range(90, 110)) 110 | // .subscribe(System.out::println); 111 | // 112 | // BigDecimal aaa = new BigDecimal("-2"); 113 | // System.out.println(aaa.compareTo(BigDecimal.ZERO) <= 0); 114 | 115 | // Bla bla = new Bla(); 116 | // System.out.println(bla.getClass().getName()); 117 | 118 | // Foo foo = new Foo(); 119 | // System.out.println(foo); 120 | 121 | } 122 | 123 | static class Bla { 124 | String a = "aaa"; 125 | 126 | @Override 127 | public String toString() { 128 | // TODO Auto-generated method stub 129 | return this.getClass().getName() + GsonUtils.toJson(this); 130 | } 131 | 132 | } 133 | 134 | static class Foo extends Bla { 135 | String c = "ccc"; 136 | 137 | public Foo() { 138 | super.a = "bbb"; 139 | } 140 | 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/utils/MyLogger.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.utils; 2 | 3 | import java.io.IOException; 4 | import java.text.SimpleDateFormat; 5 | import java.util.Date; 6 | import java.util.TimeZone; 7 | import java.util.logging.ConsoleHandler; 8 | import java.util.logging.FileHandler; 9 | import java.util.logging.Handler; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | import java.util.logging.SimpleFormatter; 13 | 14 | import com.mbcu.mmm.models.internal.Config; 15 | 16 | public class MyLogger { 17 | 18 | private static final String LOG_NAME = "log.%s.%s.txt"; 19 | private static final int limit = 1024 * 1024 * 20; // 20 Mb 20 | private static final int numLogFiles = 20; 21 | private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SS"); 22 | 23 | public static void setup(Config config, boolean... isConsole) throws IOException { 24 | 25 | // Get the global logger to configure it 26 | Logger logger = Logger.getLogger(""); 27 | logger.setLevel(Level.ALL); 28 | String timeStamp = sdf.format(new Date()); 29 | sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo")); 30 | 31 | String fileName = String.format(LOG_NAME, config.getCredentials().getAddress(), timeStamp); 32 | FileHandler fileHandler = new FileHandler(fileName, limit, numLogFiles, true); 33 | 34 | // Create txt Formatter 35 | SimpleFormatter formatter = new SimpleFormatter(); 36 | fileHandler.setFormatter(formatter); 37 | logger.addHandler(fileHandler); 38 | 39 | if (isConsole.length > 0) { 40 | if (isConsole[0]) { 41 | Handler consoleHandler = new ConsoleHandler(); 42 | consoleHandler.setLevel(Level.ALL); 43 | consoleHandler.setFormatter(formatter); 44 | logger.addHandler(consoleHandler); 45 | } 46 | } 47 | } 48 | 49 | public static Logger getLogger(String name) { 50 | return Logger.getLogger(name); 51 | } 52 | 53 | public static void exception(Logger log, String cause, Exception e) { 54 | StringBuffer res = new StringBuffer("EXCEPTION\n"); 55 | res.append("object-info start\n"); 56 | res.append(cause); 57 | res.append("\nobject-info end\ntrace start\n"); 58 | res.append(MyUtils.stackTrace(e)); 59 | res.append("\ntrace end"); 60 | log.log(Level.SEVERE, res.toString()); 61 | } 62 | } -------------------------------------------------------------------------------- /src/main/java/com/mbcu/mmm/utils/MyUtils.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.utils; 2 | 3 | import java.io.IOException; 4 | import java.io.PrintWriter; 5 | import java.io.StringWriter; 6 | import java.math.BigDecimal; 7 | import java.math.RoundingMode; 8 | import java.nio.charset.StandardCharsets; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.util.Arrays; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | public class MyUtils { 17 | 18 | private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@" 19 | + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"; 20 | private static Pattern pattern = Pattern.compile(EMAIL_PATTERN); 21 | 22 | public static String readFile(String path) throws IOException { 23 | byte[] encoded = Files.readAllBytes(Paths.get(path)); 24 | return new String(encoded, StandardCharsets.UTF_8); 25 | } 26 | 27 | public static String stackTrace(Throwable t) { 28 | Exception e = (Exception) t; 29 | StringWriter sw = new StringWriter(); 30 | PrintWriter pw = new PrintWriter(sw); 31 | e.printStackTrace(pw); 32 | String sStackTrace = sw.toString(); // stack trace as a string 33 | // System.out.println(sStackTrace); 34 | pw.close(); 35 | try { 36 | sw.close(); 37 | } catch (IOException e1) { 38 | e1.printStackTrace(); 39 | } 40 | return sStackTrace; 41 | } 42 | 43 | public static void toFile(String string, Path path) { 44 | byte[] strToBytes = string.getBytes(); 45 | try { 46 | Files.write(path, strToBytes); 47 | } catch (IOException e) { 48 | e.printStackTrace(); 49 | } 50 | } 51 | 52 | public static boolean isEmail(String test) { 53 | Matcher matcher = pattern.matcher(test); 54 | return matcher.matches(); 55 | } 56 | 57 | public static > boolean isInEnum(String value, Class enumClass) { 58 | return Arrays.stream(enumClass.getEnumConstants()).anyMatch(e -> e.name().equalsIgnoreCase(value)); 59 | } 60 | 61 | 62 | private static final BigDecimal SQRT_DIG = new BigDecimal(150); 63 | private static final BigDecimal SQRT_PRE = new BigDecimal(10).pow(SQRT_DIG.intValue()); 64 | 65 | /** 66 | * Private utility method used to compute the square root of a BigDecimal. 67 | * 68 | * @author Luciano Culacciatti 69 | * @url http://www.codeproject.com/Tips/257031/Implementing-SqrtRoot-in-BigDecimal 70 | */ 71 | private static BigDecimal sqrtNewtonRaphson (BigDecimal c, BigDecimal xn, BigDecimal precision){ 72 | BigDecimal fx = xn.pow(2).add(c.negate()); 73 | BigDecimal fpx = xn.multiply(new BigDecimal(2)); 74 | BigDecimal xn1 = fx.divide(fpx,2*SQRT_DIG.intValue(),RoundingMode.HALF_DOWN); 75 | xn1 = xn.add(xn1.negate()); 76 | BigDecimal currentSquare = xn1.pow(2); 77 | BigDecimal currentPrecision = currentSquare.subtract(c); 78 | currentPrecision = currentPrecision.abs(); 79 | if (currentPrecision.compareTo(precision) <= -1){ 80 | return xn1; 81 | } 82 | return sqrtNewtonRaphson(c, xn1, precision); 83 | } 84 | 85 | /** 86 | * Uses Newton Raphson to compute the square root of a BigDecimal. 87 | * 88 | * @author Luciano Culacciatti 89 | * @url http://www.codeproject.com/Tips/257031/Implementing-SqrtRoot-in-BigDecimal 90 | */ 91 | public static BigDecimal bigSqrt(BigDecimal c){ 92 | return sqrtNewtonRaphson(c,new BigDecimal(1),new BigDecimal(1).divide(SQRT_PRE)); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | Archetype Created Web Application 13 | 14 | -------------------------------------------------------------------------------- /src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Hello World!

4 | 5 | 6 | -------------------------------------------------------------------------------- /src/test/java/com/mbcu/mmm/sequences/NotifierTest.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertFalse; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import org.junit.Test; 10 | 11 | import com.mbcu.mmm.sequences.Emailer.SendEmailBotError; 12 | 13 | public class NotifierTest { 14 | 15 | @Test 16 | public void testRequestEmailNotice() { 17 | long tsa = System.currentTimeMillis(); 18 | Emailer.SendEmailBotError a = new SendEmailBotError("aaa", "XRP/JPY", tsa, false); 19 | Emailer.SendEmailBotError b = new SendEmailBotError("aaa", "XRP/JPY", tsa, false); 20 | 21 | assertEquals(a, b); 22 | Map map = new HashMap<>(); 23 | map.put(a, 1L); 24 | map.put(b, 2l); 25 | assertEquals(map.size(), 1); 26 | 27 | Emailer.SendEmailBotError c = new SendEmailBotError("cc", "XRP/JPY", tsa, false); 28 | 29 | assertFalse(a.equals(c)); 30 | map.clear(); 31 | map.put(a, 1L); 32 | map.put(c, 1L); 33 | assertEquals(map.size(), 2); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/mbcu/mmm/sequences/counters/YukiTest.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.sequences.counters; 2 | 3 | import java.io.IOException; 4 | import java.math.BigDecimal; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | 12 | import com.mbcu.mmm.models.internal.BotConfig; 13 | import com.mbcu.mmm.models.internal.Config; 14 | import com.mbcu.mmm.models.internal.RLOrder; 15 | import com.mbcu.mmm.models.internal.RLOrder.Direction; 16 | import com.mbcu.mmm.sequences.Common; 17 | import com.ripple.core.coretypes.AccountID; 18 | import com.ripple.core.coretypes.Amount; 19 | import com.ripple.core.coretypes.Currency; 20 | 21 | public class YukiTest { 22 | private static final String configPath = "config.txt"; 23 | private Yuki yuki; 24 | private Common c; 25 | 26 | @Before 27 | public void init() throws IOException { 28 | c = Common.newInstance(Config.build(configPath)); 29 | yuki = Yuki.newInstance(Config.build(configPath)); 30 | } 31 | 32 | /* 33 | * 2017-07-12, 10:54:30 Made offer to buy 2 JPY.TokyoJPY at price 1.00579 34 | * JPY.MrRipple bought 0.693638 JPY.TokyoJPY at price 0.050171 XRP bought 35 | * 0.100348 XRP at price 19.8 JPY.MrRipple bought 1.30636 JPY.TokyoJPY at 36 | * price 0.050175 XRP 37 | * 38 | * buy quantity:-0.69363877168/JPY/r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN 39 | * totalPrice:-0.6890598/JPY/rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS 40 | * rate:0.9933744005069883 buy 41 | * quantity:-1.30636122832/JPY/r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN 42 | * totalPrice:-1.2978306/JPY/rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS 43 | * rate:0.9934771700953337 44 | * 45 | * for "pair": 46 | * "JPY.rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS/JPY.r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN", 47 | * "gridSpace": 0.0005, matches buy 48 | * quantity:0.6890598/JPY/rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS 49 | * totalPrice:0.69329424178/JPY/r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN 50 | * rate:1.006145245710169131909886485904416 51 | * pair:JPY.rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS/JPY. 52 | * r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN 53 | * 54 | * buy quantity:1.2978306/JPY/rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS 55 | * totalPrice:1.30571231302/JPY/r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN 56 | * rate:1.006072990589064551259617395367315 57 | * pair:JPY.rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS/JPY. 58 | * r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN 59 | * 60 | * for "pair": 61 | * "JPY.r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN/JPY.rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS", 62 | * "gridSpace": 0.0005 sell 63 | * quantity:0.69363877168/JPY/r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN 64 | * totalPrice:0.68940661938584/JPY/rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS 65 | * rate:0.9938986220682132789743019862098721 66 | * pair:JPY.rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS/JPY. 67 | * r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN 68 | * 69 | * sell quantity:1.30636122832/JPY/r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN 70 | * totalPrice:1.29848378061416/JPY/rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS 71 | * rate:0.9939699314897989470361595399005741 72 | * pair:JPY.rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS/JPY. 73 | * r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN 74 | * 75 | */ 76 | 77 | // @Test 78 | // public void testAutobridgeArbitrage(){ 79 | // Amount q1 = RLOrder.amount(new BigDecimal("-0.69363877168"), 80 | // Currency.fromString("JPY"), 81 | // AccountID.fromAddress("r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN")); 82 | // Amount t1 = RLOrder.amount(new BigDecimal("-0.6890598"), 83 | // Currency.fromString("JPY"), 84 | // AccountID.fromAddress("rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS")); 85 | // Amount q2 = RLOrder.amount(new BigDecimal("-1.30636122832"), 86 | // Currency.fromString("JPY"), 87 | // AccountID.fromAddress("r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN")); 88 | // Amount t2 = RLOrder.amount(new BigDecimal("-1.2978306"), 89 | // Currency.fromString("JPY"), 90 | // AccountID.fromAddress("rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS")); 91 | // RLOrder r1 = RLOrder.rateUnneeded(Direction.BUY, q1, t1); 92 | // RLOrder r2 = RLOrder.rateUnneeded(Direction.BUY, q2, t2); 93 | // 94 | // List list = new ArrayList<>(); 95 | // list.add(r1); 96 | // list.add(r2); 97 | // list.forEach(oe -> { 98 | // 99 | // System.out.println(yuki.buildCounter(oe).stringify()); 100 | // }); 101 | // 102 | // 103 | // 104 | // } 105 | // 106 | 107 | // @Test 108 | // public void testORConsume(){ 109 | //// String consumeOneHalf = 110 | // "{\"engine_result\":\"tesSUCCESS\",\"engine_result_code\":0,\"engine_result_message\":\"The 111 | // transaction was applied. Only final in a validated 112 | // ledger.\",\"ledger_hash\":\"A53C42A135396D1C9B7708E96977CB71454F4AE6C7900D0CF30B62AEC3FE5851\",\"ledger_index\":30854512,\"meta\":{\"AffectedNodes\":[{\"DeletedNode\":{\"FinalFields\":{\"ExchangeRate\":\"500A7AB7B21D6FCE\",\"Flags\":0,\"RootIndex\":\"11EEBF7DFC0076D299322039F1493C921CAAEFE85B322E5D500A7AB7B21D6FCE\",\"TakerGetsCurrency\":\"0000000000000000000000000000000000000000\",\"TakerGetsIssuer\":\"0000000000000000000000000000000000000000\",\"TakerPaysCurrency\":\"0000000000000000000000004A50590000000000\",\"TakerPaysIssuer\":\"6F2531F2B8CDB96D6D986D9D75CC0156DF2C5387\"},\"LedgerEntryType\":\"DirectoryNode\",\"LedgerIndex\":\"11EEBF7DFC0076D299322039F1493C921CAAEFE85B322E5D500A7AB7B21D6FCE\"}},{\"ModifiedNode\":{\"FinalFields\":{\"Balance\":{\"currency\":\"JPY\",\"issuer\":\"rrrrrrrrrrrrrrrrrrrrBZbvji\",\"value\":\"25493.59330038829\"},\"Flags\":1114112,\"HighLimit\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"0\"},\"HighNode\":\"0000000000000399\",\"LowLimit\":{\"currency\":\"JPY\",\"issuer\":\"raNDu1gNyZ5hipBTKxm5zx7NovA1rNnNRf\",\"value\":\"100000\"},\"LowNode\":\"0000000000000001\"},\"LedgerEntryType\":\"RippleState\",\"LedgerIndex\":\"21110D01A36A7EB20188B11D685B847322DE631145C31803E1760550FA399BAD\",\"PreviousFields\":{\"Balance\":{\"currency\":\"JPY\",\"issuer\":\"rrrrrrrrrrrrrrrrrrrrBZbvji\",\"value\":\"25552.70487260873\"}},\"PreviousTxnID\":\"D3A84F7F810AC01AE88B7C120A01DB5694DE4775AE3DBE2613C849EBBA75C6F3\",\"PreviousTxnLgrSeq\":30854492}},{\"ModifiedNode\":{\"FinalFields\":{\"Flags\":0,\"IndexPrevious\":\"0000000000001044\",\"Owner\":\"rLjDNH9g1AajRT2pxALZmwesd64p4x9XZJ\",\"RootIndex\":\"718BBB28EE36A3AC1DCBC75790357E2057015302DBA637DFD7851E820B5583BA\"},\"LedgerEntryType\":\"DirectoryNode\",\"LedgerIndex\":\"3B2BE34F74E8E10113F2D4C84053344F678D64E12D537D8D931D8981DF3AC7F4\"}},{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"raNDu1gNyZ5hipBTKxm5zx7NovA1rNnNRf\",\"Balance\":\"2517023578\",\"Flags\":0,\"OwnerCount\":9,\"Sequence\":5451},\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"72A59CDF8FFBF65C20D01D3A3D5DA5BAE3158A3881E7AEE525A01B1CC73D32DD\",\"PreviousFields\":{\"Balance\":\"2515023698\",\"Sequence\":5450},\"PreviousTxnID\":\"D3A84F7F810AC01AE88B7C120A01DB5694DE4775AE3DBE2613C849EBBA75C6F3\",\"PreviousTxnLgrSeq\":30854492}},{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"rLjDNH9g1AajRT2pxALZmwesd64p4x9XZJ\",\"Balance\":\"1686510443008\",\"Flags\":0,\"OwnerCount\":2546,\"Sequence\":229780},\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"7F28475BBAE8E0654C4D4C876DFBF7A3C40DEFEB6DF2F4C301F7F7F3514F255F\",\"PreviousFields\":{\"Balance\":\"1686512443008\",\"OwnerCount\":2547},\"PreviousTxnID\":\"9563022137D4F43F080FE59F74847D41C4C3222DB4FC0A70560AFDC33771712A\",\"PreviousTxnLgrSeq\":30854493}},{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"rLjDNH9g1AajRT2pxALZmwesd64p4x9XZJ\",\"BookDirectory\":\"11EEBF7DFC0076D299322039F1493C921CAAEFE85B322E5D500A7AB7C76B2D09\",\"BookNode\":\"0000000000000000\",\"Flags\":131072,\"OwnerNode\":\"0000000000001045\",\"Sequence\":229778,\"TakerGets\":\"7885940\",\"TakerPays\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"232.6099563824587\"}},\"LedgerEntryType\":\"Offer\",\"LedgerIndex\":\"83F71EE4B60B9016D25138FCE5D0D1E8DEF8C794940B42B53C8082516F1BEF40\",\"PreviousFields\":{\"TakerGets\":\"8442902\",\"TakerPays\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"249.0385503771742\"}},\"PreviousTxnID\":\"5A2B58D139CDEADCD4B1F6DDE2CB766E5C9BECCD893AEB6747558D76E6586785\",\"PreviousTxnLgrSeq\":30854493}},{\"DeletedNode\":{\"FinalFields\":{\"Account\":\"rLjDNH9g1AajRT2pxALZmwesd64p4x9XZJ\",\"BookDirectory\":\"11EEBF7DFC0076D299322039F1493C921CAAEFE85B322E5D500A7AB7B21D6FCE\",\"BookNode\":\"0000000000000000\",\"Flags\":131072,\"OwnerNode\":\"0000000000001045\",\"PreviousTxnID\":\"D3A84F7F810AC01AE88B7C120A01DB5694DE4775AE3DBE2613C849EBBA75C6F3\",\"PreviousTxnLgrSeq\":30854492,\"Sequence\":229774,\"TakerGets\":\"0\",\"TakerPays\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"0\"}},\"LedgerEntryType\":\"Offer\",\"LedgerIndex\":\"85794675649594FC8F8595FB5BB02342ABA3D5801DA543C13BCE89558BA3021C\",\"PreviousFields\":{\"TakerGets\":\"1443038\",\"TakerPays\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"42.56499105563746\"}}}},{\"ModifiedNode\":{\"FinalFields\":{\"Balance\":{\"currency\":\"JPY\",\"issuer\":\"rrrrrrrrrrrrrrrrrrrrBZbvji\",\"value\":\"-25536929.46912782\"},\"Flags\":2228224,\"HighLimit\":{\"currency\":\"JPY\",\"issuer\":\"rLjDNH9g1AajRT2pxALZmwesd64p4x9XZJ\",\"value\":\"10000000000\"},\"HighNode\":\"0000000000000000\",\"HighQualityIn\":1000000000,\"LowLimit\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"0\"},\"LowNode\":\"0000000000000001\"},\"LedgerEntryType\":\"RippleState\",\"LedgerIndex\":\"FB9399D7485EA21D73E649E9F821B8C10A1008285E32107B6F30F934723EB5C8\",\"PreviousFields\":{\"Balance\":{\"currency\":\"JPY\",\"issuer\":\"rrrrrrrrrrrrrrrrrrrrBZbvji\",\"value\":\"-25536870.47554278\"}},\"PreviousTxnID\":\"D3A84F7F810AC01AE88B7C120A01DB5694DE4775AE3DBE2613C849EBBA75C6F3\",\"PreviousTxnLgrSeq\":30854492}}],\"TransactionIndex\":4,\"TransactionResult\":\"tesSUCCESS\"},\"status\":\"closed\",\"transaction\":{\"Account\":\"raNDu1gNyZ5hipBTKxm5zx7NovA1rNnNRf\",\"Fee\":\"120\",\"Flags\":2147483648,\"LastLedgerSequence\":30854514,\"Memos\":[{\"Memo\":{\"MemoData\":\"726D2D312E322E33\",\"MemoType\":\"636C69656E74\"}}],\"Sequence\":5450,\"SigningPubKey\":\"02E2F1208D1715E18B0957FC819546FA7434B4A19EE38321932D2ED28FA090678E\",\"TakerGets\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"60\"},\"TakerPays\":\"2000000\",\"TransactionType\":\"OfferCreate\",\"TxnSignature\":\"3045022100EE1BC622B0768D2F01515AE8BC8157EA3EF361BB65C9E680C44CE7C87066CE4402207594042DD3E0FD7C8D0B59B84C96ECD0BF8780CB85B893FD5626F877CB3FB6F5\",\"date\":552025652,\"hash\":\"024C1B17CE1C35425550B6CC5676230B488C5680CADE21D226F8EA2D3594204D\",\"owner_funds\":\"25493.59330038829\"},\"type\":\"transaction\",\"validated\":true}\r\n"; 113 | //// c.filterStream2(consumeOneHalf); 114 | // String consumeOneHalf2 = 115 | // "{\"engine_result\":\"tesSUCCESS\",\"engine_result_code\":0,\"engine_result_message\":\"The 116 | // transaction was applied. Only final in a validated 117 | // ledger.\",\"ledger_hash\":\"A53C42A135396D1C9B7708E96977CB71454F4AE6C7900D0CF30B62AEC3FE5851\",\"ledger_index\":30854512,\"meta\":{\"AffectedNodes\":[{\"DeletedNode\":{\"FinalFields\":{\"ExchangeRate\":\"500A7AB7B21D6FCE\",\"Flags\":0,\"RootIndex\":\"11EEBF7DFC0076D299322039F1493C921CAAEFE85B322E5D500A7AB7B21D6FCE\",\"TakerGetsCurrency\":\"0000000000000000000000000000000000000000\",\"TakerGetsIssuer\":\"0000000000000000000000000000000000000000\",\"TakerPaysCurrency\":\"0000000000000000000000004A50590000000000\",\"TakerPaysIssuer\":\"6F2531F2B8CDB96D6D986D9D75CC0156DF2C5387\"},\"LedgerEntryType\":\"DirectoryNode\",\"LedgerIndex\":\"11EEBF7DFC0076D299322039F1493C921CAAEFE85B322E5D500A7AB7B21D6FCE\"}},{\"ModifiedNode\":{\"FinalFields\":{\"Balance\":{\"currency\":\"JPY\",\"issuer\":\"rrrrrrrrrrrrrrrrrrrrBZbvji\",\"value\":\"25493.59330038829\"},\"Flags\":1114112,\"HighLimit\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"0\"},\"HighNode\":\"0000000000000399\",\"LowLimit\":{\"currency\":\"JPY\",\"issuer\":\"raNDu1gNyZ5hipBTKxm5zx7NovA1rNnNRf\",\"value\":\"100000\"},\"LowNode\":\"0000000000000001\"},\"LedgerEntryType\":\"RippleState\",\"LedgerIndex\":\"21110D01A36A7EB20188B11D685B847322DE631145C31803E1760550FA399BAD\",\"PreviousFields\":{\"Balance\":{\"currency\":\"JPY\",\"issuer\":\"rrrrrrrrrrrrrrrrrrrrBZbvji\",\"value\":\"25552.70487260873\"}},\"PreviousTxnID\":\"D3A84F7F810AC01AE88B7C120A01DB5694DE4775AE3DBE2613C849EBBA75C6F3\",\"PreviousTxnLgrSeq\":30854492}},{\"ModifiedNode\":{\"FinalFields\":{\"Flags\":0,\"IndexPrevious\":\"0000000000001044\",\"Owner\":\"rLjDNH9g1AajRT2pxALZmwesd64p4x9XZJ\",\"RootIndex\":\"718BBB28EE36A3AC1DCBC75790357E2057015302DBA637DFD7851E820B5583BA\"},\"LedgerEntryType\":\"DirectoryNode\",\"LedgerIndex\":\"3B2BE34F74E8E10113F2D4C84053344F678D64E12D537D8D931D8981DF3AC7F4\"}},{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"raNDu1gNyZ5hipBTKxm5zx7NovA1rNnNRf\",\"Balance\":\"2517023578\",\"Flags\":0,\"OwnerCount\":9,\"Sequence\":5451},\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"72A59CDF8FFBF65C20D01D3A3D5DA5BAE3158A3881E7AEE525A01B1CC73D32DD\",\"PreviousFields\":{\"Balance\":\"2515023698\",\"Sequence\":5450},\"PreviousTxnID\":\"D3A84F7F810AC01AE88B7C120A01DB5694DE4775AE3DBE2613C849EBBA75C6F3\",\"PreviousTxnLgrSeq\":30854492}},{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"rLjDNH9g1AajRT2pxALZmwesd64p4x9XZJ\",\"Balance\":\"1686510443008\",\"Flags\":0,\"OwnerCount\":2546,\"Sequence\":229780},\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"7F28475BBAE8E0654C4D4C876DFBF7A3C40DEFEB6DF2F4C301F7F7F3514F255F\",\"PreviousFields\":{\"Balance\":\"1686512443008\",\"OwnerCount\":2547},\"PreviousTxnID\":\"9563022137D4F43F080FE59F74847D41C4C3222DB4FC0A70560AFDC33771712A\",\"PreviousTxnLgrSeq\":30854493}},{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"rLjDNH9g1AajRT2pxALZmwesd64p4x9XZJ\",\"BookDirectory\":\"11EEBF7DFC0076D299322039F1493C921CAAEFE85B322E5D500A7AB7C76B2D09\",\"BookNode\":\"0000000000000000\",\"Flags\":131072,\"OwnerNode\":\"0000000000001045\",\"Sequence\":229778,\"TakerGets\":\"7885940\",\"TakerPays\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"232.6099563824587\"}},\"LedgerEntryType\":\"Offer\",\"LedgerIndex\":\"83F71EE4B60B9016D25138FCE5D0D1E8DEF8C794940B42B53C8082516F1BEF40\",\"PreviousFields\":{\"TakerGets\":\"8442902\",\"TakerPays\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"249.0385503771742\"}},\"PreviousTxnID\":\"5A2B58D139CDEADCD4B1F6DDE2CB766E5C9BECCD893AEB6747558D76E6586785\",\"PreviousTxnLgrSeq\":30854493}},{\"DeletedNode\":{\"FinalFields\":{\"Account\":\"rLjDNH9g1AajRT2pxALZmwesd64p4x9XZJ\",\"BookDirectory\":\"11EEBF7DFC0076D299322039F1493C921CAAEFE85B322E5D500A7AB7B21D6FCE\",\"BookNode\":\"0000000000000000\",\"Flags\":131072,\"OwnerNode\":\"0000000000001045\",\"PreviousTxnID\":\"D3A84F7F810AC01AE88B7C120A01DB5694DE4775AE3DBE2613C849EBBA75C6F3\",\"PreviousTxnLgrSeq\":30854492,\"Sequence\":229774,\"TakerGets\":\"0\",\"TakerPays\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"0\"}},\"LedgerEntryType\":\"Offer\",\"LedgerIndex\":\"85794675649594FC8F8595FB5BB02342ABA3D5801DA543C13BCE89558BA3021C\",\"PreviousFields\":{\"TakerGets\":\"1443038\",\"TakerPays\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"42.56499105563746\"}}}},{\"ModifiedNode\":{\"FinalFields\":{\"Balance\":{\"currency\":\"JPY\",\"issuer\":\"rrrrrrrrrrrrrrrrrrrrBZbvji\",\"value\":\"-25536929.46912782\"},\"Flags\":2228224,\"HighLimit\":{\"currency\":\"JPY\",\"issuer\":\"rLjDNH9g1AajRT2pxALZmwesd64p4x9XZJ\",\"value\":\"10000000000\"},\"HighNode\":\"0000000000000000\",\"HighQualityIn\":1000000000,\"LowLimit\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"0\"},\"LowNode\":\"0000000000000001\"},\"LedgerEntryType\":\"RippleState\",\"LedgerIndex\":\"FB9399D7485EA21D73E649E9F821B8C10A1008285E32107B6F30F934723EB5C8\",\"PreviousFields\":{\"Balance\":{\"currency\":\"JPY\",\"issuer\":\"rrrrrrrrrrrrrrrrrrrrBZbvji\",\"value\":\"-25536870.47554278\"}},\"PreviousTxnID\":\"D3A84F7F810AC01AE88B7C120A01DB5694DE4775AE3DBE2613C849EBBA75C6F3\",\"PreviousTxnLgrSeq\":30854492}}],\"TransactionIndex\":4,\"TransactionResult\":\"tesSUCCESS\"},\"status\":\"closed\",\"transaction\":{\"Account\":\"raNDu1gNyZ5hipBTKxm5zx7NovA1rNnNRf\",\"Fee\":\"120\",\"Flags\":2147483648,\"LastLedgerSequence\":30854514,\"Memos\":[{\"Memo\":{\"MemoData\":\"726D2D312E322E33\",\"MemoType\":\"636C69656E74\"}}],\"Sequence\":5450,\"SigningPubKey\":\"02E2F1208D1715E18B0957FC819546FA7434B4A19EE38321932D2ED28FA090678E\",\"TakerGets\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"60\"},\"TakerPays\":\"2000000\",\"TransactionType\":\"OfferCreate\",\"TxnSignature\":\"3045022100EE1BC622B0768D2F01515AE8BC8157EA3EF361BB65C9E680C44CE7C87066CE4402207594042DD3E0FD7C8D0B59B84C96ECD0BF8780CB85B893FD5626F877CB3FB6F5\",\"date\":552025652,\"hash\":\"024C1B17CE1C35425550B6CC5676230B488C5680CADE21D226F8EA2D3594204D\",\"owner_funds\":\"25493.59330038829\"},\"type\":\"transaction\",\"validated\":true}\r\n"; 118 | // c.filterStream2(consumeOneHalf2); 119 | // } 120 | 121 | @Test 122 | public void testORConsumed() { 123 | 124 | /* 125 | * { "pair": "XRP/JPY.rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS", 126 | * "startMiddlePrice": 20.3, "gridSpace": 0.1, "buyGridLevels": 10, 127 | * "sellGridLevels": 10, "buyOrderQuantity": 2, "sellOrderQuantity": 2 } 128 | * 129 | */ 130 | 131 | /* 132 | * percentToCounter 50 133 | */ 134 | String consumedHalf = "{\"engine_result\":\"tesSUCCESS\",\"engine_result_code\":0,\"engine_result_message\":\"The transaction was applied. Only final in a validated ledger.\",\"ledger_hash\":\"23B2DF6090F92CF440EE8F3B9A57BC9855DBC7FF8D1EF3DA6ADEAC0690C3EF51\",\"ledger_index\":30856627,\"meta\":{\"AffectedNodes\":[{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"raNDu1gNyZ5hipBTKxm5zx7NovA1rNnNRf\",\"BookDirectory\":\"A7A2258942BF79A1C2A55DA88560879DC312966AFDD1CD05590C03BC0A6F7FDE\",\"BookNode\":\"0000000000000000\",\"Flags\":0,\"OwnerNode\":\"0000000000000018\",\"Sequence\":5456,\"TakerGets\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"88.71\"},\"TakerPays\":\"3000000\"},\"LedgerEntryType\":\"Offer\",\"LedgerIndex\":\"056871F43F37D6B924F7D35F4D2C0DF6D1E02A71E638EA5D454F5211FBC21420\",\"PreviousFields\":{\"TakerGets\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"147.85\"},\"TakerPays\":\"5000000\"},\"PreviousTxnID\":\"677CA05F654AE8A07936A7205C6925746CB00A6B615105639C428857096963B3\",\"PreviousTxnLgrSeq\":30856614}},{\"ModifiedNode\":{\"FinalFields\":{\"Balance\":{\"currency\":\"JPY\",\"issuer\":\"rrrrrrrrrrrrrrrrrrrrBZbvji\",\"value\":\"24903.01942038829\"},\"Flags\":1114112,\"HighLimit\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"0\"},\"HighNode\":\"0000000000000399\",\"LowLimit\":{\"currency\":\"JPY\",\"issuer\":\"raNDu1gNyZ5hipBTKxm5zx7NovA1rNnNRf\",\"value\":\"100000\"},\"LowNode\":\"0000000000000001\"},\"LedgerEntryType\":\"RippleState\",\"LedgerIndex\":\"21110D01A36A7EB20188B11D685B847322DE631145C31803E1760550FA399BAD\",\"PreviousFields\":{\"Balance\":{\"currency\":\"JPY\",\"issuer\":\"rrrrrrrrrrrrrrrrrrrrBZbvji\",\"value\":\"24962.27770038829\"}},\"PreviousTxnID\":\"7FE4EDDC776B3ECC777444F5DF7CCB3AC7CC6C105B935BC72A63993DD9C86347\",\"PreviousTxnLgrSeq\":30856584}},{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"raNDu1gNyZ5hipBTKxm5zx7NovA1rNnNRf\",\"Balance\":\"2537023506\",\"Flags\":0,\"OwnerCount\":12,\"Sequence\":5457},\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"72A59CDF8FFBF65C20D01D3A3D5DA5BAE3158A3881E7AEE525A01B1CC73D32DD\",\"PreviousFields\":{\"Balance\":\"2535023506\"},\"PreviousTxnID\":\"677CA05F654AE8A07936A7205C6925746CB00A6B615105639C428857096963B3\",\"PreviousTxnLgrSeq\":30856614}},{\"ModifiedNode\":{\"FinalFields\":{\"Balance\":{\"currency\":\"JPY\",\"issuer\":\"rrrrrrrrrrrrrrrrrrrrBZbvji\",\"value\":\"5226.319164203065\"},\"Flags\":1114112,\"HighLimit\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"0\"},\"HighNode\":\"00000000000001BC\",\"LowLimit\":{\"currency\":\"JPY\",\"issuer\":\"rHMjZANhquizS74FutV3CNoWfQcoVkBxf\",\"value\":\"10000000000\"},\"LowNode\":\"0000000000000000\"},\"LedgerEntryType\":\"RippleState\",\"LedgerIndex\":\"9E03D12B7281B41A487F01494BCE1A7099624E50E4FEF5ACE11B2D5BB3C353FB\",\"PreviousFields\":{\"Balance\":{\"currency\":\"JPY\",\"issuer\":\"rrrrrrrrrrrrrrrrrrrrBZbvji\",\"value\":\"5167.179164203065\"}},\"PreviousTxnID\":\"7FE4EDDC776B3ECC777444F5DF7CCB3AC7CC6C105B935BC72A63993DD9C86347\",\"PreviousTxnLgrSeq\":30856584}},{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"rHMjZANhquizS74FutV3CNoWfQcoVkBxf\",\"Balance\":\"3419983825\",\"Flags\":0,\"OwnerCount\":2,\"Sequence\":43},\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"E828FC718630DEC1E542D4D2A50074CDB7B3183062FBC59D6E88B63198BF27BF\",\"PreviousFields\":{\"Balance\":\"3421983837\",\"Sequence\":42},\"PreviousTxnID\":\"7FE4EDDC776B3ECC777444F5DF7CCB3AC7CC6C105B935BC72A63993DD9C86347\",\"PreviousTxnLgrSeq\":30856584}}],\"TransactionIndex\":32,\"TransactionResult\":\"tesSUCCESS\"},\"status\":\"closed\",\"transaction\":{\"Account\":\"rHMjZANhquizS74FutV3CNoWfQcoVkBxf\",\"Fee\":\"12\",\"Flags\":2148007936,\"LastLedgerSequence\":30856629,\"Memos\":[{\"Memo\":{\"MemoData\":\"726D2D312E322E33\",\"MemoType\":\"636C69656E74\"}}],\"Sequence\":42,\"SigningPubKey\":\"03E8BBF8387DCDCE12F8B8F3CAC425D2587548E6510659E27F74E02EC7FE1DAE0E\",\"TakerGets\":\"2000000\",\"TakerPays\":{\"currency\":\"JPY\",\"issuer\":\"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\"value\":\"59.14\"},\"TransactionType\":\"OfferCreate\",\"TxnSignature\":\"3044022045BE95C9BC4AEDABF74AA052E72394819A1B9D7374E7FDD0F92C35F438290E45022064DF94A41EBCAFCCE246E27EE336A97D669CA1D26085D4E1B2D2940E394AD6D6\",\"date\":552033351,\"hash\":\"FC1649A761A589BAE43B22657B1A6A41942818E92EB7715C98A3B382DAED9879\",\"owner_funds\":\"3389983825\"},\"type\":\"transaction\",\"validated\":true}\r\n"; 135 | c.filterStream2(consumedHalf, "rHMjZANhquizS74FutV3CNoWfQcoVkBxf"); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/test/java/com/mbcu/mmm/utils/UtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.mbcu.mmm.utils; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import java.io.IOException; 7 | import java.math.BigDecimal; 8 | import java.math.MathContext; 9 | import java.math.RoundingMode; 10 | import java.time.ZoneOffset; 11 | import java.time.ZonedDateTime; 12 | import java.util.Arrays; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.concurrent.atomic.AtomicBoolean; 17 | import java.util.stream.Collectors; 18 | 19 | import static org.junit.Assert.assertNotEquals; 20 | import static org.junit.Assert.assertNotNull; 21 | import static org.junit.Assert.assertNull; 22 | 23 | import org.json.JSONException; 24 | import org.json.JSONObject; 25 | import org.junit.Before; 26 | import org.junit.Test; 27 | 28 | import com.mbcu.mmm.main.WebSocketClient; 29 | import com.mbcu.mmm.models.internal.Config; 30 | import com.mbcu.mmm.models.internal.NameIssuer; 31 | import com.mbcu.mmm.models.internal.RLOrder; 32 | import com.mbcu.mmm.models.internal.RLOrder.Direction; 33 | import com.mbcu.mmm.models.request.AccountInfo; 34 | import com.mbcu.mmm.rx.RxBus; 35 | import com.mbcu.mmm.rx.RxBusProvider; 36 | import com.mbcu.mmm.sequences.Common; 37 | import com.mbcu.mmm.sequences.counters.Yuki; 38 | import com.ripple.core.coretypes.AccountID; 39 | import com.ripple.core.coretypes.Amount; 40 | import com.ripple.core.coretypes.Currency; 41 | import com.ripple.core.coretypes.uint.UInt32; 42 | import com.ripple.core.types.known.tx.Transaction; 43 | import com.ripple.core.types.known.tx.signed.SignedTransaction; 44 | import com.ripple.core.types.known.tx.txns.OfferCreate; 45 | 46 | import io.reactivex.schedulers.Schedulers; 47 | import io.reactivex.subjects.PublishSubject; 48 | import io.reactivex.subjects.Subject; 49 | 50 | public class UtilsTest { 51 | 52 | private final AtomicBoolean flagWaitSeq1 = new AtomicBoolean(true); 53 | private final AtomicBoolean flagWaitSeq2 = new AtomicBoolean(false); 54 | 55 | private Subject seqSyncObs = PublishSubject.create(); 56 | private static final String configPath = "config.txt"; 57 | private Config config; 58 | 59 | @Before 60 | public void init() throws IOException { 61 | config = Config.build(configPath); 62 | } 63 | 64 | @Test 65 | public void testMinAmount() { 66 | BigDecimal value = new BigDecimal("0.0061864"); 67 | Amount a = RLOrder.amount(value, Currency.XRP, null); 68 | assertEquals("0.006186", a.valueText()); 69 | } 70 | 71 | @Test 72 | public void testMinRLOrder() { 73 | BigDecimal a = BigDecimal.ONE.divide(new BigDecimal("5.4E-15"), MathContext.DECIMAL64); 74 | System.out.println("aaa" + a.toPlainString()); 75 | } 76 | 77 | @Test 78 | public void signTest() { 79 | int nowLedger = 32953426; 80 | OfferCreate offerCreate = new OfferCreate(); 81 | Amount amount = new Amount(new BigDecimal("20"), Currency.fromString("JPY"), 82 | AccountID.fromAddress("rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS")); 83 | offerCreate.takerGets(amount); 84 | offerCreate.takerPays(new Amount(new BigDecimal("1"))); 85 | 86 | offerCreate.sequence(new UInt32(String.valueOf("32441"))); 87 | offerCreate.fee(new Amount(new BigDecimal("0.00012"))); 88 | // offerCreate.maxFee(new Amount(new BigDecimal("0.001"))); 89 | offerCreate.lastLedgerSequence(new UInt32(String.valueOf(nowLedger + 5))); 90 | offerCreate.account(AccountID.fromAddress(config.getCredentials().getAddress())); 91 | System.out.println(offerCreate.prettyJSON()); 92 | SignedTransaction signed = offerCreate.sign(config.getCredentials().getSecret()); 93 | System.out.println(signed.tx_blob); 94 | } 95 | 96 | @Test 97 | public void splitTest() { 98 | String a = "111, 999"; 99 | String[] els = a.split("-"); 100 | assertEquals(a, els[els.length - 1]); 101 | assertEquals(els.length, 1); 102 | 103 | } 104 | 105 | @Test 106 | public void nameIssuerMap() { 107 | Map map = new HashMap<>(); 108 | 109 | Amount a1 = new Amount(Currency.fromString("ABC"), AccountID.fromAddress("rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS")); 110 | NameIssuer a = NameIssuer.from(a1); 111 | Amount b1 = new Amount(Currency.fromString("ABC"), AccountID.fromAddress("rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS")); 112 | NameIssuer b = NameIssuer.from(b1); 113 | map.put(a, "a"); 114 | map.put(b, "b"); 115 | assertEquals(map.size(), 1); 116 | Amount c1 = new Amount(Currency.fromString("ABD"), AccountID.fromAddress("rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS")); 117 | NameIssuer c = NameIssuer.from(c1); 118 | map.put(c, "c"); 119 | assertEquals(map.size(), 2); 120 | Amount d1 = new Amount(Currency.fromString("ABC"), AccountID.fromAddress("raNDu1gNyZ5hipBTKxm5zx7NovA1rNnNRf")); 121 | NameIssuer d = NameIssuer.from(d1); 122 | map.put(d, "c"); 123 | assertEquals(map.size(), 3); 124 | } 125 | 126 | @Test 127 | public void amountEqual() { 128 | 129 | Amount c = new Amount(Currency.fromString("ABC"), AccountID.fromAddress("rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS")); 130 | Amount d = new Amount(Currency.fromString("ABC"), AccountID.fromAddress("rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS")); 131 | assertEquals(c, d); 132 | 133 | Amount e = new Amount(Currency.fromString("ABD"), AccountID.fromAddress("rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS")); 134 | assertNotEquals(c, e); 135 | 136 | } 137 | 138 | @Test 139 | public void testBigDecimalFloor() { 140 | BigDecimal a = new BigDecimal(4.202); 141 | BigDecimal res1 = a.divide(new BigDecimal(5.0), MathContext.DECIMAL32); 142 | assertEquals(res1.intValue(), 0); 143 | BigDecimal res2 = a.divide(new BigDecimal(2), MathContext.DECIMAL32); 144 | assertEquals(res2.intValue(), 2); 145 | 146 | } 147 | 148 | @Test 149 | public void testBlockingObs() { 150 | Subject pendingOrdersObs = PublishSubject.create(); 151 | System.out.println("a"); 152 | 153 | pendingOrdersObs.subscribeOn(Schedulers.io()).subscribe(o -> { 154 | System.out.println("b"); 155 | System.out.println("c"); 156 | 157 | }); 158 | System.out.println("d"); 159 | 160 | pendingOrdersObs.onNext(new Object()); 161 | } 162 | 163 | @Test 164 | public void testStreamFilter() { 165 | 166 | List lines = Arrays.asList("spring", "node", "mkyong"); 167 | List res = lines.stream().filter(line -> "aaa".equals(line)).collect(Collectors.toList()); 168 | 169 | assertNotEquals(res, null); 170 | assertEquals(0, res.size()); 171 | 172 | } 173 | 174 | @Test 175 | public void testJsonObjectOrString() { 176 | String t1 = " {\r\n \"flags\": 0,\r\n \"quality\": \"100000\",\r\n \"seq\": 12486,\r\n \"taker_gets\": {\r\n \"currency\": \"JPY\",\r\n \"issuer\": \"rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS\",\r\n \"value\": \"10\"\r\n },\r\n \"taker_pays\": \"1000000\"\r\n }"; 177 | JSONObject whole = new JSONObject(t1); 178 | 179 | Amount res; 180 | String key = "taker_pays"; 181 | try { 182 | JSONObject inside = whole.getJSONObject(key); 183 | BigDecimal value = new BigDecimal(inside.getString("value")); 184 | Currency currency = Currency.fromString(inside.getString("currency")); 185 | AccountID issuer = AccountID.fromAddress(inside.getString("issuer")); 186 | res = new Amount(value, currency, issuer); 187 | } catch (JSONException e) { 188 | System.out.println(); 189 | res = new Amount(new BigDecimal(whole.getString(key))); 190 | } 191 | // assertNotNull(whole.getJSONObject("taker_gets")); 192 | // assertNotNull(whole.getJSONObject("taker_pays")); 193 | } 194 | 195 | // 196 | // @Test 197 | // public void testRemoveFirstChar(){ 198 | // String s = "-0.0525506494749084/RJP/rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS"; 199 | // s = s.substring(1, s.length()); 200 | // assertEquals(s, 201 | // "0.0525506494749084/RJP/rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS"); 202 | // 203 | // } 204 | // 205 | // 206 | // @Test 207 | // public void testCurrencyNative(){ 208 | // Currency c = Currency.XRP; 209 | // assertTrue(c.isNative()); 210 | // } 211 | // 212 | // @Test 213 | // public void testCurrencyFromString(){ 214 | // Amount b = new Amount(Currency.fromString("JPY"), 215 | // AccountID.fromAddress("rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS")); 216 | // Amount bb = new Amount(Currency.fromString("JPY"), 217 | // AccountID.fromAddress("rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS")); 218 | // assertEquals(b, bb); 219 | // Amount c = new Amount(Currency.fromString("USD"), 220 | // AccountID.fromAddress("rB3gZey7VWHYRqJHLoHDEJXJ2pEPNieKiS")); 221 | // assertNotEquals(b, c); 222 | // Amount d= new Amount(new BigDecimal("0")); 223 | // assertNotEquals(b, d); 224 | // Amount e = new Amount(Currency.fromString("JPY"), 225 | // AccountID.fromAddress("rHMjZANhquizS74FutV3CNoWfQcoVkBxf")); 226 | // assertNotEquals(b, e); 227 | // } 228 | // 229 | // @Test 230 | // public void testBigDecimal(){ 231 | // BigDecimal a = new BigDecimal("345.24245364745677678968089554"); 232 | // BigDecimal b = a.setScale(6, BigDecimal.ROUND_HALF_DOWN); 233 | // assertEquals(b.toString(), "345.242454"); 234 | // MathContext mc = new MathContext(16, RoundingMode.HALF_DOWN); 235 | // BigDecimal c = a.round(mc); 236 | // assertEquals(c.toString(), "345.2424536474568"); 237 | // } 238 | // 239 | // @Test 240 | // public void testObs(){ 241 | // seqSyncObs.subscribe(flag -> { 242 | // System.out.println(flag); 243 | // }); 244 | // 245 | // seqSyncObs.onNext(flagWaitSeq1.compareAndSet(false, true)); // false 246 | // seqSyncObs.onNext(flagWaitSeq2.compareAndSet(false, true)); // true 247 | // 248 | // 249 | // } 250 | 251 | } 252 | -------------------------------------------------------------------------------- /src/test/java/com/ripple/core/coretypes/CurrencyTest.java: -------------------------------------------------------------------------------- 1 | package com.ripple.core.coretypes; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | public class CurrencyTest { 8 | 9 | @Test 10 | public void fromString() { 11 | Currency currency = Currency.fromString("JPY"); 12 | assertEquals(currency.toString(), "JPY"); 13 | 14 | } 15 | 16 | @Test 17 | public void testXRPCode() { 18 | assertEquals(Currency.XRP.toString(), "XRP"); 19 | 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /target/.gitignore: -------------------------------------------------------------------------------- 1 | /classes/ 2 | /test-classes/ 3 | --------------------------------------------------------------------------------