├── .gitignore ├── README.md ├── build.gradle ├── cpp └── FullTextDataRealtimeProvider │ ├── .gitignore │ ├── DemoExternalRealtimeProvider.cpp │ └── adapter.bat └── src └── main ├── java └── velox │ └── api │ └── layer0 │ ├── common │ └── TextStreamParser.java │ ├── live │ ├── DemoExternalRealtimeProvider.java │ ├── DemoExternalRealtimeTradingProvider.java │ ├── TradeAudit.java │ └── advanced │ │ ├── CrossPlatformTradingProvider.java │ │ ├── DemoExternalRealtimeUserMessageProvider.java │ │ └── FullTextDataRealtimeProvider.java │ └── replay │ ├── DemoGeneratorReplayProvider.java │ ├── DemoTextDataReplayProvider.java │ └── advanced │ ├── DemoAdvancedReplayProvider.java │ ├── DynamicAverage.java │ ├── Ema.java │ ├── FullTextDataReplayProvider.java │ ├── HandlerBase.java │ ├── HandlerBookmapIndicators.java │ ├── HandlerBookmapSimple.java │ ├── HandlerListener.java │ ├── IndicatorsPack.java │ ├── IntrinsicPrice.java │ └── OrderBookMbp.java └── resources └── icon_accept.gif /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /out 3 | /build 4 | .classpath 5 | .project 6 | .settings 7 | /.gradle/ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bookmap Layer 0 API 2 | 3 | ## General overview of L0 API 4 | 5 | Bookmap API consists of 2 main parts: 6 | 7 | - L0 API - allows you to feed raw data into Bookmap 8 | - L1 API - used for building indicators/strategies and feeding processed data into Bookmap 9 | 10 | This repository contains examples of L0 API usage. For L1 API, visit 11 | the [DemoStrategies repository](https://github.com/BookmapAPI/DemoStrategies) 12 | 13 | ## Use cases 14 | 15 | Layer0 API is a relatively simple way to: 16 | 17 | 1) Replay your own file format with Bookmap - referred to as **Replay** modules. 18 | 2) Connect Bookmap to your own data source in real-time (both for receiving data and 19 | trading) - referred to as **Live** modules 20 | 21 | Doing that requires basic Java knowledge, however (1) is possible even without any Java 22 | knowledge at all using any programming language if you don’t mind converting files before 23 | playing instead of integrating format support into Bookmap platform. 24 | 25 | ## Code examples 26 | 27 | **Layer0ApiDemo** project provides 4 demo classes with detailed comments that illustrate the most 28 | typical scenarios: 29 | 30 | 1) [`DemoTextDataReplayProvider`](src/main/java/velox/api/layer0/replay/DemoTextDataReplayProvider.java) - loads simple text format into Bookmap. As an option, 31 | you can just generate a file in this format and feed it to Bookmap using this module. This 32 | enables using any language for conversion to Bookmap format. 33 | 2) [`DemoGeneratorReplayProvider`](src/main/java/velox/api/layer0/replay/DemoGeneratorReplayProvider.java) - mimics a replay provider, but instead of loading data 34 | this provider generates it. Example of setting order queue position and displaying simple 35 | indicators using legacy API. 36 | 3) [`DemoExternalRealtimeProvider`](src/main/java/velox/api/layer0/live/DemoExternalRealtimeProvider.java) - example of custom realtime data provider. This one 37 | only generates random data for illustration purposes, but you can use it to build your 38 | own connectivity. 39 | 4) [`DemoExternalRealtimeTradingProvider`](src/main/java/velox/api/layer0/live/DemoExternalRealtimeTradingProvider.java) - extends `DemoExternalRealtimeProvider` 40 | providing trading capability. Simulates limit orders over the generated data. Shows how 41 | to build your own provider for connecting to a platform with trading support. 42 | The project also contains some more complicated examples. 43 | 44 | ## Javadoc 45 | 46 | Javadoc is available in: 47 | 48 | - [Bookmap Maven repository](https://maven.bookmap.com/maven2/releases/com/bookmap/api/api-core/) - 49 | open a directory with a name that corresponds to your Bookmap version (available at _Help -> About_). 50 | Download the file `api-core--javadoc.jar`, where `` is the version of 51 | your Bookmap. 52 | 53 | - Use the javadoc bundled with your Bookmap - `bm-l1api-javadoc.jar` typically located 54 | in `C:\Program Files\Bookmap\lib` 55 | (location might differ depending on the path selected during installation). 56 | 57 | The javadoc contains documentation for all levels of API, but for L0 API it’s mostly sufficient to 58 | only look inside `velox.api.layer0` package. 59 | 60 | ## Developing your L0 module 61 | 62 | The good starting points for your L0 modules are the classes: 63 | - `ExternalLiveBaseProvider` - for `Live` modules 64 | - `ExternalReaderBaseProvider` - for `Replay` modules 65 | 66 | Extending those classes will provide some parts of the logic already implemented for you. 67 | It’s also highly advised to read the javadoc for these classes, as it describes the L0 modules lifecycle. 68 | 69 | ## Loading modules 70 | 71 | There are 2 ways to load modules. 72 | 73 | ### 1. Loading via configuration files 74 | 75 | This approach is recommended for development, as it allows loading class files directly, without 76 | building a jar file. 77 | For Bookmap to load your module, it should be added to a configuration file inside 78 | `C:\Program Files\Bookmap\lib\UserModules\L0` 79 | There are 2 files, `external-live-modules.txt` and `external-reader-modules.txt` 80 | The first one contains a list of live connectivity modules, and the second one contains a list of 81 | replay modules. 82 | 83 | Files can contain comment lines (start with #) and lines in the following format: 84 | ` ` 85 | The path can point to either folder or .jar file with classes, whatever is more convenient. 86 | For example, the line in `external-live-modules.txt` can look like this: 87 | `velox.api.layer0.live.DemoExternalRealtimeTradingProvider D:\Layer0ApiDemo\build\classes\java\main\` 88 | 89 | Note that for this approach to work all the files should be included into the 90 | folder/jar file that you specify as ``, i.e. if your module uses 91 | a file from `resources` folder of class path - make sure it is in the same folder. 92 | 93 | ### 2. Loading from a jar file 94 | 95 | Modules annotated with `@Layer0LiveModule` or `@Layer0ReplayModule` can be 96 | packed into the JAR file and placed in `C:\Bookmap\API\Layer0ApiModules` (or similar location if 97 | the path was changed during the installation process). Bookmap will load it automatically on 98 | startup, as 99 | long as it’s permitted by license. 100 | 101 | ## Using the modules 102 | 103 | After modules are configured, those are used similarly to Bookmap internal functionality. 104 | 105 | ### 1. Using `Replay` modules 106 | 107 | Select a file the same way you would open a Bookmap feed file. Note, that since the extension of 108 | your file 109 | will likely be different from `.bmf`, you should first type “*” into the file name field and press 110 | enter - 111 | this will reset the file type filter and will allow you to load any type of file. 112 | 113 | ### 2. Using `Live` modules 114 | 115 | Navigate to _Connections -> Configure_, and select your module from the _Platform_ dropdown. Fill username, 116 | password fields, connect the same way you would do with any other connection. 117 | If you don’t need username or password just leave those empty - actual use of that data will be 118 | defined by your code. 119 | If you need to place some additional data like server address, you can place it into one of those 120 | fields. E.g. user “u1” at server “127.0.0.1” could be set as u1@127.0.0.1 inside “username” field. 121 | Use `Layer0CredentialsFieldsManager` annotation if you want to customize the input fields. 122 | 123 | ## Environment setup 124 | 125 | Check out [IDE and tricks](https://github.com/BookmapAPI/DemoStrategies#ide-and-tricks) 126 | for more details about running your project from IDE - the process and the main caveats are similar. 127 | 128 | ### Modifying the example project 129 | 130 | The repository contains a gradle project which you can import into an IDE of your choice. 131 | Make sure you have Bookmap installed (using the default path will make things a bit more simple), 132 | then import the example `Layer0ApiDemo` project as a gradle project. 133 | Use `gradle build` command for building the classes, or `gradle jar` to build the jar file ( 134 | available 135 | in `build/libs/`) 136 | 137 | ### Creating your own project 138 | 139 | For your own project, you can copy the build.gradle file to a new folder and create `src\main\java` 140 | and `src\main\resources` folders near it. 141 | If you don’t want to use gradle, you can just use API (either from `C:\Program 142 | Files\Bookmap\lib\bm-l1api.jar` or downloaded from the repository) as a compile-time 143 | dependency and build classes or jar file in any other way. 144 | 145 | 146 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'eclipse' 3 | apply plugin: 'idea' 4 | 5 | repositories { 6 | mavenCentral() 7 | maven { 8 | url "https://maven.bookmap.com/maven2/releases/" 9 | } 10 | } 11 | 12 | 13 | dependencies { 14 | implementation group: 'com.google.code.gson', name: 'gson', version: '2.4' 15 | if (!findProperty('is_built_from_main_bookmap_project')) { 16 | compileOnly group: 'com.bookmap.api', name: 'api-core', version: '7.4.0.19'; 17 | } 18 | } 19 | 20 | eclipse.classpath.downloadJavadoc = true 21 | idea.module.downloadJavadoc = true 22 | -------------------------------------------------------------------------------- /cpp/FullTextDataRealtimeProvider/.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.obj -------------------------------------------------------------------------------- /cpp/FullTextDataRealtimeProvider/DemoExternalRealtimeProvider.cpp: -------------------------------------------------------------------------------- 1 | // Demo to use with FullTextDataRealtimeProvider that 2 | // generates random data for instruments that you subscribe to. 3 | // You can compile it with visual studio by launching 4 | // "Developer Command Prompt" (e.g. for VS 2017 it's 5 | // "Developer Command Prompt for VS 2017" and running "cl DemoExternalRealtimeProvider.cpp" 6 | // in the folder with this cpp file. 7 | // This code was tested under Windows with VS 2017, but you should be able to use similar 8 | // (or exactly the same) approach with other platforms/compilers 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | using namespace std; 20 | 21 | const int DEPTH_LEVELS_COUNT = 10; 22 | 23 | // We don't really need to report time 24 | // (bookmap will ignore it in realtimemode), 25 | // but just for consistency 26 | long long getTime() { 27 | long long ms = chrono::duration_cast( 28 | chrono::system_clock::now().time_since_epoch()).count(); 29 | return ms; 30 | } 31 | 32 | const char * toString(bool b) { 33 | return b ? "true" : "false"; 34 | } 35 | 36 | // Primitive way to generate JSON. Will break if strings contain some 37 | // characters, like " or \n. Used for simplicity. 38 | void onDepth(string alias, bool isBid, int price, int size) { 39 | printf("Depth {\"alias\":\"%s\",\"isBid\":%s,\"price\":%d,\"size\":%d,\"time\":%lld}\n", 40 | alias.c_str(), toString(isBid), price, size, getTime()); 41 | } 42 | 43 | void onTrade(string alias, bool isBid, int price, int size) { 44 | printf("Trade {\"alias\":\"%s\",\"price\":%d,\"size\":%d,\"tradeInfo\":{\ 45 | \"isOtc\":false,\"isBidAggressor\":%s,\"isExecutionStart\":true,\"isExecutionEnd\":true}\ 46 | ,\"time\":%lld}\n", 47 | alias.c_str(), price, size, toString(isBid), getTime()); 48 | } 49 | 50 | void onInstrumentAdded(string alias, string symbol, string exchange, string type, 51 | double pips, double multiplier, string fullName, bool isFullDepth, 52 | double sizeMultiplier) { 53 | printf("InstrumentAdded {\"alias\":\"%s\",\"instrumentInfo\":\ 54 | {\"pips\":%lf,\"multiplier\":%lf,\"fullName\":\"%s\",\"isFullDepth\":%s,\ 55 | \"sizeMultiplier\":%lf,\"symbol\":\"%s\",\"exchange\":\"%s\",\"type\":\"%s\"},\ 56 | \"time\":%lld}\n", 57 | alias.c_str(), pips, multiplier, fullName.c_str(), toString(isFullDepth), 58 | sizeMultiplier, symbol.c_str(), exchange.c_str(), type.c_str(), getTime()); 59 | // Flushing because otherwise user will have to wait while it's in buffer 60 | fflush(stdout); 61 | } 62 | 63 | void onInstrumentAlreadySubscribed(string symbol, string exchange, string type) { 64 | printf("InstrumentAlreadySubscribed {\"symbol\":\"%s\",\"exchange\":\"%s\",\ 65 | \"type\":\"%s\",\"time\":%lld}\n", 66 | symbol.c_str(), exchange.c_str(), type.c_str(), getTime()); 67 | // Flushing because otherwise user will have to wait while it's in buffer 68 | fflush(stdout); 69 | } 70 | 71 | void onLoginSuccessful() { 72 | printf("LoginSuccessful {\"time\":%lld}\n", getTime()); 73 | // Flushing because otherwise user will have to wait while it's in buffer 74 | fflush(stdout); 75 | } 76 | 77 | void onLoginFailed(string reason, string message) { 78 | printf("LoginFailed {\"reason\":\"%s\",\"message\":\"%s\",\"time\":%lld}\n", 79 | reason.c_str(), message.c_str(), getTime()); 80 | // Flushing because otherwise user will have to wait while it's in buffer 81 | fflush(stdout); 82 | } 83 | 84 | struct Instrument { 85 | 86 | string alias; 87 | double pips; 88 | 89 | int basePrice; 90 | 91 | Instrument(string alias, double pips) { 92 | this->alias = alias; 93 | this->pips = pips; 94 | 95 | // Pick random price that will be used to generate the data 96 | // This is an integer representation of a price (before multiplying 97 | // by pips) 98 | this->basePrice = (int)(rand() % 10000 + 1000); 99 | } 100 | 101 | void generateData() { 102 | 103 | // Determining best bid/ask 104 | int bestBid = getBestBid(); 105 | int bestAsk = getBestAsk(); 106 | 107 | // Populating 10 levels to each side of best bid/best ask with 108 | // random data 109 | for (int i = 0; i < DEPTH_LEVELS_COUNT; ++i) { 110 | int levelsOffset = i; 111 | onDepth(alias, true, bestBid - levelsOffset, getRandomSize()); 112 | onDepth(alias, false, bestAsk + levelsOffset, getRandomSize()); 113 | } 114 | 115 | // Trade on best bid, ask agressor 116 | onTrade(alias, false, bestBid, 1); 117 | // Trade on best ask, bid agressor 118 | onTrade(alias, true, bestAsk, 1); 119 | 120 | // With 10% chance change BBO 121 | if (rand() % 100 < 10) { 122 | // 50% chance to move up, 50% to move down 123 | if (rand() % 100 > 50) { 124 | // Moving up - erasing best ask, erasing last reported bid 125 | // level (emulating exchange only reporting few levels) 126 | ++basePrice; 127 | onDepth(alias, false, bestAsk, 0); 128 | onDepth(alias, true, bestBid - (DEPTH_LEVELS_COUNT - 1), 0); 129 | // Could also populate new best bid and add last best ask, 130 | // but this can be omitted - those will be populated during 131 | // next simulation step 132 | } 133 | else { 134 | // Moving down - erasing best bid, erasing last reported ask 135 | // level (emulating exchange only reporting few levels) 136 | --basePrice; 137 | onDepth(alias, true, bestBid, 0); 138 | onDepth(alias, false, bestAsk + (DEPTH_LEVELS_COUNT - 1), 0); 139 | // Could also populate new best ask and add last best bid, 140 | // but this can be omitted - those will be populated during 141 | // next simulation step 142 | } 143 | } 144 | } 145 | 146 | int getBestAsk() { 147 | return basePrice; 148 | } 149 | 150 | int getBestBid() { 151 | return getBestAsk() - 1; 152 | } 153 | 154 | int getRandomSize() { 155 | return (int)(1 + rand() % 10); 156 | } 157 | 158 | }; 159 | 160 | bool closing = false; 161 | 162 | mutex instrumentsMutex; 163 | map instruments; 164 | 165 | void login(string user, string password, bool demo) { 166 | // With real connection provider would attempt establishing connection here. 167 | bool isValid = "pass" == password && "user" == user && demo == true; 168 | 169 | if (isValid) { 170 | // Report succesful login 171 | onLoginSuccessful(); 172 | } 173 | else { 174 | // Report failed login 175 | // Since we don't have proper json serializer in this example, 176 | // we have to escape newlines here 177 | onLoginFailed("WRONG_CREDENTIALS", 178 | "This provider only acepts following credentials:\\n\ 179 | username: user\\npassword: pass\\nis demo: checked"); 180 | } 181 | } 182 | 183 | string createAlias(string symbol, string exchange, string type) { 184 | return symbol + "/" + exchange + "/" + type; 185 | } 186 | 187 | void subscribe(string symbol, string exchange, string type) { 188 | std::lock_guard lock(instrumentsMutex); 189 | 190 | string alias = createAlias(symbol, exchange, type); 191 | if (instruments.find(alias) != instruments.end()) { 192 | onInstrumentAlreadySubscribed(symbol, exchange, type); 193 | } 194 | else { 195 | // We are performing subscription synchronously for simplicity, 196 | // but if subscription process takes long it's better to do it 197 | // asynchronously 198 | 199 | // Randomly determining pips. In reality it will be received 200 | // from external source 201 | double pips = rand() % 100 > 50 ? 0.5 : 0.25; 202 | 203 | instruments.emplace(alias, Instrument(alias, pips)); 204 | 205 | onInstrumentAdded(alias, symbol, exchange, type, pips, 1, "Full name:" + alias, false, 1); 206 | } 207 | } 208 | 209 | void unsubscribe(string alias) { 210 | std::lock_guard lock(instrumentsMutex); 211 | instruments.erase(alias); 212 | } 213 | 214 | void simulateStep() { 215 | std::lock_guard lock(instrumentsMutex); 216 | for (auto entry : instruments) { 217 | entry.second.generateData(); 218 | } 219 | // Instead of flushing every single update we flush at the end 220 | // This is a bit more performance-friendly approach 221 | // But this probably doesn't make a practical difference 222 | // as 200K+ events per second can be pushed both ways, 223 | // bottlenecking the actual processing. 224 | fflush(stdout); 225 | } 226 | 227 | void simulate() { 228 | while (!closing) { 229 | // Generate some data changes 230 | simulateStep(); 231 | // Waiting a bit before generating more data. 232 | // Interruptable sleep would be better. 233 | // Downside of this approach is that bookmap will have to wait up to 1s to 234 | // shut down this thread. 235 | this_thread::sleep_for(chrono::milliseconds(1000)); 236 | } 237 | } 238 | 239 | int main() { 240 | 241 | thread simulationThread(simulate); 242 | 243 | bool readSuccess; 244 | string command; 245 | do { 246 | readSuccess = !getline(cin, command).eof(); 247 | if (!readSuccess) { 248 | // Do nothing. This is end of file. 249 | // (bookmap died without closing the module). 250 | // If we ignore EOF subprocess might stay e.g. 251 | // if bookmap was killed via task manager. 252 | } else if (command == "login") { 253 | 254 | string user, password, demoString; 255 | getline(cin, user); 256 | getline(cin, password); 257 | getline(cin, demoString); 258 | bool demo = demoString == "true"; 259 | 260 | login(user, password, demo); 261 | 262 | } else if (command == "subscribe") { 263 | 264 | string symbol, exchange, type; 265 | getline(cin, symbol); 266 | getline(cin, exchange); 267 | getline(cin, type); 268 | 269 | subscribe(symbol, exchange, type); 270 | 271 | } else if (command == "unsubscribe") { 272 | string alias; 273 | getline(cin, alias); 274 | unsubscribe(alias); 275 | } 276 | } while (command != "close" && readSuccess); 277 | 278 | closing = true; 279 | simulationThread.join(); 280 | 281 | return 0; 282 | } 283 | -------------------------------------------------------------------------------- /cpp/FullTextDataRealtimeProvider/adapter.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | rem This is an examle adapter.bat file 3 | rem Copy to Config folder along with executable 4 | DemoExternalRealtimeProvider.exe 5 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/common/TextStreamParser.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.common; 2 | 3 | import java.awt.Color; 4 | import java.awt.image.BufferedImage; 5 | import java.io.BufferedReader; 6 | import java.io.ByteArrayInputStream; 7 | import java.io.FileReader; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | import java.util.Base64; 12 | 13 | import javax.imageio.ImageIO; 14 | 15 | import com.google.gson.Gson; 16 | 17 | import velox.api.layer0.data.FileEndReachedUserMessage; 18 | import velox.api.layer0.data.FileNotSupportedUserMessage; 19 | import velox.api.layer0.data.IndicatorDefinitionUserMessage; 20 | import velox.api.layer0.data.IndicatorPointUserMessage; 21 | import velox.api.layer0.data.OrderQueuePositionUserMessage; 22 | import velox.api.layer0.data.ReadFileLoginData; 23 | import velox.api.layer0.data.TextDataMessage; 24 | import velox.api.layer0.replay.ExternalReaderBaseProvider; 25 | import velox.api.layer1.Layer1ApiDataListener; 26 | import velox.api.layer1.Layer1ApiListener; 27 | import velox.api.layer1.data.BalanceInfo; 28 | import velox.api.layer1.data.DisconnectionReason; 29 | import velox.api.layer1.data.ExecutionInfo; 30 | import velox.api.layer1.data.InstrumentInfo; 31 | import velox.api.layer1.data.LoginData; 32 | import velox.api.layer1.data.LoginFailedReason; 33 | import velox.api.layer1.data.MarketMode; 34 | import velox.api.layer1.data.OrderInfoUpdate; 35 | import velox.api.layer1.data.StatusInfo; 36 | import velox.api.layer1.data.SystemTextMessageType; 37 | import velox.api.layer1.data.TradeInfo; 38 | import velox.api.layer1.layers.Layer1ApiUpstreamRelay; 39 | 40 | /** 41 | * Reads data from a stream that essentially describes sequence of calls to 42 | * {@link Layer1ApiListener} and sends to all subscribers. 43 | */ 44 | public class TextStreamParser extends Layer1ApiUpstreamRelay { 45 | 46 | public static class Event { 47 | public long time; 48 | } 49 | 50 | public static class EventInstrumentAdded extends Event { 51 | public String alias; 52 | public InstrumentInfo instrumentInfo; 53 | } 54 | 55 | public static class EventInstrumentRemoved extends Event { 56 | public String alias; 57 | } 58 | 59 | public static class EventInstrumentNotFound extends Event { 60 | public String symbol; 61 | public String exchange; 62 | public String type; 63 | } 64 | 65 | public static class EventInstrumentAlreadySubscribed extends Event { 66 | public String symbol; 67 | public String exchange; 68 | public String type; 69 | } 70 | 71 | public static class EventTrade extends Event { 72 | public String alias; 73 | public double price; 74 | public int size; 75 | public TradeInfo tradeInfo; 76 | } 77 | 78 | public static class EventDepth extends Event { 79 | public String alias; 80 | public boolean isBid; 81 | public int price; 82 | public int size; 83 | } 84 | 85 | public static class EventMboSend extends Event { 86 | public String alias; 87 | public String orderId; 88 | public boolean isBid; 89 | public int price; 90 | public int size; 91 | } 92 | 93 | public static class EventMboReplace extends Event { 94 | public String alias; 95 | public String orderId; 96 | public int price; 97 | public int size; 98 | } 99 | 100 | public static class EventMboCancel extends Event { 101 | public String alias; 102 | public String orderId; 103 | } 104 | 105 | public static class EventMarketMode extends Event { 106 | public String alias; 107 | public MarketMode marketMode; 108 | } 109 | 110 | public static class EventOrderUpdated extends Event { 111 | public OrderInfoUpdate orderInfoUpdate; 112 | } 113 | 114 | public static class EventOrderExecuted extends Event { 115 | public ExecutionInfo executionInfo; 116 | } 117 | 118 | public static class EventStatus extends Event { 119 | public StatusInfo statusInfo; 120 | } 121 | 122 | public static class EventBalance extends Event { 123 | public BalanceInfo balanceInfo; 124 | } 125 | 126 | public static class EventLoginFailed extends Event { 127 | public LoginFailedReason reason; 128 | public String message; 129 | } 130 | 131 | public static class EventLoginSuccessful extends Event { 132 | } 133 | 134 | public static class EventConnectionLost extends Event { 135 | public DisconnectionReason reason; 136 | public String message; 137 | } 138 | 139 | public static class EventConnectionRestored extends Event { 140 | } 141 | 142 | public static class EventSystemTextMessage extends Event { 143 | public String message; 144 | public SystemTextMessageType messageType; 145 | } 146 | 147 | public static class IndicatorDefinitionUserMessageEvent extends Event { 148 | public int id; 149 | public String alias; 150 | public String indicatorName; 151 | public short mainLineStyleMask; 152 | public short mainLineStyleMultiplier; 153 | public int mainLineWidth; 154 | public Color lineColor; 155 | public short rightLineStyleMask; 156 | public short rightLineStyleMultiplier; 157 | public int rightLineWidth; 158 | public String base64EndodedIcon; 159 | public int iconOffsetX; 160 | public int iconOffsetY; 161 | public boolean showOnMainChart; 162 | public String valueFormat; 163 | } 164 | 165 | public static class IndicatorPointUserMessageEvent extends Event { 166 | public int id; 167 | public double price; 168 | } 169 | 170 | public static class OrderQueuePositionUserMessageEvent extends Event { 171 | public String orderId; 172 | public int position; 173 | } 174 | 175 | public static class TextDataMessageEvent extends Event { 176 | public String alias; 177 | public String source; 178 | public double price; 179 | public double size; 180 | public Boolean isBid; 181 | public String data; 182 | } 183 | 184 | private final Gson gson = new Gson(); 185 | 186 | private Thread readerThread; 187 | private long currentTime = 0; 188 | 189 | private boolean play = true; 190 | 191 | private BufferedReader reader; 192 | 193 | public TextStreamParser() { 194 | } 195 | 196 | public void start(InputStream inputStream) { 197 | 198 | try { 199 | reader = new BufferedReader(new InputStreamReader(inputStream)); 200 | 201 | // Reading one line to guarantee that when we exit this method 202 | // getCurrentTime will return meaningful result. 203 | readLine(); 204 | 205 | readerThread = new Thread(this::read); 206 | readerThread.start(); 207 | } catch (@SuppressWarnings("unused") IOException e) { 208 | adminListeners.forEach(listener -> listener.onUserMessage(new FileNotSupportedUserMessage())); 209 | } 210 | } 211 | 212 | private void read() { 213 | try { 214 | while (!Thread.interrupted() && play) { 215 | readLine(); 216 | } 217 | } catch (@SuppressWarnings("unused") IOException e) { 218 | reportFileEnd(); 219 | } 220 | } 221 | 222 | public void reportFileEnd() { 223 | adminListeners.forEach(listener -> listener.onUserMessage(new FileEndReachedUserMessage())); 224 | play = false; 225 | } 226 | 227 | private void readLine() throws IOException { 228 | String line = reader.readLine(); 229 | if (line == null && play) { 230 | reportFileEnd(); 231 | } else { 232 | String[] tokens = line.split(" ", 2); 233 | String eventCode = tokens[0]; 234 | String eventData = tokens[1]; 235 | switch (eventCode) { 236 | case "InstrumentAdded": { 237 | EventInstrumentAdded event = gson.fromJson(eventData, EventInstrumentAdded.class); 238 | currentTime = event.time; 239 | onInstrumentAdded(event.alias, event.instrumentInfo); 240 | break; 241 | } 242 | case "InstrumentRemoved": { 243 | EventInstrumentRemoved event = gson.fromJson(eventData, EventInstrumentRemoved.class); 244 | currentTime = event.time; 245 | onInstrumentRemoved(event.alias); 246 | break; 247 | } 248 | case "InstrumentNotFound": { 249 | EventInstrumentNotFound event = gson.fromJson(eventData, EventInstrumentNotFound.class); 250 | currentTime = event.time; 251 | onInstrumentNotFound(event.symbol, event.exchange, event.type); 252 | break; 253 | } 254 | case "InstrumentAlreadySubscribed": { 255 | EventInstrumentAlreadySubscribed event = gson.fromJson(eventData, EventInstrumentAlreadySubscribed.class); 256 | currentTime = event.time; 257 | onInstrumentAlreadySubscribed(event.symbol, event.exchange, event.type); 258 | break; 259 | } 260 | case "Trade": { 261 | EventTrade event = gson.fromJson(eventData, EventTrade.class); 262 | currentTime = event.time; 263 | onTrade(event.alias, event.price, event.size, event.tradeInfo); 264 | break; 265 | } 266 | case "Depth": { 267 | EventDepth event = gson.fromJson(eventData, EventDepth.class); 268 | currentTime = event.time; 269 | onDepth(event.alias, event.isBid, event.price, event.size); 270 | break; 271 | } 272 | case "MboSend": { 273 | EventMboSend event = gson.fromJson(eventData, EventMboSend.class); 274 | currentTime = event.time; 275 | onMboSend(event.alias, event.orderId, event.isBid, event.price, event.size); 276 | break; 277 | } 278 | case "MboReplace": { 279 | EventMboReplace event = gson.fromJson(eventData, EventMboReplace.class); 280 | currentTime = event.time; 281 | onMboReplace(event.alias, event.orderId, event.price, event.size); 282 | break; 283 | } 284 | case "MboCancel": { 285 | EventMboCancel event = gson.fromJson(eventData, EventMboCancel.class); 286 | currentTime = event.time; 287 | onMboCancel(event.alias, event.orderId); 288 | break; 289 | } 290 | case "MarketMode": { 291 | EventMarketMode event = gson.fromJson(eventData, EventMarketMode.class); 292 | currentTime = event.time; 293 | onMarketMode(event.alias, event.marketMode); 294 | break; 295 | } 296 | case "OrderUpdated": { 297 | EventOrderUpdated event = gson.fromJson(eventData, EventOrderUpdated.class); 298 | currentTime = event.time; 299 | onOrderUpdated(event.orderInfoUpdate); 300 | break; 301 | } 302 | case "OrderExecuted": { 303 | EventOrderExecuted event = gson.fromJson(eventData, EventOrderExecuted.class); 304 | currentTime = event.time; 305 | onOrderExecuted(event.executionInfo); 306 | break; 307 | } 308 | case "Status": { 309 | EventStatus event = gson.fromJson(eventData, EventStatus.class); 310 | currentTime = event.time; 311 | onStatus(event.statusInfo); 312 | break; 313 | } 314 | case "Balance": { 315 | EventBalance event = gson.fromJson(eventData, EventBalance.class); 316 | currentTime = event.time; 317 | onBalance(event.balanceInfo); 318 | break; 319 | } 320 | case "LoginFailed": { 321 | EventLoginFailed event = gson.fromJson(eventData, EventLoginFailed.class); 322 | currentTime = event.time; 323 | onLoginFailed(event.reason, event.message); 324 | break; 325 | } 326 | case "LoginSuccessful": { 327 | EventLoginSuccessful event = gson.fromJson(eventData, EventLoginSuccessful.class); 328 | currentTime = event.time; 329 | onLoginSuccessful(); 330 | break; 331 | } 332 | case "ConnectionLost": { 333 | EventConnectionLost event = gson.fromJson(eventData, EventConnectionLost.class); 334 | currentTime = event.time; 335 | onConnectionLost(event.reason, event.message); 336 | break; 337 | } 338 | case "ConnectionRestored": { 339 | EventConnectionRestored event = gson.fromJson(eventData, EventConnectionRestored.class); 340 | currentTime = event.time; 341 | onConnectionRestored(); 342 | break; 343 | } 344 | case "SystemTextMessage": { 345 | EventSystemTextMessage event = gson.fromJson(eventData, EventSystemTextMessage.class); 346 | currentTime = event.time; 347 | onSystemTextMessage(event.message, event.messageType); 348 | break; 349 | } 350 | case "IndicatorDefinitionUserMessage": { 351 | IndicatorDefinitionUserMessageEvent event = gson.fromJson(eventData, 352 | IndicatorDefinitionUserMessageEvent.class); 353 | currentTime = event.time; 354 | 355 | BufferedImage icon = null; 356 | if (event.base64EndodedIcon != null) { 357 | byte[] iconBytes = Base64.getDecoder().decode(event.base64EndodedIcon); 358 | icon = ImageIO.read(new ByteArrayInputStream(iconBytes)); 359 | } 360 | 361 | onUserMessage(new IndicatorDefinitionUserMessage(event.id, event.alias, event.indicatorName, 362 | event.mainLineStyleMask, event.mainLineStyleMultiplier, event.mainLineWidth, event.lineColor, 363 | event.rightLineStyleMask, event.rightLineStyleMultiplier, event.rightLineWidth, 364 | icon, event.iconOffsetX, event.iconOffsetY, event.showOnMainChart, event.valueFormat)); 365 | break; 366 | } 367 | case "IndicatorPointUserMessage": { 368 | IndicatorPointUserMessageEvent event = gson.fromJson(eventData, IndicatorPointUserMessageEvent.class); 369 | currentTime = event.time; 370 | onUserMessage(new IndicatorPointUserMessage(event.id, event.price)); 371 | break; 372 | } 373 | case "OrderQueuePositionUserMessage": { 374 | OrderQueuePositionUserMessageEvent event = gson.fromJson(eventData, 375 | OrderQueuePositionUserMessageEvent.class); 376 | currentTime = event.time; 377 | onUserMessage(new OrderQueuePositionUserMessage(event.orderId, event.position)); 378 | break; 379 | } 380 | case "TextDataMessage": { 381 | TextDataMessageEvent event = gson.fromJson(eventData, TextDataMessageEvent.class); 382 | currentTime = event.time; 383 | onUserMessage(new TextDataMessage(event.alias, event.source, event.isBid, event.price, event.size, event.data)); 384 | break; 385 | } 386 | default: 387 | reportFileEnd(); 388 | throw new RuntimeException("Unknown event code " + eventCode); 389 | } 390 | } 391 | } 392 | 393 | public long getCurrentTime() { 394 | return currentTime; 395 | } 396 | 397 | @Override 398 | public void close() { 399 | readerThread.interrupt(); 400 | try { 401 | reader.close(); 402 | } catch (@SuppressWarnings("unused") IOException e) { 403 | } 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/live/DemoExternalRealtimeProvider.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.live; 2 | 3 | import java.util.HashMap; 4 | 5 | import velox.api.layer0.annotations.Layer0LiveModule; 6 | import velox.api.layer1.Layer1ApiAdminListener; 7 | import velox.api.layer1.annotations.Layer1ApiVersion; 8 | import velox.api.layer1.annotations.Layer1ApiVersionValue; 9 | import velox.api.layer1.data.InstrumentInfo; 10 | import velox.api.layer1.data.LoginData; 11 | import velox.api.layer1.data.LoginFailedReason; 12 | import velox.api.layer1.data.OrderSendParameters; 13 | import velox.api.layer1.data.OrderUpdateParameters; 14 | import velox.api.layer1.data.SubscribeInfo; 15 | import velox.api.layer1.data.TradeInfo; 16 | import velox.api.layer1.data.UserPasswordDemoLoginData; 17 | 18 | /** 19 | *

20 | * This a demo provider that generates data instead of actually receiving it. 21 | *

22 | */ 23 | @Layer1ApiVersion(Layer1ApiVersionValue.VERSION2) 24 | @Layer0LiveModule(fullName = "Demo external realtime", shortName = "DE") 25 | public class DemoExternalRealtimeProvider extends ExternalLiveBaseProvider { 26 | 27 | protected class Instrument { 28 | /** Number of depth levels that will be generated on each side */ 29 | private static final int DEPTH_LEVELS_COUNT = 10; 30 | 31 | protected final String alias; 32 | protected final double pips; 33 | 34 | private int basePrice; 35 | 36 | public Instrument(String alias, double pips) { 37 | this.alias = alias; 38 | this.pips = pips; 39 | 40 | // Pick random price that will be used to generate the data 41 | // This is an integer representation of a price (before multiplying 42 | // by pips) 43 | this.basePrice = (int) (Math.random() * 10000 + 1000); 44 | } 45 | 46 | public void generateData() { 47 | 48 | // Determining best bid/ask 49 | int bestBid = getBestBid(); 50 | int bestAsk = getBestAsk(); 51 | 52 | // Populating 10 levels to each side of best bid/best ask with 53 | // random data 54 | for (int i = 0; i < DEPTH_LEVELS_COUNT; ++i) { 55 | final int levelsOffset = i; 56 | dataListeners.forEach(l -> l.onDepth(alias, true, bestBid - levelsOffset, getRandomSize())); 57 | dataListeners.forEach(l -> l.onDepth(alias, false, bestAsk + levelsOffset, getRandomSize())); 58 | } 59 | 60 | // Currently Bookmap does not visualize OTC trades, so you will 61 | // mostly want isOtc=false 62 | final boolean isOtc = false; 63 | // Trade on best bid, ask agressor 64 | dataListeners.forEach(l -> l.onTrade(alias, bestBid, 1, new TradeInfo(isOtc, false))); 65 | // Trade on best ask, bid agressor 66 | dataListeners.forEach(l -> l.onTrade(alias, bestAsk, 1, new TradeInfo(isOtc, true))); 67 | 68 | // With 10% chance change BBO 69 | if (Math.random() < 0.1) { 70 | // 50% chance to move up, 50% to move down 71 | if (Math.random() > 0.5) { 72 | // Moving up - erasing best ask, erasing last reported bid 73 | // level (emulating exchange only reporting few levels) 74 | ++basePrice; 75 | dataListeners.forEach(l -> l.onDepth(alias, false, bestAsk, 0)); 76 | dataListeners.forEach(l -> l.onDepth(alias, true, bestBid - (DEPTH_LEVELS_COUNT - 1), 0)); 77 | // Could also populate new best bid and add last best ask, 78 | // but this can be omitted - those will be populated during 79 | // next simulation step 80 | } else { 81 | // Moving down - erasing best bid, erasing last reported ask 82 | // level (emulating exchange only reporting few levels) 83 | --basePrice; 84 | dataListeners.forEach(l -> l.onDepth(alias, true, bestBid, 0)); 85 | dataListeners.forEach(l -> l.onDepth(alias, false, bestAsk + (DEPTH_LEVELS_COUNT - 1), 0)); 86 | // Could also populate new best ask and add last best bid, 87 | // but this can be omitted - those will be populated during 88 | // next simulation step 89 | } 90 | } 91 | } 92 | 93 | public int getBestAsk() { 94 | return basePrice; 95 | } 96 | 97 | public int getBestBid() { 98 | return getBestAsk() - 1; 99 | } 100 | 101 | private int getRandomSize() { 102 | return (int) (1 + Math.random() * 10); 103 | } 104 | 105 | } 106 | 107 | protected HashMap instruments = new HashMap<>(); 108 | 109 | // This thread will perform data generation. 110 | private Thread connectionThread = null; 111 | 112 | /** 113 | *

114 | * Generates alias from symbol, exchange and type of the instrument. Alias 115 | * is a unique identifier for the instrument, but it's also used in many 116 | * places in UI, so it should also be easily readable. 117 | *

118 | *

119 | * Note, that you don't have to use all 3 fields. You can just ignore some 120 | * of those, for example use symbol only. 121 | *

122 | */ 123 | private static String createAlias(String symbol, String exchange, String type) { 124 | return symbol + "/" + exchange + "/" + type; 125 | } 126 | 127 | @Override 128 | public void subscribe(SubscribeInfo subscribeInfo) { 129 | String symbol = subscribeInfo.symbol; 130 | String exchange = subscribeInfo.exchange; 131 | String type = subscribeInfo.type; 132 | 133 | String alias = createAlias(symbol, exchange, type); 134 | // Since instruments also will be accessed from the data generation 135 | // thread, synchronization is required 136 | // 137 | // No need to worry about calling listener from synchronized block, 138 | // since those will be processed asynchronously 139 | synchronized (instruments) { 140 | if (instruments.containsKey(alias)) { 141 | instrumentListeners.forEach(l -> l.onInstrumentAlreadySubscribed(symbol, exchange, type)); 142 | } else { 143 | // We are performing subscription synchronously for simplicity, 144 | // but if subscription process takes long it's better to do it 145 | // asynchronously (e.g use Executor) 146 | 147 | // Randomly determining pips. In reality it will be received 148 | // from external source 149 | double pips = Math.random() > 0.5 ? 0.5 : 0.25; 150 | 151 | final Instrument newInstrument = new Instrument(alias, pips); 152 | instruments.put(alias, newInstrument); 153 | 154 | final InstrumentInfo instrumentInfo = new InstrumentInfo( 155 | symbol, exchange, type, newInstrument.pips, 1, "", false); 156 | 157 | instrumentListeners.forEach(l -> l.onInstrumentAdded(alias, instrumentInfo)); 158 | } 159 | } 160 | } 161 | 162 | @Override 163 | public void unsubscribe(String alias) { 164 | synchronized (instruments) { 165 | if (instruments.remove(alias) != null) { 166 | instrumentListeners.forEach(l -> l.onInstrumentRemoved(alias)); 167 | } 168 | } 169 | } 170 | 171 | @Override 172 | public String formatPrice(String alias, double price) { 173 | // Use default Bookmap price formatting logic for simplicity. 174 | // Values returned by this method will be used on price axis and in few 175 | // other places. 176 | 177 | double pips; 178 | synchronized (instruments) { 179 | pips = instruments.get(alias).pips; 180 | } 181 | 182 | return formatPriceDefault(pips, price); 183 | } 184 | 185 | @Override 186 | public void sendOrder(OrderSendParameters orderSendParameters) { 187 | // This method will not be called because this adapter does not report 188 | // trading capabilities 189 | throw new RuntimeException("Not trading capable"); 190 | } 191 | 192 | @Override 193 | public void updateOrder(OrderUpdateParameters orderUpdateParameters) { 194 | // This method will not be called because this adapter does not report 195 | // trading capabilities 196 | throw new RuntimeException("Not trading capable"); 197 | } 198 | 199 | @Override 200 | public void login(LoginData loginData) { 201 | UserPasswordDemoLoginData userPasswordDemoLoginData = (UserPasswordDemoLoginData) loginData; 202 | 203 | // If connection process takes a while then it's better to do it in 204 | // separate thread 205 | connectionThread = new Thread(() -> handleLogin(userPasswordDemoLoginData)); 206 | connectionThread.start(); 207 | } 208 | 209 | private void handleLogin(UserPasswordDemoLoginData userPasswordDemoLoginData) { 210 | // With real connection provider would attempt establishing connection 211 | // here. 212 | boolean isValid = "pass".equals(userPasswordDemoLoginData.password) 213 | && "user".equals(userPasswordDemoLoginData.user) && userPasswordDemoLoginData.isDemo == true; 214 | 215 | if (isValid) { 216 | // Report succesful login 217 | adminListeners.forEach(Layer1ApiAdminListener::onLoginSuccessful); 218 | 219 | // Generate some events each second 220 | while (!Thread.interrupted()) { 221 | 222 | // Generate some data changes 223 | simulate(); 224 | 225 | // Waiting a bit before generating more data 226 | try { 227 | Thread.sleep(1000); 228 | } catch (@SuppressWarnings("unused") InterruptedException e) { 229 | Thread.currentThread().interrupt(); 230 | } 231 | } 232 | } else { 233 | // Report failed login 234 | adminListeners.forEach(l -> l.onLoginFailed(LoginFailedReason.WRONG_CREDENTIALS, 235 | "This provider only acepts following credentials:\n" 236 | + "username: user\n" 237 | + "password: pass\n" 238 | + "is demo: checked")); 239 | } 240 | } 241 | 242 | protected void simulate() { 243 | // Generating some data for each of the instruments 244 | synchronized (instruments) { 245 | instruments.values().forEach(Instrument::generateData); 246 | } 247 | } 248 | 249 | @Override 250 | public String getSource() { 251 | // String identifying where data came from. 252 | // For example you can use that later in your indicator. 253 | return "realtime demo"; 254 | } 255 | 256 | @Override 257 | public void close() { 258 | // Stop events generation 259 | connectionThread.interrupt(); 260 | } 261 | 262 | } 263 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/live/DemoExternalRealtimeTradingProvider.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.live; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | 8 | import velox.api.layer0.annotations.Layer0LiveModule; 9 | import velox.api.layer1.annotations.Layer1ApiVersion; 10 | import velox.api.layer1.annotations.Layer1ApiVersionValue; 11 | import velox.api.layer1.data.ExecutionInfo; 12 | import velox.api.layer1.data.Layer1ApiProviderSupportedFeatures; 13 | import velox.api.layer1.data.OrderCancelParameters; 14 | import velox.api.layer1.data.OrderDuration; 15 | import velox.api.layer1.data.OrderInfoBuilder; 16 | import velox.api.layer1.data.OrderMoveParameters; 17 | import velox.api.layer1.data.OrderResizeParameters; 18 | import velox.api.layer1.data.OrderSendParameters; 19 | import velox.api.layer1.data.OrderStatus; 20 | import velox.api.layer1.data.OrderType; 21 | import velox.api.layer1.data.OrderUpdateParameters; 22 | import velox.api.layer1.data.SimpleOrderSendParameters; 23 | import velox.api.layer1.data.StatusInfoBuilder; 24 | import velox.api.layer1.data.SystemTextMessageType; 25 | 26 | /** 27 | *

28 | * This provider generates data according to same rules as parent provider, but 29 | * also has some trading capabilities. 30 | *

31 | * 32 | *

33 | * It does not aim to be realistic, so it's somewhat simplified. 34 | *

35 | */ 36 | @Layer1ApiVersion(Layer1ApiVersionValue.VERSION2) 37 | @Layer0LiveModule(fullName = "Demo external trading", shortName = "DT") 38 | public class DemoExternalRealtimeTradingProvider extends DemoExternalRealtimeProvider { 39 | 40 | AtomicInteger orderIdGenerator = new AtomicInteger(); 41 | AtomicInteger executionIdGenerator = new AtomicInteger(); 42 | 43 | private final Map workingOrders = new HashMap<>(); 44 | private final Map tradeAuditMap = new HashMap<>(); 45 | 46 | @Override 47 | public void sendOrder(OrderSendParameters orderSendParameters) { 48 | // Since we did not report OCO/OSO/Brackets support, this method can 49 | // only receive simple orders 50 | SimpleOrderSendParameters simpleParameters = (SimpleOrderSendParameters) orderSendParameters; 51 | 52 | // Detecting order type 53 | OrderType orderType = OrderType.getTypeFromPrices(simpleParameters.stopPrice, simpleParameters.limitPrice); 54 | 55 | // Even if order will be rejected provider should first acknowledge it 56 | // with PENDING_SUBMIT. 57 | // This allows Bookmap visualization to distinguish between orders that 58 | // were just sent and orders that were rejected earlier and now the last 59 | // state is reported 60 | // If your datasource does not provide some variation of PENDING_SUBMIT 61 | // status, you are advised to send a fake message with PENDING_SUBMIT 62 | // before reporting REJECT - this will make Bookmap consider all rejects 63 | // to be new ones instead of historical ones 64 | final OrderInfoBuilder builder = new OrderInfoBuilder( 65 | simpleParameters.alias, 66 | // ID should normally be generated by exchange 67 | "o" + orderIdGenerator.incrementAndGet(), 68 | simpleParameters.isBuy, 69 | orderType, 70 | simpleParameters.clientId, 71 | simpleParameters.doNotIncrease); 72 | // You need to set these fields, otherwise Bookmap might not handle 73 | // order correctly 74 | builder.setStopPrice(simpleParameters.stopPrice) 75 | .setLimitPrice(simpleParameters.limitPrice) 76 | .setUnfilled(simpleParameters.size) 77 | .setDuration(OrderDuration.GTC) 78 | .setStatus(OrderStatus.PENDING_SUBMIT); 79 | tradingListeners.forEach(l -> l.onOrderUpdated(builder.build())); 80 | // Marking all fields as unchanged, since they were just reported and 81 | // fields will be marked as changed automatically when modified. 82 | builder.markAllUnchanged(); 83 | 84 | // First, since we are not going to emulate stop or market orders in 85 | // this demo, 86 | // let's reject anything except for Limit and Market orders. 87 | if (orderType != OrderType.LMT && orderType != OrderType.MKT) { 88 | // Necessary fields are already populated, so just change status to 89 | // rejected and send 90 | builder.setStatus(OrderStatus.REJECTED); 91 | tradingListeners.forEach(l -> l.onOrderUpdated(builder.build())); 92 | builder.markAllUnchanged(); 93 | 94 | // Provider can complain to user here explaining what was done wrong 95 | adminListeners.forEach(l -> l.onSystemTextMessage("This provider only supports market and limit orders", 96 | SystemTextMessageType.ORDER_FAILURE)); 97 | } else { 98 | // Placing it into list of working orders so it will be simulated. 99 | // Synchronizing since trading simulation will be done in different 100 | // thread 101 | synchronized (workingOrders) { 102 | workingOrders.put(builder.getOrderId(), builder); 103 | } 104 | 105 | // We are going to simulate this order, entering WORKING state 106 | builder.setStatus(OrderStatus.WORKING); 107 | tradingListeners.forEach(l -> l.onOrderUpdated(builder.build())); 108 | builder.markAllUnchanged(); 109 | } 110 | 111 | } 112 | 113 | @Override 114 | public void updateOrder(OrderUpdateParameters orderUpdateParameters) { 115 | 116 | // OrderMoveToMarketParameters will not be sent as we did not declare 117 | // support for it, 3 other requests remain 118 | 119 | synchronized (workingOrders) { 120 | // instanceof is not recommended here because subclass, if it appears, 121 | // will anyway mean an action that existing code can not process as 122 | // expected 123 | if (orderUpdateParameters.getClass() == OrderCancelParameters.class) { 124 | 125 | // Cancel order with provided ID 126 | OrderCancelParameters orderCancelParameters = (OrderCancelParameters) orderUpdateParameters; 127 | OrderInfoBuilder order = workingOrders.remove(orderCancelParameters.orderId); 128 | order.setStatus(OrderStatus.CANCELLED); 129 | tradingListeners.forEach(l -> l.onOrderUpdated(order.build())); 130 | 131 | } else if (orderUpdateParameters.getClass() == OrderResizeParameters.class) { 132 | 133 | // Resize order with provided ID 134 | OrderResizeParameters orderResizeParameters = (OrderResizeParameters) orderUpdateParameters; 135 | OrderInfoBuilder order = workingOrders.get(orderResizeParameters.orderId); 136 | order.setUnfilled(orderResizeParameters.size); 137 | tradingListeners.forEach(l -> l.onOrderUpdated(order.build())); 138 | 139 | } else if (orderUpdateParameters.getClass() == OrderMoveParameters.class) { 140 | 141 | // Change stop/limit prices of an order with provided ID 142 | OrderMoveParameters orderMoveParameters = (OrderMoveParameters) orderUpdateParameters; 143 | OrderInfoBuilder order = workingOrders.get(orderMoveParameters.orderId); 144 | // No need to update stop price as this demo only supports limit 145 | // and market orders 146 | order.setLimitPrice(orderMoveParameters.limitPrice); 147 | tradingListeners.forEach(l -> l.onOrderUpdated(order.build())); 148 | 149 | // New price might trigger execution 150 | simulateOrders(); 151 | 152 | } else { 153 | throw new UnsupportedOperationException("Unsupported order type"); 154 | } 155 | } 156 | } 157 | 158 | 159 | @Override 160 | public Layer1ApiProviderSupportedFeatures getSupportedFeatures() { 161 | // Expanding parent supported features, reporting basic trading support 162 | return super.getSupportedFeatures() 163 | .toBuilder() 164 | .setTrading(true) 165 | .setSupportedOrderDurations(Arrays.asList(OrderDuration.GTC)) 166 | // At the moment of writing this method it was not possible to 167 | // report limit orders support, but no stop orders support 168 | // If you actually need it, you can report stop orders support 169 | // but reject stop orders when those are sent. 170 | .setSupportedStopOrders(Arrays.asList(OrderType.LMT, OrderType.MKT)) 171 | .build(); 172 | } 173 | 174 | @Override 175 | protected void simulate() { 176 | // Perform data changes simulation 177 | super.simulate(); 178 | 179 | simulateOrders(); 180 | updateTradeAuditInfo(); 181 | } 182 | 183 | public void simulateOrders() { 184 | // Simulate order executions 185 | synchronized (workingOrders) { 186 | synchronized (instruments) { 187 | // Purging orders that are no longer working - those do not have 188 | // to be simulated 189 | workingOrders.values().removeIf(o -> o.getStatus() != OrderStatus.WORKING); 190 | 191 | for (OrderInfoBuilder order : workingOrders.values()) { 192 | String alias = order.getInstrumentAlias(); 193 | Instrument instrument = instruments.get(alias); 194 | 195 | // Only simulating if user is subscribed to instrument - 196 | // this is because we do not generate data when there is no 197 | // subscription 198 | if (instrument != null) { 199 | // Determining on which price level order can be 200 | // executed. Note the multiplication by pips part - 201 | // that's because order price is a raw value and 202 | // instrument bid/ask are level numbers. 203 | 204 | double bestPrice = order.isBuy() 205 | ? instrument.getBestAsk() * instrument.pips 206 | : instrument.getBestBid() * instrument.pips; 207 | 208 | boolean shouldBeExecuted = order.getType() == OrderType.MKT || 209 | (order.isBuy() 210 | ? bestPrice <= order.getLimitPrice() 211 | : bestPrice >= order.getLimitPrice()); 212 | 213 | if (shouldBeExecuted) { 214 | // For simplicity fully executing order with the 215 | // best price 216 | 217 | // Reporting executions. 218 | int unfilled = order.getUnfilled(); 219 | // Generating id for execution - usually will be 220 | // received from exchange 221 | final String executionId = "e" + executionIdGenerator.incrementAndGet(); 222 | final long executionTime = System.currentTimeMillis(); 223 | // Note that last parameter is execution time. While 224 | // time of event itself can be derived from the time 225 | // when it was sent for realtime executions, you are 226 | // allowed to send historical executions for those 227 | // to be displayed in account info panel. 228 | ExecutionInfo executionInfo = new ExecutionInfo(order.getOrderId(), unfilled, bestPrice, 229 | executionId, executionTime); 230 | tradingListeners.forEach(l -> l.onOrderExecuted(executionInfo)); 231 | 232 | // Changing the order itself 233 | order.setAverageFillPrice(bestPrice); 234 | order.setUnfilled(0); 235 | order.setFilled(unfilled); 236 | order.setStatus(OrderStatus.FILLED); 237 | tradingListeners.forEach(l -> l.onOrderUpdated(order.build())); 238 | order.markAllUnchanged(); 239 | 240 | synchronized (tradeAuditMap) { 241 | TradeAudit tradeAudit = tradeAuditMap.computeIfAbsent(alias, k -> new TradeAudit()); 242 | tradeAudit.recalculateInfo(order.isBuy(), executionInfo); 243 | } 244 | } 245 | } 246 | } 247 | } 248 | } 249 | } 250 | 251 | private void updateTradeAuditInfo() { 252 | synchronized (instruments) { 253 | synchronized (tradeAuditMap) { 254 | for (String alias : instruments.keySet()) { 255 | Instrument instrument = instruments.get(alias); 256 | TradeAudit tradeAudit = tradeAuditMap.computeIfAbsent(alias, (k) -> new TradeAudit()); 257 | 258 | double bestPrice = tradeAudit.position > 0 259 | ? instrument.getBestBid() 260 | : instrument.getBestAsk(); 261 | 262 | double theoreticalExitPrice = bestPrice * instrument.pips; 263 | double unrealizedPnl = tradeAudit.getUnrealizedPnl(theoreticalExitPrice); 264 | 265 | StatusInfoBuilder statusInfoBuilder = new StatusInfoBuilder() 266 | .setInstrumentAlias(alias) 267 | .setAveragePrice(tradeAudit.averagePrice) 268 | .setPosition(tradeAudit.position) 269 | .setRealizedPnl(tradeAudit.realizedPnl) 270 | .setUnrealizedPnl(unrealizedPnl) 271 | .setVolume(tradeAudit.volume); 272 | 273 | tradingListeners.forEach(t -> t.onStatus(statusInfoBuilder.build())); 274 | } 275 | } 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/live/TradeAudit.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.live; 2 | 3 | import velox.api.layer1.data.ExecutionInfo; 4 | 5 | import java.math.BigDecimal; 6 | import java.math.MathContext; 7 | 8 | import static java.math.BigDecimal.valueOf; 9 | 10 | /** 11 | * A container for a position and P&L information 12 | */ 13 | class TradeAudit { 14 | int position; 15 | int volume; 16 | double realizedPnl; 17 | double averagePrice = Double.NaN; 18 | 19 | private BigDecimal position() { 20 | return valueOf(position); 21 | } 22 | 23 | private BigDecimal realizedPnl() { 24 | return valueOf(realizedPnl); 25 | } 26 | 27 | private BigDecimal averagePrice() { 28 | return valueOf(averagePrice); 29 | } 30 | 31 | /** 32 | * Unrealized P&L can be calculated as: 33 | * (Theoretical Exit Price – Average Open Price) * Position 34 | */ 35 | double getUnrealizedPnl(double theoreticalExitPrice) { 36 | if (position == 0) { 37 | return 0; 38 | } 39 | 40 | BigDecimal diff = valueOf(theoreticalExitPrice).subtract(averagePrice()); 41 | return diff.multiply(position()).doubleValue(); 42 | } 43 | 44 | /** 45 | * Recalculates trade audit information on each order execution 46 | * 47 | * @param isBuy Side of execution (buy/sell) 48 | * @param info Information about order execution 49 | */ 50 | void recalculateInfo(boolean isBuy, ExecutionInfo info) { 51 | volume += info.size; 52 | 53 | double oldPosition = position; 54 | position += isBuy ? info.size : -info.size; 55 | 56 | if (oldPosition == 0) { 57 | // open a position 58 | averagePrice = info.price; 59 | } else if (oldPosition > 0 && isBuy || oldPosition < 0 && !isBuy) { 60 | // the case when increasing the existing position long or short 61 | BigDecimal totalPrice = valueOf(oldPosition).abs() 62 | .multiply(averagePrice(), MathContext.DECIMAL64) 63 | .add(valueOf(info.size).multiply(valueOf(info.price)), MathContext.DECIMAL64); 64 | 65 | averagePrice = totalPrice 66 | .divide(position().abs(), MathContext.DECIMAL64) 67 | .doubleValue(); 68 | } else { 69 | // the case when reducing the existing position, 70 | // also making a check if a counter-side position was opened 71 | // e.g. the current position is 2, we're selling 3 contracts 72 | // and now the position is -1 73 | double oldPosAbs = Math.abs(oldPosition); 74 | double minQty = Math.min(oldPosAbs, info.size); 75 | 76 | // PnL realized += (Sell Price - Buy Price) * Qty 77 | BigDecimal sellPrice = isBuy ? averagePrice() : BigDecimal.valueOf(info.price); 78 | BigDecimal buyPrice = isBuy ? BigDecimal.valueOf(info.price) : averagePrice(); 79 | BigDecimal priceDiff = sellPrice.subtract(buyPrice); 80 | 81 | realizedPnl = realizedPnl() 82 | .add(priceDiff.multiply(valueOf(minQty), MathContext.DECIMAL64)) 83 | .doubleValue(); 84 | if (oldPosAbs >= info.size) { 85 | // avg open price does not change since we're reducing the position 86 | // but set it to NaN if the position is closed (0) 87 | averagePrice = position == 0 ? Double.NaN : averagePrice; 88 | } else { 89 | // for a counter-side position, 90 | // the average price will be the latest execution price 91 | averagePrice = info.price; 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/live/advanced/CrossPlatformTradingProvider.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.live.advanced; 2 | 3 | import java.util.Collections; 4 | import java.util.Optional; 5 | import java.util.Set; 6 | 7 | import velox.api.layer0.annotations.Layer0LiveModule; 8 | import velox.api.layer0.live.DemoExternalRealtimeTradingProvider; 9 | import velox.api.layer1.annotations.Layer1ApiVersion; 10 | import velox.api.layer1.annotations.Layer1ApiVersionValue; 11 | import velox.api.layer1.data.InstrumentCoreInfo; 12 | import velox.api.layer1.data.Layer1ApiProviderSupportedFeatures; 13 | import velox.api.layer1.data.OrderSendParameters; 14 | import velox.api.layer1.data.SimpleOrderSendParameters; 15 | import velox.api.layer1.data.SymbolMappingInfo; 16 | import velox.api.layer1.messages.Layer1ApiGetAliasMessage; 17 | 18 | /** 19 | *

20 | * This provider is an example of trading-only provider (designed to be used 21 | * with separate data source) 22 | *

23 | *

24 | * This demo won't look very nice when used and only exists to illustrate the 25 | * technical part. Some things to keep in mind: 26 | *

    27 | *
  • Prices where trading happens will not match displayed data (because both 28 | * are independently generated randomly)
  • 29 | *
  • Position isn't reported to Bookmap (same as in parent class), this 30 | * prevents you from using built-in position close command. 31 | *
32 | *

33 | */ 34 | @Layer1ApiVersion(Layer1ApiVersionValue.VERSION2) 35 | @Layer0LiveModule(fullName = "Cross-platform trading demo", shortName = "DCT") 36 | public class CrossPlatformTradingProvider extends DemoExternalRealtimeTradingProvider { 37 | 38 | @Override 39 | public Layer1ApiProviderSupportedFeatures getSupportedFeatures() { 40 | // Declaring cross-platform trading functionality. 41 | // Trading-related capabilities will be extracted from here. Parent class will 42 | // declare basic trading support. 43 | return super.getSupportedFeatures().toBuilder() 44 | // Add cross trading from Random and Rithmic 45 | .setSymbolsMappingFunction(alternatives -> { 46 | 47 | Optional crossTradingInstrument = alternatives.stream() 48 | .filter(a -> a.type.endsWith("EXT:" + CrossPlatformTradingProvider.class.getName())) 49 | .findAny(); 50 | 51 | if (crossTradingInstrument.isPresent()) { 52 | // This is our own instrument that we most likely just defined. 53 | // Need to at least provide pips/multiplier for it 54 | return new SymbolMappingInfo(Collections.emptySet(), Collections.emptySet(), 1, price -> 0.25); 55 | } else { 56 | Optional sourceInstrument = alternatives.stream() 57 | .filter(a -> a.type.endsWith("@RANDOM") || a.type.endsWith("@RITHMIC")) 58 | .findAny(); 59 | Optional mappingInfo = sourceInstrument.map(instrument -> { 60 | String crossTradingTargetType = instrument.type 61 | .replaceAll("RANDOM|RITHMIC", "EXT:" + CrossPlatformTradingProvider.class.getName()); 62 | InstrumentCoreInfo crossTradingTarget = new InstrumentCoreInfo( 63 | instrument.symbol, 64 | instrument.exchange, 65 | crossTradingTargetType); 66 | // Note: in this case you can pass this instrument as either alternative or 67 | // a cross trading target (so as a either first or second parameter). This 68 | // will produce similar immediate effect, but will change the potential 69 | // effects later: alternative means that it's the same instrument, just in 70 | // another connection syntax. Cross trading target (crossTradingTo field) 71 | // means that it's a different symbol that we can cross trade to (like ES->MES) 72 | return new SymbolMappingInfo( 73 | Collections.emptySet(), 74 | Set.of(crossTradingTarget), 75 | 1, price -> 0.25); 76 | }); 77 | return mappingInfo.orElse(null); 78 | } 79 | }) 80 | .build(); 81 | } 82 | 83 | @Override 84 | public void sendOrder(OrderSendParameters orderSendParameters) { 85 | 86 | // Since we use trading simulated by DemoExternalRealtimeTradingProvider we need to initiate subscription 87 | 88 | // Since we did not report OCO/OSO/Brackets support, this method can 89 | // only receive simple orders 90 | SimpleOrderSendParameters simpleParameters = (SimpleOrderSendParameters) orderSendParameters; 91 | String alias = simpleParameters.alias; 92 | 93 | // Normally we should understand alias and be able to map it to corresponding 94 | // instrument. 95 | // E.g. receiving order for ESZ9.CME from Rithmic would mean it should be sent 96 | // to an instrument with symbol ESZ9 and exchange CME. 97 | // In this demo we just create instrument with provided alias to start 98 | // (very unrealistic) trading simulation. 99 | final Instrument newInstrument = new Instrument(alias, 0.25); 100 | instruments.putIfAbsent(alias, newInstrument); 101 | 102 | super.sendOrder(orderSendParameters); 103 | } 104 | 105 | @Override 106 | public Object sendUserMessage(Object data) { 107 | // For cross-trading to work it's important to return alias of the instrument 108 | if (data.getClass() == Layer1ApiGetAliasMessage.class) { 109 | return ((Layer1ApiGetAliasMessage)data).symbol; 110 | } 111 | 112 | return super.sendUserMessage(data); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/live/advanced/DemoExternalRealtimeUserMessageProvider.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.live.advanced; 2 | 3 | import velox.api.layer0.annotations.Layer0LiveModule; 4 | import velox.api.layer0.live.DemoExternalRealtimeProvider; 5 | import velox.api.layer1.annotations.Layer1ApiVersion; 6 | import velox.api.layer1.annotations.Layer1ApiVersionValue; 7 | import velox.api.layer1.reading.UserDataUserMessage; 8 | 9 | import java.math.BigInteger; 10 | 11 | /** 12 | * This provider generates data according to same rules as parent provider, but also generates UserDataUserMessages 13 | * for related demo (see UserDataUserMessageDemo from the DemoStrategiesrepo.). 14 | */ 15 | @Layer1ApiVersion(Layer1ApiVersionValue.VERSION2) 16 | @Layer0LiveModule(fullName = "Demo external user message", shortName = "DU") 17 | public class DemoExternalRealtimeUserMessageProvider extends DemoExternalRealtimeProvider { 18 | 19 | private static final String RANDOM_DATA_TAG = "RandomData"; 20 | 21 | @Override 22 | protected void simulate() { 23 | // Perform data changes simulation 24 | super.simulate(); 25 | synchronized (instruments) { 26 | instruments.forEach(this::simulateRandomData); 27 | } 28 | } 29 | 30 | private void simulateRandomData(String alias, Instrument instrument) { 31 | if (Math.random() > 0.9) { 32 | // In 10% cases we send UserDataUserMessage with tag RandomData. 33 | // We send a random integer number not far from BBO as a byte array. 34 | int medium = (instrument.getBestAsk() + instrument.getBestBid()) / 2; 35 | BigInteger randomResult = BigInteger.valueOf(medium + (int) (Math.random() * 20) - 10); 36 | byte[] data = randomResult.toByteArray(); 37 | 38 | // In 30% cases we send global user message (alias = null), in other cases - aliased user message. 39 | String aliasResult = Math.random() > 0.3 ? alias : null; 40 | 41 | adminListeners.forEach(l -> l.onUserMessage( 42 | new UserDataUserMessage(RANDOM_DATA_TAG, aliasResult, data) 43 | )); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/live/advanced/FullTextDataRealtimeProvider.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.live.advanced; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | import java.nio.charset.StandardCharsets; 6 | import java.util.Map; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | 9 | import velox.api.layer0.annotations.Layer0LiveModule; 10 | import velox.api.layer0.common.TextStreamParser; 11 | import velox.api.layer0.data.FileEndReachedUserMessage; 12 | import velox.api.layer1.annotations.Layer1ApiVersion; 13 | import velox.api.layer1.annotations.Layer1ApiVersionValue; 14 | import velox.api.layer1.common.ListenableHelper; 15 | import velox.api.layer1.common.Log; 16 | import velox.api.layer1.data.InstrumentInfo; 17 | import velox.api.layer1.data.Layer1ApiProviderSupportedFeatures; 18 | import velox.api.layer1.data.Layer1ApiProviderSupportedFeaturesBuilder; 19 | import velox.api.layer1.data.LoginData; 20 | import velox.api.layer1.data.LoginFailedReason; 21 | import velox.api.layer1.data.OrderSendParameters; 22 | import velox.api.layer1.data.OrderUpdateParameters; 23 | import velox.api.layer1.data.SubscribeInfo; 24 | import velox.api.layer1.data.UserPasswordDemoLoginData; 25 | import velox.api.layer1.layers.Layer1ApiRelay; 26 | import velox.api.layer1.providers.helper.PriceFormatHelper; 27 | 28 | /** 29 | *

30 | * This a an example of how you can interface bookmap with external executable using standard input/output. 31 | *

32 | */ 33 | @Layer1ApiVersion(Layer1ApiVersionValue.VERSION2) 34 | @Layer0LiveModule(fullName = "Text external realtime", shortName = "TE") 35 | public class FullTextDataRealtimeProvider extends Layer1ApiRelay { 36 | 37 | /** Subprocess that will do all the job */ 38 | private Process childProcess = null; 39 | private OutputStream outputStream = null; 40 | 41 | /** Parser for the data received from the subprocess */ 42 | private TextStreamParser parser; 43 | 44 | /** Used to provide price formatting on Java side (for simplicity). */ 45 | private final Map pipsMap = new ConcurrentHashMap<>(); 46 | 47 | public FullTextDataRealtimeProvider() { 48 | super(null); 49 | } 50 | 51 | @Override 52 | public void subscribe(SubscribeInfo subscribeInfo) { 53 | send("subscribe", 54 | subscribeInfo.symbol, 55 | subscribeInfo.exchange, 56 | subscribeInfo.type); 57 | } 58 | 59 | @Override 60 | public void unsubscribe(String alias) { 61 | send("unsubscribe", alias); 62 | } 63 | 64 | @Override 65 | public void login(LoginData loginData) { 66 | UserPasswordDemoLoginData userPasswordDemoLoginData = (UserPasswordDemoLoginData) loginData; 67 | 68 | // If startup process takes a while then it's better to do it in a separate thread 69 | // When doing in same thread UI can block until this method returns 70 | handleLogin(userPasswordDemoLoginData); 71 | } 72 | 73 | private void handleLogin(UserPasswordDemoLoginData userPasswordDemoLoginData) { 74 | 75 | if (childProcess == null) { 76 | try { 77 | // Starting the adapter itself. 78 | // Let's start batch file that will run actual provider 79 | // (so you can edit the file without rebuilding the jar). 80 | // Running executable directly might be preferred in real use case. 81 | childProcess = Runtime.getRuntime().exec("adapter.bat"); 82 | outputStream = childProcess.getOutputStream(); 83 | } catch (IOException e) { 84 | Log.error("Failed to start adapter", e); 85 | // Report failed login 86 | adminListeners.forEach(l -> l.onLoginFailed(LoginFailedReason.FATAL, 87 | "Failed to start the adapter executable, see log for details.\n" 88 | + "To run this demo you need to compile a provider from" 89 | + " cpp/FullTextDataRealtimeProvider subfolder\n" 90 | + "and create adapter.bat pointing to it in Config folder first\n" 91 | + "\n" 92 | + "Error: " + e.getMessage())); 93 | } 94 | } 95 | 96 | if (childProcess != null) { 97 | send("login", 98 | userPasswordDemoLoginData.user, 99 | userPasswordDemoLoginData.password, 100 | userPasswordDemoLoginData.isDemo ? "true" : "false"); 101 | 102 | // Parser might be already initialized from the previous attempt 103 | if (parser == null) { 104 | parser = new TextStreamParser(); 105 | ListenableHelper.addListeners(parser, this); 106 | parser.start(childProcess.getInputStream()); 107 | } 108 | } 109 | } 110 | 111 | @Override 112 | public void onUserMessage(Object data) { 113 | if (data instanceof FileEndReachedUserMessage) { 114 | // Ignore it - this means stream end was reached 115 | } else { 116 | super.onUserMessage(data); 117 | } 118 | } 119 | 120 | @Override 121 | public String getSource() { 122 | return "External executable"; 123 | } 124 | 125 | @Override 126 | public void onInstrumentAdded(String alias, InstrumentInfo instrumentInfo) { 127 | // Intercepting instrument additions and remembering minimal increment to use later 128 | pipsMap.put(alias, instrumentInfo.pips); 129 | 130 | super.onInstrumentAdded(alias, instrumentInfo); 131 | } 132 | 133 | @Override 134 | public String formatPrice(String alias, double price) { 135 | // Formatting could be moved into native code too 136 | // if advanced logic is needed. 137 | // One way to do it would be assigning request some ID and then waiting 138 | // for a response marked by the same ID to be printed by the executable 139 | return PriceFormatHelper.formatPriceDefault(pipsMap.get(alias), price); 140 | } 141 | 142 | /** 143 | * Send string to executable. We'll use primitive format just to simplify 144 | * parsing (generating JSON is much simpler than parsing it). It's a bit 145 | * inconsistent, but easier to use this way. Feel free to replace this by more 146 | * advanced format if it suits your code better. 147 | */ 148 | private void send(String... lines) { 149 | try { 150 | StringBuilder builder = new StringBuilder(); 151 | for (String line : lines) { 152 | builder.append(line); 153 | builder.append('\n'); 154 | } 155 | byte[] data = builder.toString().getBytes(StandardCharsets.US_ASCII); 156 | outputStream.write(data); 157 | outputStream.flush(); 158 | } catch (IOException e) { 159 | throw new RuntimeException(e); 160 | } 161 | } 162 | 163 | @Override 164 | public void close() { 165 | if (childProcess != null) { 166 | send("close"); 167 | try { 168 | childProcess.waitFor(); 169 | } catch (InterruptedException e) { 170 | throw new RuntimeException("Interrupted while waiting" 171 | + " for the subprocess to terminate", e); 172 | } 173 | } 174 | if (parser != null) { 175 | parser.close(); 176 | } 177 | } 178 | 179 | @Override 180 | public Layer1ApiProviderSupportedFeatures getSupportedFeatures() { 181 | return new Layer1ApiProviderSupportedFeaturesBuilder().build(); 182 | } 183 | 184 | @Override 185 | public void sendOrder(OrderSendParameters orderSendParameters) { 186 | // This method will not be called because this adapter does not report 187 | // trading capabilities 188 | // It could be forwarded into the executable in a similar way, but it would be 189 | // important to define a protocol to not lose types in the process 190 | // (e.g. print types along with other data) 191 | throw new UnsupportedOperationException("Not trading capable"); 192 | } 193 | 194 | @Override 195 | public void updateOrder(OrderUpdateParameters orderUpdateParameters) { 196 | // This method will not be called because this adapter does not report 197 | // trading capabilities 198 | // It could be forwarded into the executable in a similar way, but it would be 199 | // (e.g. print types along with other data) 200 | throw new UnsupportedOperationException("Not trading capable"); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/replay/DemoGeneratorReplayProvider.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.replay; 2 | 3 | 4 | import java.awt.Color; 5 | import java.awt.image.BufferedImage; 6 | import java.io.IOException; 7 | 8 | import javax.imageio.ImageIO; 9 | 10 | import velox.api.layer0.annotations.Layer0ReplayModule; 11 | import velox.api.layer0.data.FileEndReachedUserMessage; 12 | import velox.api.layer0.data.FileNotSupportedUserMessage; 13 | import velox.api.layer0.data.IndicatorDefinitionUserMessage; 14 | import velox.api.layer0.data.IndicatorPointUserMessage; 15 | import velox.api.layer0.data.OrderQueuePositionUserMessage; 16 | import velox.api.layer0.data.ReadFileLoginData; 17 | import velox.api.layer0.live.DemoExternalRealtimeTradingProvider; 18 | import velox.api.layer1.annotations.Layer1ApiVersion; 19 | import velox.api.layer1.annotations.Layer1ApiVersionValue; 20 | import velox.api.layer1.data.ExecutionInfo; 21 | import velox.api.layer1.data.InstrumentInfo; 22 | import velox.api.layer1.data.LoginData; 23 | import velox.api.layer1.data.OrderDuration; 24 | import velox.api.layer1.data.OrderInfoBuilder; 25 | import velox.api.layer1.data.OrderStatus; 26 | import velox.api.layer1.data.OrderType; 27 | import velox.api.layer1.data.TradeInfo; 28 | 29 | /** 30 | *

31 | * Instead of actually reading the file generates data, so you can select any 32 | * file with this one loaded. 33 | *

34 | *

35 | * Illustrates how to manipulate order queue position and display legacy API 36 | * indicators. 37 | *

38 | *

39 | * This should simplify transition for those who used "Recorder API". API used 40 | * in this example will be removed in the future in favor of L2 API based 41 | * solution. 42 | *

43 | *

44 | * It's newer version of BookmapRecorderDemo. Main differences are: 45 | *

    46 | *
  • Depth update and trade update prices are divided by pips now
  • 47 | *
  • Indicator updates and order updates are now sent differently.
  • 48 | *
49 | *

50 | *

51 | * For more details on working with orders see 52 | * {@link DemoExternalRealtimeTradingProvider} 53 | *

54 | */ 55 | @Layer1ApiVersion(Layer1ApiVersionValue.VERSION2) 56 | @Layer0ReplayModule 57 | public class DemoGeneratorReplayProvider extends ExternalReaderBaseProvider { 58 | /** 59 | * Some point in time, just for convenience (nanoseconds) 60 | */ 61 | private static final long INITIAL_TIME = 1400000000_000000000L; 62 | 63 | /** 64 | * Number of nanoseconds in one second 65 | */ 66 | private static final long NS_IN_SEC = 1_000_000_000L; 67 | 68 | private Thread readerThread; 69 | private long currentTime = 0; 70 | 71 | @Override 72 | public void login(LoginData loginData) { 73 | // We are not going to really read the file - just launch the reader thread. 74 | ReadFileLoginData fileData = (ReadFileLoginData) loginData; 75 | 76 | try { 77 | // For demo purposes let's just check the extension. 78 | // Usually you will want to take a look at file content here to 79 | // ensure it's expected file format 80 | if (!fileData.file.getName().endsWith(".simpleformat2.txt")) { 81 | throw new IOException("File extension not supported"); 82 | } else { 83 | readerThread = new Thread(this::read); 84 | readerThread.start(); 85 | 86 | currentTime = INITIAL_TIME; 87 | } 88 | } catch (@SuppressWarnings("unused") IOException e) { 89 | adminListeners.forEach(listener -> listener.onUserMessage(new FileNotSupportedUserMessage())); 90 | } 91 | } 92 | 93 | /** 94 | * 95 | */ 96 | private void read() { 97 | // instrument1 defined 1 second after feed is started 98 | currentTime += 1 * NS_IN_SEC; 99 | InstrumentInfo instrument1 = new InstrumentInfo("Test instrument", null, null, 25, 1, "Test instrument - full name", false); 100 | instrumentListeners.forEach(l -> l.onInstrumentAdded("Test instrument", instrument1)); 101 | 102 | // And the second instrument is defined at the same point in time 103 | InstrumentInfo instrument2 = new InstrumentInfo("Test instrument 2", null, null, 10, 1, "Test instrument 2 - full name", false); 104 | instrumentListeners.forEach(l -> l.onInstrumentAdded("Test instrument 2", instrument2)); 105 | 106 | currentTime += NS_IN_SEC; 107 | 108 | // Let's generate 10 bid + 10 ask levels for 1'st instrument, and 5+5 for second. 109 | for (int i = 1; i <= 10; ++i) { 110 | // Defining final version of i to allow using it inside lambda 111 | final int q = i; 112 | final int sizeBid = i * 22; 113 | dataListeners.forEach(l -> l.onDepth("Test instrument", true, 40 - q, sizeBid)); 114 | 115 | final int sizeAsk = i * 15; 116 | dataListeners.forEach(l -> l.onDepth("Test instrument", false, 40 + q, sizeAsk)); 117 | } 118 | for (int i = 1; i <= 5; ++i) { 119 | // Defining final version of i to allow using it inside lambda 120 | final int q = i; 121 | final int sizeBid = i * 2; 122 | dataListeners.forEach(l -> l.onDepth("Test instrument 2", true, 500 - q, sizeBid)); 123 | 124 | final int sizeAsk = i * 1; 125 | dataListeners.forEach(l -> l.onDepth("Test instrument 2", false, 500 + q, sizeAsk)); 126 | } 127 | 128 | // Advance time 1 sec forward. 129 | currentTime += NS_IN_SEC; 130 | 131 | // Now let's start changing the data (for both instruments) 132 | for (int i = 0; i <= 50; ++i) { 133 | // Defining final version of i to allow using it inside lambda 134 | final int q = i; 135 | 136 | // Remove old level 137 | currentTime += NS_IN_SEC / 20; 138 | dataListeners.forEach(l -> l.onDepth("Test instrument", false, 40 + (q + 1), 0)); 139 | // Add new level 140 | final int sizeBid1 = q * 5 + 100; 141 | currentTime += NS_IN_SEC / 20; 142 | dataListeners.forEach(l -> l.onDepth("Test instrument", false, 40 + (q + 1 + 10), sizeBid1)); 143 | 144 | // Remove old level 145 | currentTime += NS_IN_SEC / 20; 146 | dataListeners.forEach(l -> l.onDepth("Test instrument", true, 40 + (q - 1 - 10), 0)); 147 | // Add new level 148 | final int sizeAsk1 = q * 10 + 100; 149 | currentTime += NS_IN_SEC / 20; 150 | dataListeners.forEach(l -> l.onDepth("Test instrument", true, 40 + (q - 1), sizeAsk1)); 151 | 152 | // Remove old level 153 | currentTime += NS_IN_SEC / 20; 154 | dataListeners.forEach(l -> l.onDepth("Test instrument 2", false, 500 + (-q + 1 + 5), 0)); 155 | // Add new level 156 | final int sizeBid2 = q * 5 + 100; 157 | currentTime += NS_IN_SEC / 20; 158 | dataListeners.forEach(l -> l.onDepth("Test instrument 2", false, 500 + (-q + 1), sizeBid2)); 159 | 160 | // Remove old level 161 | currentTime += NS_IN_SEC / 20; 162 | dataListeners.forEach(l -> l.onDepth("Test instrument 2", true, 500 + (-q - 1), 0)); 163 | // Add new level 164 | final int sizeAsk2 = q * 10 + 100; 165 | currentTime += NS_IN_SEC / 20; 166 | dataListeners.forEach(l -> l.onDepth("Test instrument 2", true, 500 + (-q - 1 - 5), sizeAsk2)); 167 | } 168 | 169 | 170 | BufferedImage icon; 171 | try { 172 | icon = ImageIO.read(DemoGeneratorReplayProvider.class.getResourceAsStream("/icon_accept.gif")); 173 | } catch (IOException e) { 174 | throw new RuntimeException("failed to load icon", e); 175 | } 176 | // Line and icons 177 | currentTime += NS_IN_SEC / 10; 178 | IndicatorDefinitionUserMessage indicatorDefinitionMessage = new IndicatorDefinitionUserMessage( 179 | 1, "Test instrument 2", "Indicator 1", 180 | (short)0xFFFF, (short)1, 1, Color.ORANGE, 181 | (short)0xFF08, (short)1, 2, 182 | icon, -icon.getWidth() / 2, -icon.getHeight() / 2, true); 183 | // No line, only icons 184 | // IndicatorDefinitionUserMessage indicatorDefinitionMessage = new IndicatorDefinitionUserMessage( 185 | // 1, "Test instrument 2", 186 | // (short)0x0000, (short)1, 1, Color.ORANGE, 187 | // (short)0x0000, (short)1, 2, Color.GREEN, 188 | // icon, -icon.getWidth() / 2, -icon.getHeight() / 2); 189 | // No icon, different line style 190 | // IndicatorDefinitionUserMessage indicatorDefinitionMessage = new IndicatorDefinitionUserMessage( 191 | // 1, "Test instrument 2", 192 | // (short)0x5555, (short)20, 5, Color.ORANGE, 193 | // (short)0x5555, (short)40, 10, Color.GREEN, 194 | // null, 0, 0); 195 | adminListeners.forEach(l -> l.onUserMessage(indicatorDefinitionMessage)); 196 | currentTime += NS_IN_SEC / 10; 197 | adminListeners.forEach(l -> l.onUserMessage(new IndicatorPointUserMessage(1, 4440.0))); 198 | currentTime += NS_IN_SEC; 199 | adminListeners.forEach(l -> l.onUserMessage(new IndicatorPointUserMessage(1, 4450.0))); 200 | currentTime += NS_IN_SEC; 201 | adminListeners.forEach(l -> l.onUserMessage(new IndicatorPointUserMessage(1, Double.NaN))); 202 | currentTime += NS_IN_SEC; 203 | adminListeners.forEach(l -> l.onUserMessage(new IndicatorPointUserMessage(1, 4450.0))); 204 | 205 | 206 | // Let's create a trade for the 2'nd instrument. We won't update depth data for simplicity. 207 | // Price is 4500.0 (pips is 10), size is 150, agressor is bid (last parameter of TradeInfo). 208 | dataListeners.forEach(l -> l.onTrade("Test instrument 2", 4500.0 / 10, 150, new TradeInfo(false, true))); 209 | 210 | 211 | // Let's create an order 212 | currentTime += NS_IN_SEC; 213 | OrderInfoBuilder order = new OrderInfoBuilder("Test instrument 2", "order1", false, OrderType.LMT, "client-id-1", false); 214 | order 215 | .setLimitPrice(4580) 216 | .setUnfilled(5) 217 | .setDuration(OrderDuration.GTC) 218 | .setStatus(OrderStatus.PENDING_SUBMIT); 219 | 220 | tradingListeners.forEach(l -> l.onOrderUpdated(order.build())); 221 | order.markAllUnchanged(); 222 | 223 | order.setStatus(OrderStatus.WORKING); 224 | tradingListeners.forEach(l -> l.onOrderUpdated(order.build())); 225 | order.markAllUnchanged(); 226 | 227 | // Let's record order position data. If you comment this out BookMap will compute position using built-in algorithms 228 | for (int position = 310 /* 315 is the size on the order's price level, order size is 5, so initially there are 310 shares before our order*/; 229 | position > 100; --position) { 230 | // Decreasing order position - will look like it advances to the head of the queue 231 | currentTime += NS_IN_SEC / 30; 232 | final OrderQueuePositionUserMessage positionMessage = new OrderQueuePositionUserMessage("order1", position); 233 | adminListeners.forEach(l -> l.onUserMessage(positionMessage)); 234 | } 235 | 236 | // Let's decrease the price 237 | currentTime += NS_IN_SEC; 238 | order.setLimitPrice(4480); 239 | tradingListeners.forEach(l -> l.onOrderUpdated(order.build())); 240 | order.markAllUnchanged(); 241 | 242 | // Let's execute the order 243 | currentTime += NS_IN_SEC; 244 | ExecutionInfo executionInfo = new ExecutionInfo( 245 | order.getOrderId(), 246 | 5, 247 | 4480, 248 | "execution-id-1", 249 | // Execution time in milliseconds. Used only in account information. 250 | currentTime / 1000_000); 251 | tradingListeners.forEach(l -> l.onOrderExecuted(executionInfo)); 252 | 253 | // And mark it as filled 254 | order.setFilled(5); 255 | order.setUnfilled(0); 256 | order.setStatus(OrderStatus.FILLED); 257 | tradingListeners.forEach(l -> l.onOrderUpdated(order.build())); 258 | order.markAllUnchanged(); 259 | 260 | // Report file end 261 | reportFileEnd(); 262 | } 263 | 264 | public void reportFileEnd() { 265 | adminListeners.forEach(listener -> listener.onUserMessage(new FileEndReachedUserMessage())); 266 | } 267 | 268 | @Override 269 | public long getCurrentTime() { 270 | return currentTime; 271 | } 272 | 273 | @Override 274 | public String getSource() { 275 | // String identifying where data came from. 276 | // For example you can use that later in your indicator. 277 | return "generated example data"; 278 | } 279 | 280 | @Override 281 | public void close() { 282 | readerThread.interrupt(); 283 | } 284 | 285 | } 286 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/replay/DemoTextDataReplayProvider.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.replay; 2 | 3 | 4 | import java.io.BufferedReader; 5 | import java.io.FileReader; 6 | import java.io.IOException; 7 | 8 | import com.google.gson.Gson; 9 | 10 | import velox.api.layer0.annotations.Layer0ReplayModule; 11 | import velox.api.layer0.data.FileEndReachedUserMessage; 12 | import velox.api.layer0.data.FileNotSupportedUserMessage; 13 | import velox.api.layer0.data.ReadFileLoginData; 14 | import velox.api.layer1.Layer1ApiListener; 15 | import velox.api.layer1.annotations.Layer1ApiVersion; 16 | import velox.api.layer1.annotations.Layer1ApiVersionValue; 17 | import velox.api.layer1.data.InstrumentInfo; 18 | import velox.api.layer1.data.LoginData; 19 | import velox.api.layer1.data.TradeInfo; 20 | import velox.api.layer1.reading.UserDataUserMessage; 21 | 22 | /** 23 | * Allows reading simple text format (that mimics {@link Layer1ApiListener} 24 | * methods) to be replayed by Bookmap. 25 | */ 26 | @Layer1ApiVersion(Layer1ApiVersionValue.VERSION2) 27 | @Layer0ReplayModule 28 | public class DemoTextDataReplayProvider extends ExternalReaderBaseProvider { 29 | 30 | private final Gson gson = new Gson(); 31 | 32 | private Thread readerThread; 33 | private long currentTime = 0; 34 | 35 | private boolean play = true; 36 | 37 | private BufferedReader reader; 38 | 39 | @Override 40 | public void login(LoginData loginData) { 41 | ReadFileLoginData fileData = (ReadFileLoginData) loginData; 42 | 43 | try { 44 | // For demo purposes let's just check the extension. 45 | // Usually you will want to take a look at file content here to 46 | // ensure it's expected file format 47 | if (!fileData.file.getName().endsWith(".simpleformat.txt")) { 48 | throw new IOException("File extension not supported"); 49 | } else { 50 | 51 | reader = new BufferedReader(new FileReader(fileData.file)); 52 | 53 | // Reading one line to guarantee that when we exit this method 54 | // getCurrentTime will return meaningful result. 55 | // Alternative is to wait for first line to be read by 56 | // readerThread 57 | // Reading it here also allows a bit of extra validation, since 58 | // in case of error it's still possible to report that file is 59 | // not supported 60 | readLine(); 61 | 62 | readerThread = new Thread(this::read); 63 | readerThread.start(); 64 | } 65 | } catch (@SuppressWarnings("unused") IOException e) { 66 | adminListeners.forEach(listener -> listener.onUserMessage(new FileNotSupportedUserMessage())); 67 | } 68 | } 69 | 70 | private void read() { 71 | try { 72 | while (!Thread.interrupted() && play) { 73 | readLine(); 74 | } 75 | } catch (@SuppressWarnings("unused") IOException e) { 76 | reportFileEnd(); 77 | } 78 | } 79 | 80 | public void reportFileEnd() { 81 | adminListeners.forEach(listener -> listener.onUserMessage(new FileEndReachedUserMessage())); 82 | play = false; 83 | } 84 | 85 | private void readLine() throws IOException { 86 | String line = reader.readLine(); 87 | if (line == null && play) { 88 | reportFileEnd(); 89 | } else { 90 | String[] tokens = line.split(";;;"); 91 | currentTime = Long.parseLong(tokens[0]); 92 | String eventCode = tokens[1]; 93 | switch (eventCode) { 94 | case "onInstrumentAdded": { 95 | String alias = tokens[2]; 96 | InstrumentInfo instrumentInfo = gson.fromJson(tokens[3], InstrumentInfo.class); 97 | instrumentListeners.forEach( 98 | l -> l.onInstrumentAdded(alias, instrumentInfo)); 99 | break; 100 | } 101 | case "onTrade": { 102 | String alias = tokens[2]; 103 | double price = Double.parseDouble(tokens[3]); 104 | int size = Integer.parseInt(tokens[4]); 105 | TradeInfo tradeInfo = gson.fromJson(tokens[5], TradeInfo.class); 106 | dataListeners.forEach( 107 | l -> l.onTrade(alias, price, size, tradeInfo)); 108 | break; 109 | } 110 | case "onDepth": { 111 | String alias = tokens[2]; 112 | boolean isBid = Boolean.parseBoolean(tokens[3]); 113 | int price = Integer.parseInt(tokens[4]); 114 | int size = Integer.parseInt(tokens[5]); 115 | 116 | dataListeners.forEach( 117 | l -> l.onDepth(alias, isBid, price, size)); 118 | break; 119 | } 120 | case "onUserDataUserMessage": { 121 | String tag = tokens[2]; 122 | String alias = tokens[3]; 123 | byte[] data = tokens[4].getBytes(); 124 | 125 | adminListeners.forEach( 126 | l -> l.onUserMessage( 127 | new UserDataUserMessage(tag, alias, data) 128 | )); 129 | break; 130 | } 131 | 132 | default: 133 | reportFileEnd(); 134 | throw new RuntimeException("Unknown event code " + eventCode); 135 | } 136 | } 137 | } 138 | 139 | @Override 140 | public long getCurrentTime() { 141 | return currentTime; 142 | } 143 | 144 | @Override 145 | public String getSource() { 146 | // String identifying where data came from. 147 | // For example you can use that later in your indicator. 148 | return "simple example data"; 149 | } 150 | 151 | @Override 152 | public void close() { 153 | readerThread.interrupt(); 154 | try { 155 | reader.close(); 156 | } catch (@SuppressWarnings("unused") IOException e) { 157 | } 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/replay/advanced/DemoAdvancedReplayProvider.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.replay.advanced; 2 | 3 | 4 | import java.io.IOException; 5 | import java.util.concurrent.CountDownLatch; 6 | 7 | import velox.api.layer0.annotations.Layer0ReplayModule; 8 | import velox.api.layer0.data.FileEndReachedUserMessage; 9 | import velox.api.layer0.data.FileNotSupportedUserMessage; 10 | import velox.api.layer0.data.IndicatorDefinitionUserMessage; 11 | import velox.api.layer0.data.IndicatorPointUserMessage; 12 | import velox.api.layer0.data.ReadFileLoginData; 13 | import velox.api.layer0.data.TextDataMessage; 14 | import velox.api.layer0.replay.ExternalReaderBaseProvider; 15 | import velox.api.layer1.annotations.Layer1ApiVersion; 16 | import velox.api.layer1.annotations.Layer1ApiVersionValue; 17 | import velox.api.layer1.common.Log; 18 | import velox.api.layer1.data.InstrumentInfo; 19 | import velox.api.layer1.data.LoginData; 20 | import velox.api.layer1.data.TradeInfo; 21 | 22 | /** 23 | *

24 | * Reads demo file that you can download and generates some indicators while 25 | * reading it. 26 | *

27 | *

28 | * Typically you would have indicators already generated in same or some other 29 | * file (e.g. as a result of simulation) 30 | *

31 | */ 32 | @Layer1ApiVersion(Layer1ApiVersionValue.VERSION2) 33 | @Layer0ReplayModule 34 | public class DemoAdvancedReplayProvider extends ExternalReaderBaseProvider implements HandlerListener { 35 | 36 | private Thread readerThread; 37 | private volatile long currentTime = 0; 38 | private CountDownLatch timeReadLatch = new CountDownLatch(1); 39 | 40 | private HandlerBookmapIndicators handler; 41 | 42 | @Override 43 | public void login(LoginData loginData) { 44 | ReadFileLoginData fileData = (ReadFileLoginData) loginData; 45 | 46 | try { 47 | // You can download sample file at https://bookmap.com/shared/feeds/BookmapRecorderDemo_ES-CL_20181002.zip 48 | // Extract before loading. 49 | // Only loading that demo file. 50 | if (!fileData.file.getName().equals("BookmapRecorderDemo_ES-CL_20181002.txt")) { 51 | throw new IOException("File extension not supported"); 52 | } else { 53 | 54 | handler = new HandlerBookmapIndicators(this, fileData.file.getAbsolutePath()); 55 | 56 | readerThread = new Thread(this::read); 57 | readerThread.start(); 58 | 59 | timeReadLatch.await(); 60 | } 61 | } catch (@SuppressWarnings("unused") Exception e) { 62 | adminListeners.forEach(listener -> listener.onUserMessage(new FileNotSupportedUserMessage())); 63 | } 64 | } 65 | 66 | private void read() { 67 | try { 68 | handler.run(); 69 | } catch (@SuppressWarnings("unused") Exception e) { 70 | // We could also report unsupported file if no callback were invoked yet 71 | // In this case bookmap would try other reader modules 72 | reportFileEnd(); 73 | } 74 | } 75 | 76 | public void reportFileEnd() { 77 | adminListeners.forEach(listener -> listener.onUserMessage(new FileEndReachedUserMessage())); 78 | 79 | // In case no time was read - there will be no new time updates after this anyway 80 | timeReadLatch.countDown(); 81 | } 82 | 83 | @Override 84 | public long getCurrentTime() { 85 | return currentTime; 86 | } 87 | 88 | @Override 89 | public String getSource() { 90 | // String identifying where data came from. 91 | // For example you can use that later in your indicator. 92 | return "advanced example data"; 93 | } 94 | 95 | @Override 96 | public void close() { 97 | handler.stop(); 98 | try { 99 | readerThread.join(); 100 | } catch (InterruptedException e) { 101 | Log.error("Interrupted while waiting for reader to stop", e); 102 | Thread.currentThread().interrupt(); 103 | } 104 | } 105 | 106 | @Override 107 | public void onFileEnd() { 108 | reportFileEnd(); 109 | } 110 | 111 | private void setCurrentTime(long t) { 112 | currentTime = t; 113 | timeReadLatch.countDown(); 114 | } 115 | 116 | @Override 117 | public void onDepth(long t, String alias, boolean isBuy, int price, int size) { 118 | setCurrentTime(t); 119 | dataListeners.forEach(l -> l.onDepth(alias, isBuy, price, size)); 120 | } 121 | 122 | @Override 123 | public void onTrade(long t, String alias, double price, int size, boolean isBidAggressor) { 124 | setCurrentTime(t); 125 | dataListeners.forEach(l -> l.onTrade(alias, price, size, new TradeInfo(false, isBidAggressor))); 126 | } 127 | 128 | @Override 129 | public void onInstrument(long t, InstrumentInfo instrumentInfo) { 130 | setCurrentTime(t); 131 | instrumentListeners.forEach(l -> l.onInstrumentAdded(instrumentInfo.symbol, instrumentInfo)); 132 | } 133 | 134 | @Override 135 | public void onTextData(long t, TextDataMessage textDataMessage) { 136 | setCurrentTime(t); 137 | adminListeners.forEach(l -> l.onUserMessage(textDataMessage)); 138 | } 139 | 140 | @Override 141 | public void onIndicatorDefinition(long t, IndicatorDefinitionUserMessage indicatorDefinitionUserMessage) { 142 | setCurrentTime(t); 143 | adminListeners.forEach(l -> l.onUserMessage(indicatorDefinitionUserMessage)); 144 | } 145 | 146 | @Override 147 | public void onIndicatorPoint(long t, IndicatorPointUserMessage indicatorPointUserMessage) { 148 | setCurrentTime(t); 149 | adminListeners.forEach(l -> l.onUserMessage(indicatorPointUserMessage)); 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/replay/advanced/DynamicAverage.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.replay.advanced; 2 | 3 | public class DynamicAverage { 4 | private long counter = 0; 5 | private double cumulative = 0; 6 | 7 | public void update(double x) { 8 | cumulative += x; 9 | counter++; 10 | } 11 | 12 | public double getAverage() { 13 | return cumulative / counter; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/replay/advanced/Ema.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.replay.advanced; 2 | 3 | public class Ema { 4 | private double value = 0; 5 | private Long nanosecondsPrev = null; 6 | private final double halfLifeFactor; 7 | 8 | public Ema(double halfLifeNanoseconds) { 9 | this.halfLifeFactor = -Math.log(2) / halfLifeNanoseconds; 10 | } 11 | 12 | public void onUpdate(long nanoseconds, double x) { 13 | if (nanosecondsPrev == null) { 14 | nanosecondsPrev = nanoseconds; 15 | } 16 | value = getValue(nanoseconds); 17 | value += x; 18 | nanosecondsPrev = nanoseconds; 19 | } 20 | 21 | public double getValue(long nanoseconds) { 22 | if (nanosecondsPrev == null) { 23 | nanosecondsPrev = nanoseconds; 24 | } 25 | long dt = nanoseconds - nanosecondsPrev; 26 | return value * Math.exp(dt * halfLifeFactor); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/replay/advanced/FullTextDataReplayProvider.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.replay.advanced; 2 | 3 | 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.zip.GZIPInputStream; 9 | 10 | import velox.api.layer0.annotations.Layer0ReplayModule; 11 | import velox.api.layer0.common.TextStreamParser; 12 | import velox.api.layer0.data.FileNotSupportedUserMessage; 13 | import velox.api.layer0.data.ReadFileLoginData; 14 | import velox.api.layer0.replay.DemoTextDataReplayProvider; 15 | import velox.api.layer1.annotations.Layer1ApiVersion; 16 | import velox.api.layer1.annotations.Layer1ApiVersionValue; 17 | import velox.api.layer1.common.ListenableHelper; 18 | import velox.api.layer1.data.Layer1ApiProviderSupportedFeatures; 19 | import velox.api.layer1.data.Layer1ApiProviderSupportedFeaturesBuilder; 20 | import velox.api.layer1.data.LoginData; 21 | import velox.api.layer1.layers.Layer1ApiRelay; 22 | 23 | /** 24 | *

25 | * Similar to {@link DemoTextDataReplayProvider} but supports more detailed 26 | * format essentially allowing to use it instead of Recorder API. 27 | *

28 | *

29 | * It is generally similar to {@link DemoTextDataReplayProvider} but is more 30 | * complicated/supports more events, so that might be a better place to look if 31 | * you are just getting started 32 | *

33 | *

34 | * You can download sample file here 35 | *

36 | */ 37 | @Layer1ApiVersion(Layer1ApiVersionValue.VERSION2) 38 | @Layer0ReplayModule 39 | public class FullTextDataReplayProvider extends Layer1ApiRelay { 40 | 41 | TextStreamParser parser; 42 | 43 | public FullTextDataReplayProvider() { 44 | super(null); 45 | } 46 | 47 | @Override 48 | public void login(LoginData loginData) { 49 | ReadFileLoginData fileData = (ReadFileLoginData) loginData; 50 | 51 | try { 52 | File file = fileData.file; 53 | String fileName = file.getName(); 54 | boolean isRawBmtext = fileName.endsWith(".bmtext"); 55 | boolean isGzippedBmtext = fileName.endsWith(".bmtext.gz"); 56 | if (!isRawBmtext && !isGzippedBmtext) { 57 | throw new IOException("File extension not supported"); 58 | } else { 59 | 60 | parser = new TextStreamParser(); 61 | ListenableHelper.addListeners(parser, this); 62 | 63 | InputStream inputStream = new FileInputStream(file); 64 | if (isGzippedBmtext) { 65 | inputStream = new GZIPInputStream(inputStream); 66 | } 67 | parser.start(inputStream); 68 | } 69 | } catch (@SuppressWarnings("unused") IOException e) { 70 | adminListeners.forEach(listener -> listener.onUserMessage(new FileNotSupportedUserMessage())); 71 | } 72 | } 73 | 74 | @Override 75 | public Layer1ApiProviderSupportedFeatures getSupportedFeatures() { 76 | return new Layer1ApiProviderSupportedFeaturesBuilder().build(); 77 | } 78 | 79 | @Override 80 | public long getCurrentTime() { 81 | return parser.getCurrentTime(); 82 | } 83 | 84 | @Override 85 | public String getSource() { 86 | return "Advanced text format"; 87 | } 88 | 89 | @Override 90 | public void close() { 91 | if (parser != null) { 92 | parser.close(); 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/replay/advanced/HandlerBase.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.replay.advanced; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.FileReader; 5 | 6 | public abstract class HandlerBase { 7 | 8 | protected final HandlerListener listener; 9 | private final String filenameIn; 10 | protected boolean skipFirstLine = false; 11 | private volatile boolean shouldStop = false; 12 | 13 | public HandlerBase(HandlerListener listener, String fin) { 14 | this.listener = listener; 15 | this.filenameIn = fin; 16 | } 17 | 18 | public void run() throws Exception { 19 | String line; 20 | BufferedReader reader = new BufferedReader(new FileReader(filenameIn)); 21 | if (skipFirstLine) { 22 | reader.readLine(); 23 | } 24 | int n = 0; 25 | while ((line = reader.readLine()) != null && !shouldStop) { 26 | processLine(line); 27 | n++; 28 | } 29 | System.out.println("Lines processed: " + n); 30 | listener.onFileEnd(); 31 | reader.close(); 32 | } 33 | 34 | public void stop() { 35 | shouldStop = true; 36 | } 37 | 38 | protected abstract void processLine(String line) throws Exception; 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/replay/advanced/HandlerBookmapIndicators.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.replay.advanced; 2 | 3 | import java.awt.Color; 4 | import java.util.HashMap; 5 | import java.util.Random; 6 | 7 | import velox.api.layer0.data.IndicatorDefinitionUserMessage; 8 | import velox.api.layer0.data.IndicatorPointUserMessage; 9 | import velox.api.layer0.data.TextDataMessage; 10 | import velox.api.layer1.data.InstrumentInfo; 11 | 12 | @SuppressWarnings("deprecation") 13 | public class HandlerBookmapIndicators extends HandlerBookmapSimple { 14 | 15 | private double probability = 0.2; 16 | 17 | private final int numIntrinsicIndicators = 10; 18 | private final double[] intrinsicParams = generateParams(numIntrinsicIndicators, 4, 1.4); 19 | 20 | private final int numVolumeEmaIndicators = 10; 21 | private final double[] emaParams = generateParams(numVolumeEmaIndicators, 1e9, 2); 22 | 23 | private Random rand = new Random(); 24 | private HashMap datas = new HashMap<>(); 25 | 26 | public HandlerBookmapIndicators(HandlerListener listener, String fin) throws Exception { 27 | super(listener, fin); 28 | } 29 | 30 | @Override 31 | protected void onDepth(long t, int id, boolean isBuy, long price, long size) throws Exception { 32 | super.onDepth(t, id, isBuy, price, size); 33 | datas.get(id).onDepth(isBuy, price, size); 34 | onEvent(t); 35 | } 36 | 37 | @Override 38 | protected void onTrade(long t, int id, boolean isBuy, double price, long size) throws Exception { 39 | super.onTrade(t, id, isBuy, price, size); 40 | datas.get(id).onTrade(t, isBuy, size); 41 | onEvent(t); 42 | if (size >= 5) { 43 | InstrumentInfo instrumentInfo = instruments.get(id); 44 | listener.onTextData(t, new TextDataMessage(instrumentInfo.symbol, "Big trade", 45 | isBuy, instrumentInfo.pips * price, size, "Big trade of size " + size)); 46 | } 47 | } 48 | 49 | @Override 50 | protected void onInstrument(long t, int id, String alias, double pips, double multiplier) throws Exception { 51 | super.onInstrument(t, id, alias, pips, multiplier); 52 | initIndicators(t, id); 53 | } 54 | 55 | private int getFirstIndicatorId(int instrId) { 56 | return instrId * 2 * (numIntrinsicIndicators + numVolumeEmaIndicators); 57 | } 58 | 59 | private void initIndicators(long t, int id) throws Exception { 60 | datas.put(id, new IndicatorsPack(intrinsicParams, emaParams)); 61 | int currentIndicatorId = getFirstIndicatorId(id); 62 | String alias = instruments.get(id).symbol; 63 | for (int i = 0; i < numIntrinsicIndicators; i++) { 64 | listener.onIndicatorDefinition(t, new IndicatorDefinitionUserMessage( 65 | currentIndicatorId, alias, "Intrinsic bid #" + i, 66 | (short) 0xFFFF, (short) 1, 1, Color.WHITE, 67 | (short) 0xFF08, (short) 1, 1, null, 0, 0, true)); 68 | currentIndicatorId++; 69 | } 70 | for (int i = 0; i < numIntrinsicIndicators; i++) { 71 | listener.onIndicatorDefinition(t, new IndicatorDefinitionUserMessage( 72 | currentIndicatorId, alias, "Intrinsic ask #" + i, 73 | (short) 0xFFFF, (short) 1, 1, Color.WHITE, 74 | (short) 0xFF08, (short) 1, 1, null, 0, 0, true)); 75 | currentIndicatorId++; 76 | } 77 | for (int i = 0; i < numVolumeEmaIndicators; i++) { 78 | listener.onIndicatorDefinition(t, new IndicatorDefinitionUserMessage( 79 | currentIndicatorId, alias, "Volume EMA bid #" + i, 80 | (short) 0xFFFF, (short) 1, 1, 81 | new Color(46, 204, 113), (short) 0xFF08, (short) 1, 1, null, 0, 0, false, "%6.3e")); 82 | currentIndicatorId++; 83 | } 84 | for (int i = 0; i < numVolumeEmaIndicators; i++) { 85 | listener.onIndicatorDefinition(t, new IndicatorDefinitionUserMessage( 86 | currentIndicatorId, alias, "Volume EMA ask #" + i, 87 | (short) 0xFFFF, (short) 1, 1, 88 | new Color(213, 76, 60), (short) 0xFF08, (short) 1, 1, null, 0, 0, false, "%6.3e")); 89 | currentIndicatorId++; 90 | } 91 | } 92 | 93 | private void onEvent(long t) throws Exception { 94 | if (rand.nextDouble() < probability) { 95 | int id = (int) datas.keySet().toArray()[rand.nextInt(instruments.size())]; 96 | IndicatorsPack pack = datas.get(id); 97 | int firstIndicatorId = getFirstIndicatorId(id); 98 | if (rand.nextBoolean()) { 99 | boolean isBid = rand.nextBoolean(); 100 | int idx = rand.nextInt(numIntrinsicIndicators); 101 | double intrinsic = instruments.get(id).pips * pack.getIntrinsic(isBid, idx); 102 | int indicatorId = firstIndicatorId + (isBid ? 0 : numIntrinsicIndicators) + idx; 103 | listener.onIndicatorPoint(t, new IndicatorPointUserMessage(indicatorId, intrinsic)); 104 | } else { 105 | int idx = rand.nextInt(numVolumeEmaIndicators); 106 | boolean isBuy = rand.nextBoolean(); 107 | double ema = pack.getEma(t, isBuy, idx); 108 | int indicatorId = firstIndicatorId + 2 * numIntrinsicIndicators + (isBuy ? 0 : numVolumeEmaIndicators) 109 | + idx; 110 | listener.onIndicatorPoint(t, new IndicatorPointUserMessage(indicatorId, ema)); 111 | } 112 | } 113 | } 114 | 115 | private static double[] generateParams(int len, double first, double factor) { 116 | double[] params = new double[len]; 117 | for (int i = 0; i < len; i++) { 118 | params[i] = first * Math.pow(factor, i); 119 | } 120 | return params; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/replay/advanced/HandlerBookmapSimple.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.replay.advanced; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.HashMap; 5 | 6 | import velox.api.layer1.data.InstrumentInfo; 7 | 8 | public class HandlerBookmapSimple extends HandlerBase { 9 | 10 | private final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss.SSS"); 11 | protected final HashMap instruments = new HashMap<>(); 12 | 13 | public HandlerBookmapSimple(HandlerListener listener, String fin) throws Exception { 14 | super(listener, fin); 15 | } 16 | 17 | private long getNanoseconds(String s) throws Exception { 18 | String strMillis = s.substring(0, 21); 19 | String strNanos = s.substring(21); 20 | long t = 1_000_000L * sdf.parse(strMillis).getTime() + Long.parseLong(strNanos); 21 | return t; 22 | } 23 | 24 | @Override 25 | protected void processLine(String line) throws Exception { 26 | String[] s = line.split(","); 27 | long t = getNanoseconds(s[0]); 28 | int instrID = Integer.parseInt(s[1]); 29 | String eventType = s[2]; 30 | if (eventType.equals("Quote")) { 31 | onDepth(t, instrID, s[3].equals("Buy"), Long.parseLong(s[4]), Long.parseLong(s[5])); 32 | } else if (eventType.equals("BBO")) { 33 | } else if (eventType.equals("Trade")) { 34 | onTrade(t, instrID, s[3].equals("Buy"), Double.parseDouble(s[4]), Long.parseLong(s[5])); 35 | } else if (eventType.equals("InstrumentAdded")) { 36 | onInstrument(t, instrID, s[3].split("=")[1], Double.parseDouble(s[4].split("=")[1]), 37 | Double.parseDouble(s[5].split("=")[1])); 38 | } else if (eventType.equals("InstrumentRemoved")) { 39 | } else { 40 | throw new Exception("HandlerBookmapSimple: unrecognized event type: " + eventType); 41 | } 42 | } 43 | 44 | protected void onDepth(long t, int id, boolean isBuy, long price, long size) throws Exception { 45 | InstrumentInfo instrumentInfo = instruments.get(id); 46 | listener.onDepth(t, instrumentInfo.symbol, isBuy, (int)price, (int)size); 47 | } 48 | 49 | protected void onTrade(long t, int id, boolean isBuy, double price, long size) throws Exception { 50 | InstrumentInfo instrumentInfo = instruments.get(id); 51 | listener.onTrade(t, instrumentInfo.symbol, price, (int)size, isBuy); 52 | } 53 | 54 | protected void onInstrument(long t, int id, String alias, double pips, double multiplier) throws Exception { 55 | InstrumentInfo instrumentInfo = new InstrumentInfo(alias, null, null, pips, multiplier, alias, true); 56 | instruments.put(id, instrumentInfo); 57 | listener.onInstrument(t, instrumentInfo); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/replay/advanced/HandlerListener.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.replay.advanced; 2 | 3 | import velox.api.layer0.data.IndicatorDefinitionUserMessage; 4 | import velox.api.layer0.data.IndicatorPointUserMessage; 5 | import velox.api.layer0.data.TextDataMessage; 6 | import velox.api.layer1.data.InstrumentInfo; 7 | 8 | public interface HandlerListener { 9 | 10 | void onFileEnd(); 11 | 12 | void onDepth(long t, String alias, boolean isBuy, int price, int size); 13 | 14 | void onTrade(long t, String alias, double price, int size, boolean isBidAggressor); 15 | 16 | void onInstrument(long t, InstrumentInfo instrumentInfo); 17 | 18 | void onTextData(long t, TextDataMessage textDataMessage); 19 | 20 | void onIndicatorDefinition(long t, IndicatorDefinitionUserMessage indicatorDefinitionUserMessage); 21 | void onIndicatorPoint(long t, IndicatorPointUserMessage indicatorPointUserMessage); 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/replay/advanced/IndicatorsPack.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.replay.advanced; 2 | 3 | public class IndicatorsPack { 4 | 5 | private final IntrinsicPrice intrinsicPrice = new IntrinsicPrice(); 6 | private final double[] intrinsicParams; 7 | private final DynamicAverage avgSize = new DynamicAverage(); 8 | private final Ema[] emaBuy; 9 | private final Ema[] emaSell; 10 | 11 | public IndicatorsPack(double[] intrinsicParams, double[] emaParams) { 12 | this.intrinsicParams = intrinsicParams; 13 | int n = emaParams.length; 14 | emaBuy = new Ema[n]; 15 | emaSell = new Ema[n]; 16 | for (int i = 0; i < n; i++) { 17 | emaBuy[i] = new Ema(emaParams[i]); 18 | emaSell[i] = new Ema(emaParams[i]); 19 | } 20 | } 21 | 22 | public void onDepth(boolean isBuy, long price, long size) { 23 | intrinsicPrice.onDepth(isBuy, price, size); 24 | avgSize.update(size); 25 | } 26 | 27 | public void onTrade(long t, boolean isBuy, long size) { 28 | Ema[] ema = isBuy ? emaBuy : emaSell; 29 | for (Ema e : ema) { 30 | e.onUpdate(t, size); 31 | } 32 | } 33 | 34 | public double getIntrinsic(boolean isBid, int idx) { 35 | long hypotheticalMarketOrderSize = Math.round(intrinsicParams[idx] * avgSize.getAverage()); 36 | double intrinsic = intrinsicPrice.calcIntrinsic(isBid, hypotheticalMarketOrderSize); 37 | return intrinsic; 38 | } 39 | 40 | public double getEma(long t, boolean isBuy, int idx) { 41 | double ema = (isBuy ? emaBuy : emaSell)[idx].getValue(t); 42 | return ema; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/replay/advanced/IntrinsicPrice.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.replay.advanced; 2 | 3 | import java.util.Map; 4 | import java.util.Map.Entry; 5 | 6 | public class IntrinsicPrice { 7 | private final OrderBookMbp orderBook = new OrderBookMbp(); 8 | 9 | public void onDepth(boolean isBuy, long price, long size) { 10 | orderBook.onDepth(isBuy, price, size); 11 | } 12 | 13 | public double calcIntrinsic(boolean isBid, long hmos) { 14 | Map book = isBid ? orderBook.bids : orderBook.asks; 15 | double executionPrice = 0; 16 | long size = hmos; 17 | for (Entry entry : book.entrySet()) { 18 | if (size == 0) { 19 | break; 20 | } 21 | long matchedSize = Math.min(entry.getValue(), size); 22 | size -= matchedSize; 23 | executionPrice += entry.getKey() * matchedSize; 24 | } 25 | return size == 0 ? executionPrice / hmos : Double.NaN; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/velox/api/layer0/replay/advanced/OrderBookMbp.java: -------------------------------------------------------------------------------- 1 | package velox.api.layer0.replay.advanced; 2 | 3 | import java.util.Collections; 4 | import java.util.Map; 5 | import java.util.TreeMap; 6 | 7 | public class OrderBookMbp { 8 | public TreeMap bids = new TreeMap<>(Collections.reverseOrder()); 9 | public TreeMap asks = new TreeMap<>(); 10 | 11 | public void onDepth(boolean isBid, long price, long size) { 12 | Map book = isBid ? bids : asks; 13 | if (size == 0) 14 | book.remove(price); 15 | else 16 | book.put(price, size); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/icon_accept.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BookmapAPI/Layer0ApiDemo/70429b7d244c178ffd3c5c5d8bc6d5cead58af4c/src/main/resources/icon_accept.gif --------------------------------------------------------------------------------