Compare commits
188
Commits
csv_export
...
20.0.0_rc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
482c300b72 | ||
|
|
69ad995ba9 | ||
|
|
0b26ec1132 | ||
|
|
3de494c80f | ||
|
|
7deb25dd1c | ||
|
|
7a29ae1a83 | ||
|
|
6da893128c | ||
|
|
2cb458af2d | ||
|
|
04db38d627 | ||
|
|
a144b03361 | ||
|
|
9c89a9a47e | ||
|
|
007af71bb2 | ||
|
|
a216d1cc81 | ||
|
|
f8395c11b5 | ||
|
|
2b0e46ed38 | ||
|
|
5f3b243265 | ||
|
|
c9ce2d18bf | ||
|
|
7b563ba66c | ||
|
|
3e1be70923 | ||
|
|
aadd7df6c6 | ||
|
|
8206132a37 | ||
|
|
f8b3fd2e14 | ||
|
|
8b17ee72ff | ||
|
|
31ed473999 | ||
|
|
673805a7f4 | ||
|
|
91422449e9 | ||
|
|
438089a1cd | ||
|
|
6a186d1d7e | ||
|
|
9d831a2569 | ||
|
|
d47cf1a54e | ||
|
|
8c9f669e72 | ||
|
|
f5bda11797 | ||
|
|
1841a75fec | ||
|
|
34cdaeec93 | ||
|
|
3230fb9e48 | ||
|
|
888b4e22c0 | ||
|
|
534ce5a8ec | ||
|
|
c2b9ac57dc | ||
|
|
2783361325 | ||
|
|
46d35131d7 | ||
|
|
3b95154c04 | ||
|
|
1a6b918853 | ||
|
|
bc22605a25 | ||
|
|
f3d28add1c | ||
|
|
63d260a6f8 | ||
|
|
9b4a63b4a8 | ||
|
|
a502c365e5 | ||
|
|
fdacd8b543 | ||
|
|
3d1de9a0c7 | ||
|
|
b2b3fa5949 | ||
|
|
86ecf69820 | ||
|
|
8f537a8fda | ||
|
|
01b9ff6b47 | ||
|
|
2609663d3e | ||
|
|
fd89241611 | ||
|
|
a69118c5d3 | ||
|
|
5accd0e2b4 | ||
|
|
df5ad61cd2 | ||
|
|
4a7674eb29 | ||
|
|
5a458287f4 | ||
|
|
5921f9f6b5 | ||
|
|
4c73a63ae8 | ||
|
|
42b1c921a8 | ||
|
|
fab3334b1e | ||
|
|
7239dd3ade | ||
|
|
dd4c8eb649 | ||
|
|
76a29bdc8a | ||
|
|
06d708f3ea | ||
|
|
e2f70cbc19 | ||
|
|
9d54e47a03 | ||
|
|
faccdf581e | ||
|
|
1a64714a54 | ||
|
|
3ba6792a85 | ||
|
|
c32a7f1d62 | ||
|
|
10e75f30e6 | ||
|
|
20eac19c4f | ||
|
|
8c0765a2c7 | ||
|
|
7a1f88e813 | ||
|
|
3420f14c8a | ||
|
|
86277dfbfe | ||
|
|
4839e464c6 | ||
|
|
dc2ad071a9 | ||
|
|
65e49992f9 | ||
|
|
afbb9ae92d | ||
|
|
ff088142d9 | ||
|
|
93b7cf346b | ||
|
|
f0edeadd50 | ||
|
|
8ee9b84488 | ||
|
|
e3648f5373 | ||
|
|
facd3a9d81 | ||
|
|
84969cb481 | ||
|
|
6d521e77db | ||
|
|
82c49f8f49 | ||
|
|
e074309da1 | ||
|
|
2d622a5f39 | ||
|
|
d51d20e84c | ||
|
|
2ccdef9dc5 | ||
|
|
42e62a30bf | ||
|
|
c233c607fa | ||
|
|
462cb930f7 | ||
|
|
2c0f920422 | ||
|
|
5ee26065ca | ||
|
|
550a4e8d50 | ||
|
|
99b8221efc | ||
|
|
19e74c30f8 | ||
|
|
26693a4c98 | ||
|
|
d8bdb69a7c | ||
|
|
6d357a74e1 | ||
|
|
73f4e4814c | ||
|
|
312bc4be15 | ||
|
|
a0c3ba449a | ||
|
|
d5a2a7f3f0 | ||
|
|
eda8c94def | ||
|
|
9a8e80e149 | ||
|
|
6a7e3d739d | ||
|
|
e6b8274813 | ||
|
|
9783520c62 | ||
|
|
7e7f0bc37f | ||
|
|
b9e2ecdffa | ||
|
|
f1c021a474 | ||
|
|
6fda54ffa1 | ||
|
|
bdb0adc16c | ||
|
|
4b19b0ea14 | ||
|
|
fab1b4dd0f | ||
|
|
f3ed3075cf | ||
|
|
742af36b93 | ||
|
|
c573b5fbb2 | ||
|
|
2d6d4af594 | ||
|
|
2dcfad979c | ||
|
|
ba7e35649e | ||
|
|
cda4a870b7 | ||
|
|
b3a8564365 | ||
|
|
1e0114a4cf | ||
|
|
3a6e95ef05 | ||
|
|
6875263743 | ||
|
|
eac828f323 | ||
|
|
1635d6f649 | ||
|
|
2da535e9ff | ||
|
|
a98be44a40 | ||
|
|
6210ac2ee6 | ||
|
|
cfebab0488 | ||
|
|
eacd766359 | ||
|
|
8fb8f8ff58 | ||
|
|
f4b22816a2 | ||
|
|
1029a464d7 | ||
|
|
fd8b8e03ea | ||
|
|
41cf66a943 | ||
|
|
1c57c9a012 | ||
|
|
8ccab66120 | ||
|
|
e2b2f7ec9c | ||
|
|
b3c897bf0f | ||
|
|
87c9f54c8b | ||
|
|
df56b76ab6 | ||
|
|
28ff13db11 | ||
|
|
2957075769 | ||
|
|
a6477824a8 | ||
|
|
17431f5ac0 | ||
|
|
8708f02fe0 | ||
|
|
752fb2bc4a | ||
|
|
6efb78ecf9 | ||
|
|
75c1970aef | ||
|
|
c2583518d7 | ||
|
|
8792871387 | ||
|
|
68a593af7b | ||
|
|
17c2321a82 | ||
|
|
0affc8a942 | ||
|
|
ce36ea4109 | ||
|
|
7c29a3d1d8 | ||
|
|
90bc20575a | ||
|
|
cb268f505a | ||
|
|
dfd93b836b | ||
|
|
28e9c66675 | ||
|
|
c0520d6a31 | ||
|
|
1ba6e3280c | ||
|
|
3a6f50dfea | ||
|
|
d711f99548 | ||
|
|
07ace80871 | ||
|
|
93c6277bfc | ||
|
|
7ac9d05ae2 | ||
|
|
5a4b31ccc0 | ||
|
|
fb568aab39 | ||
|
|
64416de10d | ||
|
|
46953cb639 | ||
|
|
7e68a235ee | ||
|
|
85f2285ac0 | ||
|
|
70e0979836 | ||
|
|
1f80c3366e | ||
|
|
19e66a1c32 |
No files matched your search
@@ -35,3 +35,4 @@ trace_data/
|
||||
.ionide/*
|
||||
/.ignore
|
||||
/build
|
||||
.clangd/
|
||||
Vendored
+24
@@ -1,6 +1,30 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Spark Local",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/build/ninja_debug/server/sparkd",
|
||||
"args": [
|
||||
"run",
|
||||
// "test",
|
||||
// "-a",
|
||||
// "[ilink3]"
|
||||
],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceRoot}/build/ninja_debug/server/",
|
||||
"environment": [],
|
||||
"externalConsole": false,
|
||||
"MIMode": "gdb",
|
||||
"setupCommands": [
|
||||
{
|
||||
"description": "Enable pretty-printing for gdb",
|
||||
"text": "-enable-pretty-printing",
|
||||
"ignoreFailures": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SparkVM Launch",
|
||||
"type": "cppdbg",
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ else ()
|
||||
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/web ${CMAKE_BINARY_DIR}/server
|
||||
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/data_files ${CMAKE_BINARY_DIR}/server
|
||||
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/src/lib/libspark.so ${CMAKE_BINARY_DIR}/server
|
||||
COMMAND ln ARGS -sf ${EXTERNALS_ROOT}/lib/Release/libOnixS.IceImpactMulticastPriceFeedHandlerCpp.so.3.13.0.0 ${CMAKE_BINARY_DIR}/server
|
||||
COMMAND ln ARGS -sf ${EXTERNALS_ROOT}/lib/Release/libOnixS.IceImpactMulticastPriceFeedHandlerCpp.so.3.16.0.0 ${CMAKE_BINARY_DIR}/server
|
||||
COMMAND ln ARGS -sf ${EXTERNALS_ROOT}/lib/Release/libACE-5.7.5.so ${CMAKE_BINARY_DIR}/server
|
||||
COMMAND mkdir ARGS -p ${CMAKE_BINARY_DIR}/server/plugins
|
||||
COMMENT "Creating server runtime layout"
|
||||
|
||||
@@ -124,4 +124,17 @@ service OrderService {
|
||||
```
|
||||
### C++ Example
|
||||
```cpp
|
||||
#include "lib/api/public/ServiceContext.h"
|
||||
#include "lib/api/services/OrderService.client.pb.h"
|
||||
using namespace bts::api;
|
||||
ServiceContext ctx;
|
||||
auto orders = ctx.locate<orderman::OrderService>();
|
||||
auto unsub = orders.subscribe_fills().subscribe([](const auto& update) {
|
||||
std::cout
|
||||
<< "Added " << update.added().size() << " items, "
|
||||
<< "Removed " << update.removed().size() << " items, "
|
||||
<< "Updated " << update.updated().size() << " items, "
|
||||
<< "Total items " << update.all().size() << std::endl;
|
||||
});
|
||||
std::this_thread::sleep_for(std::chrono::minutes(5));
|
||||
```
|
||||
@@ -1,3 +1,51 @@
|
||||
+++
|
||||
title = "Market Data"
|
||||
+++
|
||||
+++
|
||||
|
||||
|
||||
Use the MarketDataService to subscribe enumerate all available securities.
|
||||
|
||||
```cs
|
||||
using System;
|
||||
using System.Threading;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
|
||||
namespace sample {
|
||||
class Program {
|
||||
static void Main(string[] args) {
|
||||
using var context = new ServiceContext();
|
||||
var md = context.locate<MarketDataService>();
|
||||
using var sub = md.subscribe_securities().Subscribe(update => {
|
||||
foreach (var security in update.added) {
|
||||
Console.WriteLine($"Added security {security.Id}: {security.Symbol}");
|
||||
}
|
||||
});
|
||||
|
||||
Thread.Sleep(TimeSpan.FromMinutes(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```cpp
|
||||
#include "lib/api/public/ServiceContext.h"
|
||||
#include "spark/lib/domain_model/Security.h"
|
||||
#include "lib/api/services/MarketDataService.client.pb.h"
|
||||
#include <thread>
|
||||
|
||||
using namespace bts::api;
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
ServiceContext ctx;
|
||||
auto md_service = ctx.locate<bts::api::MarketDataService>();
|
||||
auto sub = md_service.subscribe_securities().subscribe([](const auto& update) {
|
||||
for (const auto& s : update.added()) {
|
||||
std::cout << "Added security: " << s->id() << ": " << s->symbol() << std::endl;
|
||||
}
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::minutes(5));
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
@@ -68,12 +68,21 @@ To update your Spark SDK simply download an unzip a new [spark_sdk.zip](https://
|
||||
|
||||
To preserve the old image so you can go back to it later run `docker tag spark_sdk spark_sdk:your_custom_tag` before loading the new SDK image.
|
||||
|
||||
<!--
|
||||
Then stop, remove and restart spark_sdk.
|
||||
### Re-build the Dev Container
|
||||
|
||||
If you are using Visual Studio Code you also need to rebuild the dev-container so it picks up the new image.
|
||||
|
||||
#### Inside VSCode
|
||||
To re-build from inside Visual Studio Code. Click the 'Remote Explorer' icon on the left hand side.
|
||||
You should see your spark_sdk project listed under the Containers section. Right click on it and select **rebuild-container**.
|
||||
|
||||
#### Command Line
|
||||
To re-build from the command line run `docker ps -a` and find the row with the spark_sdk image.
|
||||
```
|
||||
c:\bts> docker stop spark_sdk
|
||||
c:\bts> docker rm spark_sdk
|
||||
c:\bts> docker run -d -v %cd%:/sdk --name spark_sdk bluetradingsytems/spark_sdk
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
54d96cfa3888 spark_sdk "/bin/sh -c 'echo Co…" About a minute ago Up About a minute 127.0.0.1:9000-9001->9000-9001/tcp quizzical_lichterman
|
||||
```
|
||||
Then run `docker rm 54d96cfa3888` (use your container id instead of 54d96cfa3888) or `docker rm quizzical_lichterman` (use your container name).
|
||||
|
||||
|
||||
``` -->
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
+++
|
||||
+++
|
||||
title = "Release notes"
|
||||
weight = 3
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "right"
|
||||
+++
|
||||
## 2.16.2
|
||||
### User Visible Change
|
||||
- Synthetic creation dialog has a "fractional" checkbox next to the tick settings. Users may uncheck this box to enter integer tick sizes, so the synthetic security's prices will be displayed without decimals, or fractional units.
|
||||
## 2.16.1
|
||||
### Bug Fixes
|
||||
- OrderBook's algo state parameters (in the panel on the right side) are sorted alphabetically
|
||||
## 2.16.0
|
||||
### Bug Fixes
|
||||
- Solved an issue that could result in incorrect ladder levels
|
||||
- Solved an issue that could lead to incorrect routing of orders in the event of certain MSGW changes
|
||||
### API Changes
|
||||
- Add C# client side API samples for order entry, position monitoring and market data observation
|
||||
|
||||
## 2.15.10
|
||||
### Bug Fixes
|
||||
- Solved an issue that could cause the user interface to fail when playing some alert sounds
|
||||
### API Changes
|
||||
- Added settlement and position information to the API
|
||||
- Added the ability for user algos to report custom state to the user interface
|
||||
- Added the ability for algos to discover all active securities
|
||||
## 2.15.3
|
||||
### Bug Fixes
|
||||
- Fixed a bug where some CME currency spreads exhibited incorrect tick values
|
||||
@@ -708,3 +728,4 @@ insert_anchor_links = "right"
|
||||
- Start, Day and Total PnL columns are painted with a red or green background color based on if the pnl value is negative or positive respectively
|
||||
- Corrected cash per point values, used in pnl calculations, for a number of products
|
||||
- PnL report will now remember any customizations you make to filtering and/or sorting criteria
|
||||
|
||||
+1
-1
Submodule docker/linux updated: 0df20ec20a...6f0dd9324b.
+1
-1
Submodule externals updated: c695e96940...2ea5e355a6.
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "2.1.508",
|
||||
"rollForward": "latestMajor"
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@
|
||||
<GenDir>$(BaseOutputPath)\gen\$(MSBuildProjectName)</GenDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(VCTargetsPath)'==''">
|
||||
<VCTargetsPath>C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\Common7\IDE\VC\VCTargets</VCTargetsPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(ComputerSpecificConfig)" Condition="Exists($(ComputerSpecificConfig))" />
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -115,7 +115,7 @@ namespace mbts.ui.shell
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\BTS\\stunnel\\";
|
||||
string path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\BTS\\stunnel\\";
|
||||
if (File.Exists(path + "config\\stunnel.conf"))
|
||||
{
|
||||
uint job_error = BTSUINativeMethods.setup_process_job_object();
|
||||
@@ -124,6 +124,15 @@ namespace mbts.ui.shell
|
||||
kill_stunnel();
|
||||
start_stunnel(path, job_error);
|
||||
}
|
||||
path = AppDomain.CurrentDomain.BaseDirectory + "\\..\\stunnel\\";
|
||||
if (File.Exists(path + "config\\stunnel.conf"))
|
||||
{
|
||||
uint job_error = BTSUINativeMethods.setup_process_job_object();
|
||||
//if we can't put this process and stunnel into a job, try to stop stunnel then restart
|
||||
if (job_error != 0)
|
||||
kill_stunnel();
|
||||
start_stunnel(path, job_error);
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace bts.shell {
|
||||
class MarketShellModule : IShellModule {
|
||||
[ImportingConstructor]
|
||||
public MarketShellModule(IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, OrdersModel orders_model) {
|
||||
_tsf = new TimeAndSalesWindowFactory(this, bus, consumer, settings_hub);
|
||||
_tsf = new TimeAndSalesWindowFactory(this, bus, consumer, settings_hub, orders_model.security_auxiliary_model);
|
||||
_mvf = new MarketViewWindowFactory(this, bus, settings_hub, orders_model);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts.co2;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
@@ -24,7 +26,6 @@ namespace mbts.spark {
|
||||
readonly AnimationManager _animation = new AnimationManager();
|
||||
readonly ObservableCollection<OrderRecordImmutable> _orders = new ObservableCollection<OrderRecordImmutable>();
|
||||
readonly ObservableFilterCollection<OrderRecordImmutable> _working_orders, _working_bids, _working_asks;
|
||||
readonly string _security_text, _security_key_text;
|
||||
readonly Property<double> _bid_price, _ask_price, _last_price, _day_change, _settlement, _session_high, _session_low;
|
||||
readonly Property<bool> _last_price_from_spread;
|
||||
readonly CellProperty<int> _position;
|
||||
@@ -33,48 +34,51 @@ namespace mbts.spark {
|
||||
readonly CellProperty<bool> _inside_bids, _inside_asks;
|
||||
readonly CellProperty<int> _working_bid_volume, _working_ask_volume;
|
||||
readonly CellProperty<double> _working_best_bid, _working_best_ask;
|
||||
readonly CellProperty<string> _working_bids_text, _working_asks_text;
|
||||
readonly MyOrderSummarizer _order_summarizer = new MyOrderSummarizer();
|
||||
readonly MDDisplayHelper.SizeFormatter _size_formatter = MDDisplayHelper.get_formatter(MarketSizeDisplay.get_default());
|
||||
Property<uint> _bid_direct_size, _bid_merged_size, _ask_direct_size, _ask_merged_size;
|
||||
PropertyAutoWrapper<uint, uint>
|
||||
_bid_direct_size,
|
||||
_bid_implied_size,
|
||||
_bid_merged_size,
|
||||
_ask_direct_size,
|
||||
_ask_implied_size,
|
||||
_ask_merged_size;
|
||||
double _raw_day_position;
|
||||
string _bid_merged_size_text, _ask_merged_size_text;
|
||||
ulong _bbo_ref, _last_ref;
|
||||
uint? _display_qty_divisor_backing;
|
||||
|
||||
public Row(IBus bus, SecurityMD smd, OrdersModel orders_model) : base(smd) {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_security_text = smd.security.ToString();
|
||||
_security_key_text = security_key.ToString();
|
||||
string format_price(double price) => security.ContractSpec.format(price);
|
||||
_position = new CellProperty<int>(this, nameof(position), 0);
|
||||
_position = new CellProperty<int>(this, nameof(position), fmt_display_qty_int);
|
||||
_bid_price = backing(nameof(bid_price), double.NaN);
|
||||
_ask_price = backing(nameof(ask_price), double.NaN);
|
||||
_last_price = backing(nameof(last_price), double.NaN);
|
||||
_last_size = new CellProperty<uint>(this, nameof(last_size), 0);
|
||||
_last_size = new CellProperty<uint>(this, nameof(last_size), fmt_display_qty_uint);
|
||||
_day_change = backing(nameof(day_change), double.NaN);
|
||||
_session_low = backing(nameof(session_low), double.NaN);
|
||||
_session_high = backing(nameof(session_high), double.NaN);
|
||||
_last_price_from_spread = backing(nameof(last_price_from_spread), false);
|
||||
_settlement = backing(nameof(settlement), double.NaN);
|
||||
_volume = new CellProperty<ulong>(this, nameof(volume), 0);
|
||||
_volume = new CellProperty<ulong>(this, nameof(volume), fmt_display_qty_ulong, 0);
|
||||
_inside_bids = new CellProperty<bool>(this, nameof(inside_bids));
|
||||
_inside_asks = new CellProperty<bool>(this, nameof(inside_asks));
|
||||
_working_bid_volume = new CellProperty<int>(this, nameof(working_bid_volume));
|
||||
_working_ask_volume = new CellProperty<int>(this, nameof(working_ask_volume));
|
||||
//
|
||||
_bid_direct_size = backing(nameof(bid_direct_size), default(uint));
|
||||
_bid_merged_size = backing(nameof(bid_merged_size), default(uint));
|
||||
_ask_direct_size = backing(nameof(ask_direct_size), default(uint));
|
||||
_ask_merged_size = backing(nameof(ask_merged_size), default(uint));
|
||||
_bid_direct_size = backing(nameof(bid_direct_size), 0u, convert_display_qty_uint);
|
||||
_bid_implied_size = backing(nameof(bid_implied_size), 0u, convert_display_qty_uint);
|
||||
_bid_merged_size = backing(nameof(bid_merged_size), 0u, convert_display_qty_uint);
|
||||
_ask_direct_size = backing(nameof(ask_direct_size), 0u, convert_display_qty_uint);
|
||||
_ask_implied_size = backing(nameof(ask_implied_size), 0u, convert_display_qty_uint);
|
||||
_ask_merged_size = backing(nameof(ask_merged_size), 0u, convert_display_qty_uint);
|
||||
//
|
||||
_working_orders = new ObservableFilterCollection<OrderRecordImmutable>(_orders, o => o.is_active());
|
||||
_working_bids = new ObservableFilterCollection<OrderRecordImmutable>(_working_orders, o => o.Def.Side == Side.Buy);
|
||||
_working_asks = new ObservableFilterCollection<OrderRecordImmutable>(_working_orders, o => o.Def.Side == Side.Sell);
|
||||
_working_best_bid = new CellProperty<double>(this, nameof(working_best_bid), format_price, double.NaN);
|
||||
_working_best_ask = new CellProperty<double>(this, nameof(working_best_ask), format_price, double.NaN);
|
||||
_working_bids_text = new CellProperty<string>(this, nameof(working_bids_text), string.Empty);
|
||||
_working_asks_text = new CellProperty<string>(this, nameof(working_asks_text), string.Empty);
|
||||
//
|
||||
default_command = new DelegateCommand(_show_ladder);
|
||||
bid_price_click = new DelegateCommand(() => _show_order_ticket(MarketViewView.Columns.Bid));
|
||||
@@ -90,17 +94,45 @@ namespace mbts.spark {
|
||||
_animation.AnimationsChanged += on_animations_changed;
|
||||
void on_animations_changed() => RaisePropertyChanged(nameof(animations));
|
||||
_dispose = Disposable.Create(
|
||||
attach_prop_changed_cb(_bid_direct_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(bid_direct_size_text))),
|
||||
attach_prop_changed_cb(_bid_implied_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(bid_implied_size_text))),
|
||||
attach_prop_changed_cb(_bid_merged_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(bid_merged_size_text))),
|
||||
attach_prop_changed_cb(_ask_direct_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(ask_direct_size_text))),
|
||||
attach_prop_changed_cb(_ask_implied_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(ask_implied_size_text))),
|
||||
attach_prop_changed_cb(_ask_merged_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(ask_merged_size_text))),
|
||||
_orders_model.on_security_root_orders(security.Id).OnDispatcher().Subscribe(_on_orders),
|
||||
_position.OnDispatcher().Subscribe(v => _animation.animate_position(v.newval > v.oldval)),
|
||||
_orders_model.on_security_positions(security.Id).OnDispatcher().Subscribe(t => {
|
||||
if (Set(ref _raw_day_position, t.state.Values.Sum(p => p.Item2.raw_day_pos()), nameof(raw_day_position)))
|
||||
if (Set(ref _raw_day_position, t.state.Values.Sum(p => p.Item2.raw_day_pos()), nameof(raw_day_position))) {
|
||||
RaisePropertyChanged(nameof(raw_day_position_text));
|
||||
raise_invalidated();
|
||||
}
|
||||
}),
|
||||
_inside_bids.OnDispatcher().Subscribe(v => _animation.animate_working_bids_inside(v.newval)),
|
||||
_inside_asks.OnDispatcher().Subscribe(v => _animation.animate_working_asks_inside(v.newval)),
|
||||
_day_change.OnDispatcher().Subscribe(v => _animation.animate_day_change(v.newval > v.oldval)),
|
||||
Disposable.Create(() => _animation.AnimationsChanged -= on_animations_changed)
|
||||
);
|
||||
|
||||
uint convert_display_qty_uint(uint value) =>
|
||||
value.safe_divide(_display_qty_divisor);
|
||||
|
||||
string fmt_display_qty_int(int value) =>
|
||||
value.safe_divide(_display_qty_divisor).ToString();
|
||||
|
||||
string fmt_display_qty_uint(uint value) =>
|
||||
value.safe_divide(_display_qty_divisor).ToString();
|
||||
|
||||
string fmt_display_qty_ulong(ulong value) =>
|
||||
value.safe_divide(_display_qty_divisor).ToString();
|
||||
|
||||
PropertyChange<T> mk_echo_prop_changed_cb<T>(params string[] props) =>
|
||||
(oldval, newval) => RaisePropertyChanged(props);
|
||||
|
||||
IDisposable attach_prop_changed_cb<TView, TModel>(PropertyAutoWrapper<TModel, TView> prop, PropertyChange<(TModel, TView)> callback) {
|
||||
prop.changed += callback;
|
||||
return Disposable.Create(() => prop.changed -= callback);
|
||||
}
|
||||
}
|
||||
|
||||
public event Action invalidated;
|
||||
@@ -109,32 +141,63 @@ namespace mbts.spark {
|
||||
public bool inside_bids => _inside_bids.value;
|
||||
public bool inside_asks => _inside_asks.value;
|
||||
public int position => _position.value;
|
||||
public string position_text => _position.text;
|
||||
public double raw_day_position => _raw_day_position;
|
||||
public string raw_day_position_text => SparkUtils.fmt_position(_raw_day_position.safe_divide(_display_qty_divisor));
|
||||
public double last_price => _last_price.Value;
|
||||
public bool last_price_from_spread => _last_price_from_spread.Value;
|
||||
public uint last_size => _last_size.value;
|
||||
public string last_size_text => _last_size.text;
|
||||
// bid price/size
|
||||
public double bid_price => _bid_price.Value;
|
||||
public uint bid_direct_size => _bid_direct_size.Value;
|
||||
public uint bid_implied_size => _bid_merged_size.Value - _bid_direct_size.Value;
|
||||
public uint bid_merged_size => _bid_merged_size.Value;
|
||||
public string bid_merged_size_text => _bid_merged_size_text ?? (_bid_merged_size_text = _size_formatter(_bid_merged_size.Value, bid_implied_size));
|
||||
public uint bid_direct_size => _bid_direct_size.model;
|
||||
public uint bid_implied_size => _bid_merged_size.model - _bid_direct_size.model;
|
||||
public uint bid_merged_size => _bid_merged_size.model;
|
||||
// bid size text
|
||||
public string bid_direct_size_text => _bid_direct_size.view.ToString();
|
||||
public string bid_implied_size_text => _bid_implied_size.view.ToString();
|
||||
public string bid_merged_size_text => _bid_merged_size.view.ToString();
|
||||
// ask price/size
|
||||
public double ask_price => _ask_price.Value;
|
||||
public uint ask_direct_size => _ask_direct_size.Value;
|
||||
public uint ask_implied_size => _ask_merged_size.Value - _ask_direct_size.Value;
|
||||
public uint ask_merged_size => _ask_merged_size.Value;
|
||||
public string ask_merged_size_text => _ask_merged_size_text ?? (_ask_merged_size_text = _size_formatter(_ask_merged_size.Value, ask_implied_size));
|
||||
public uint ask_direct_size => _ask_direct_size.model;
|
||||
public uint ask_implied_size => _ask_implied_size.model;
|
||||
public uint ask_merged_size => _ask_merged_size.model;
|
||||
// ask size text
|
||||
public string ask_direct_size_text => _ask_direct_size.view.ToString();
|
||||
public string ask_implied_size_text => _ask_implied_size.view.ToString();
|
||||
public string ask_merged_size_text => _ask_merged_size.view.ToString();
|
||||
//
|
||||
public ulong volume => _volume.value;
|
||||
public string volume_text => _volume.text;
|
||||
public double settlement => _settlement.Value;
|
||||
public double day_change => _day_change.Value;
|
||||
public double session_low => _session_low.Value;
|
||||
public double session_high => _session_high.Value;
|
||||
public int working_bid_volume => _working_bid_volume.value;
|
||||
public int working_ask_volume => _working_ask_volume.value;
|
||||
public string working_bids_text => OrderUtils.order_collection_summary(_order_summarizer.best_working_bid, _order_summarizer.best_working_bid_qty, _order_summarizer.working_bid_qty, security.ContractSpec);
|
||||
public string working_asks_text => OrderUtils.order_collection_summary(_order_summarizer.best_working_ask, _order_summarizer.best_working_ask_qty, _order_summarizer.working_ask_qty, security.ContractSpec);
|
||||
public string working_bids_text => OrderUtils.order_collection_summary(
|
||||
_order_summarizer.best_working_bid,
|
||||
_order_summarizer.best_working_bid_qty.safe_divide(_display_qty_divisor),
|
||||
_order_summarizer.working_bid_qty.safe_divide(_display_qty_divisor),
|
||||
security.ContractSpec);
|
||||
public string working_asks_text => OrderUtils.order_collection_summary(
|
||||
_order_summarizer.best_working_ask,
|
||||
_order_summarizer.best_working_ask_qty.safe_divide(_display_qty_divisor),
|
||||
_order_summarizer.working_ask_qty.safe_divide(_display_qty_divisor),
|
||||
security.ContractSpec);
|
||||
public double working_best_bid => _working_best_bid.value;
|
||||
public double working_best_ask => _working_best_ask.value;
|
||||
// ice props
|
||||
public string market_description => security.market_description();
|
||||
public string cleared_alias => security.cleared_alias();
|
||||
public string product_description => security.product_name();
|
||||
public string hub_alias => security.hub_alias();
|
||||
public string strip_name => security.strip_name();
|
||||
public string mic_code => security.mic_code();
|
||||
// animations
|
||||
public IList<ui.spark.CellAnimation> animations => _animation.get_animations().ToList();
|
||||
public ObservableCollection<RowAnimation> row_animations { get; } = new ObservableCollection<RowAnimation>();
|
||||
// commands
|
||||
public ICommand bid_price_click { get; }
|
||||
public ICommand bid_merged_size_click { get; }
|
||||
public ICommand bid_direct_size_click { get; }
|
||||
@@ -146,27 +209,30 @@ namespace mbts.spark {
|
||||
public ICommand ask_implied_size_click { get; }
|
||||
public ICommand working_asks_click { get; }
|
||||
public ICommand default_command { get; }
|
||||
public string market_description {get;}
|
||||
public string cleared_alias { get; }
|
||||
public string product_description { get; }
|
||||
public string hub_alias { get; }
|
||||
public string strip_name{ get; }
|
||||
public string mic_code { get; }
|
||||
|
||||
public ObservableCollection<RowAnimation> row_animations { get; } = new ObservableCollection<RowAnimation>();
|
||||
|
||||
public IReadOnlyList<DIYGrid.IGridCellStore> cell_stores => new DIYGrid.IGridCellStore[0];
|
||||
#endregion
|
||||
|
||||
uint _display_qty_divisor => _display_qty_divisor_backing ??
|
||||
(_display_qty_divisor_backing = _orders_model.security_auxiliary_model.get_display_quantity_divisor(security)).Value;
|
||||
|
||||
public void raise_invalidated() => invalidated?.Invoke();
|
||||
|
||||
public void refresh() {
|
||||
bool dirty = false;
|
||||
if (md.md.get_bbo(ref _bbo_ref, out NativePriceSize b, out NativePriceSize a)) {
|
||||
_bid_price.set(b.price);
|
||||
refresh_size(ref b, _bid_direct_size, _bid_merged_size, ref _bid_merged_size_text, nameof(bid_merged_size_text));
|
||||
// don't use the '||' operator - every 'set_new' call must be made
|
||||
// (the operation mustn't terminate the first time a set_new call returns true).
|
||||
dirty |= (
|
||||
_bid_direct_size.set_new(b.direct_size) |
|
||||
_bid_implied_size.set_new(b.implied_size) |
|
||||
_bid_merged_size.set_new(b.size) |
|
||||
_ask_direct_size.set_new(a.direct_size) |
|
||||
_ask_implied_size.set_new(a.implied_size) |
|
||||
_ask_merged_size.set_new(a.size));
|
||||
|
||||
_ask_price.set(a.price);
|
||||
refresh_size(ref a, _ask_direct_size, _ask_merged_size, ref _ask_merged_size_text, nameof(ask_merged_size_text));
|
||||
_inside_bids.on_next_distinct(_bid_price.Value >= _working_best_bid.value, ref dirty);
|
||||
_inside_asks.on_next_distinct(_ask_price.Value <= _working_best_ask.value, ref dirty);
|
||||
}
|
||||
@@ -183,16 +249,6 @@ namespace mbts.spark {
|
||||
_volume.on_next_distinct(md.md.total_volume, ref dirty);
|
||||
if (dirty)
|
||||
invalidated?.Invoke();
|
||||
|
||||
void refresh_size(ref NativePriceSize nps, Property<uint> direct, Property<uint> merged, ref string text, string text_prop) {
|
||||
if (nps.size != merged.Value || nps.direct_size != direct.Value) {
|
||||
merged.set(nps.size);
|
||||
direct.set(nps.direct_size);
|
||||
text = null;
|
||||
dirty = true;
|
||||
RaisePropertyChanged(text_prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(Row other) =>
|
||||
|
||||
@@ -9,6 +9,7 @@ using mbts.ui.utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
@@ -18,6 +19,9 @@ using static mbts.ui.spark.MarketViewView;
|
||||
|
||||
namespace mbts.spark {
|
||||
partial class MarketViewVM : SecurityMDGrid<MarketViewVM.Row, SecurityMD> {
|
||||
static IReadOnlyList<Func<MarketViewVM, IDataGridColumnFilter<Row>>> _filter_getters =
|
||||
ObjectSnapshot.property_delegates_of_type<MarketViewVM, IDataGridColumnFilter<Row>>();
|
||||
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
IWindowSettingsManager<GridSettings> _settings_manager;
|
||||
@@ -37,21 +41,21 @@ namespace mbts.spark {
|
||||
const double working_orders_col_width = 105;
|
||||
IMultiValueConverter price_converter = new ui.spark.converters.TickDisplayConverter();
|
||||
add_col(Columns.Security, "Security", r => r.security.ToString(), nameof(Row.security), DefaultColumnWidths.Security, security_filter, DataGridFilterKnownTemplates.Regex);
|
||||
add_col(Columns.Volume, "Volume", r => r.volume.ToString(), nameof(Row.volume), DefaultColumnWidths.Volume, volume_filter, DataGridFilterKnownTemplates.ULongMinMax);
|
||||
add_col(Columns.Volume, "Volume", r => r.volume_text, nameof(Row.volume), DefaultColumnWidths.Volume, volume_filter, DataGridFilterKnownTemplates.ULongMinMax);
|
||||
add_col(Columns.LastSpd, "Spd", r => r.last_price_from_spread ? "S" : "", nameof(Row.last_price_from_spread), DefaultColumnWidths.Side, last_price_from_spread_filter, DataGridFilterKnownTemplates.Regex);
|
||||
add_col(Columns.Last, "Last", r => r.format_price(r.last_price), nameof(Row.last_price), DefaultColumnWidths.Price, last_price_filter, DataGridFilterKnownTemplates.DoubleMinMax);
|
||||
add_col(Columns.LastSize, merged_size_col_header, r => r.last_size.ToString(), nameof(Row.last_size), DefaultColumnWidths.Size, last_size_filter, DataGridFilterKnownTemplates.UIntMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.Position, "Pos", r => r.position.ToString(), nameof(Row.position), DefaultColumnWidths.Size, position_filter, DataGridFilterKnownTemplates.IntMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.RawDayPosition, "Raw Day Pos", r => SparkUtils.fmt_position(r.raw_day_position), nameof(Row.raw_day_position), DefaultColumnWidths.Size, raw_day_position_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.LastSize, merged_size_col_header, r => r.last_size_text, nameof(Row.last_size), DefaultColumnWidths.Size, last_size_filter, DataGridFilterKnownTemplates.UIntMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.Position, "Pos", r => r.position_text, nameof(Row.position), DefaultColumnWidths.Size, position_filter, DataGridFilterKnownTemplates.IntMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.RawDayPosition, "Raw Day Pos", r => r.raw_day_position_text, nameof(Row.raw_day_position), DefaultColumnWidths.Size, raw_day_position_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_hl_working_orders_col(Columns.WorkingBids, working_col_header, nameof(Row.working_bids_text), nameof(Row.working_best_bid), nameof(Row.working_bids_click), working_bid_volume_filter, BTSColors.MarketData.bid, System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.BidImpliedSize, implied_size_col_header, r => r.bid_implied_size.ToString(), nameof(Row.bid_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.BidDirectSize, direct_size_col_header, r => r.bid_direct_size.ToString(), nameof(Row.bid_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.BidImpliedSize, implied_size_col_header, r => r.bid_implied_size_text, nameof(Row.bid_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.BidDirectSize, direct_size_col_header, r => r.bid_direct_size_text, nameof(Row.bid_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_hl_merged_size_col(Columns.BidMergedSize, merged_size_col_header, nameof(Row.bid_merged_size), nameof(Row.bid_merged_size_text), nameof(Row.bid_merged_size_click), bid_merged_size_filter, BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_hl_price_col(Columns.Bid, "Bid", nameof(Row.bid_price), nameof(Row.bid_price_click), bid_price_filter, BTSColors.header_text, BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_hl_price_col(Columns.Ask, "Ask", nameof(Row.ask_price), nameof(Row.ask_price_click), ask_price_filter, BTSColors.header_text, BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_hl_merged_size_col(Columns.AskMergedSize, merged_size_col_header, nameof(Row.ask_merged_size), nameof(Row.ask_merged_size_text), nameof(Row.ask_merged_size_click), ask_merged_size_filter, BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.AskDirectSize, direct_size_col_header, r => r.ask_direct_size.ToString(), nameof(Row.ask_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.AskImpliedSize, implied_size_col_header, r => r.ask_implied_size.ToString(), nameof(Row.ask_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.AskDirectSize, direct_size_col_header, r => r.ask_direct_size_text, nameof(Row.ask_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.AskImpliedSize, implied_size_col_header, r => r.ask_implied_size_text, nameof(Row.ask_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_hl_working_orders_col(Columns.WorkingAsks, working_col_header, nameof(Row.working_asks_text), nameof(Row.working_best_ask), nameof(Row.working_asks_click), working_ask_volume_filter, BTSColors.MarketData.ask, System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.Settlement, "Settlement", r => r.format_price(r.settlement), nameof(Row.settlement), DefaultColumnWidths.Settlement, settlement_filter, DataGridFilterKnownTemplates.DoubleMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
add_col(Columns.DayChange, "Change", r => r.format_price(r.day_change), nameof(Row.day_change), DefaultColumnWidths.Price, day_change_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
@@ -151,14 +155,16 @@ namespace mbts.spark {
|
||||
working_ask_volume_filter.load_state(filters.working_ask_volume);
|
||||
volume_filter.load_state(filters.volume);
|
||||
security_key_filter.load_state(filters.security_key);
|
||||
market_description_filter.load_state(filters.market_description);
|
||||
cleared_alias_filter.load_state(filters.cleared_alias);
|
||||
product_description_filter.load_state(filters.product_description);
|
||||
hub_alias_filter.load_state(filters.hub_alias);
|
||||
strip_name_filter.load_state(filters.strip_name);
|
||||
mic_code_filter.load_state(filters.mic_code);
|
||||
}
|
||||
rows_view.set_sort_descriptions(s.sorting_criterion, columns);
|
||||
}
|
||||
}), DataGridFilterHelper.hook_filters(rows_view, security_filter,
|
||||
bid_price_filter, bid_merged_size_filter, bid_direct_size_filter, bid_implied_size_filter,
|
||||
ask_price_filter, ask_merged_size_filter, ask_direct_size_filter, ask_implied_size_filter,
|
||||
last_price_filter, last_price_from_spread_filter, last_size_filter, day_change_filter, settlement_filter,
|
||||
session_low_filter, session_high_filter, position_filter, raw_day_position_filter, working_bid_volume_filter, working_ask_volume_filter, volume_filter, security_key_filter),
|
||||
}), DataGridFilterHelper.hook_filters(rows_view, _filters),
|
||||
settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager),
|
||||
WorkLoop.Start("MarketView", _refresh)
|
||||
);
|
||||
@@ -233,13 +239,15 @@ namespace mbts.spark {
|
||||
public IDataGridColumnPersistentFilter<Row, ULongMinMaxState> volume_filter { get; } = new ULongMinMax<Row>(r => r.volume, nameof(Row.volume));
|
||||
public IDataGridColumnPersistentFilter<Row, string> security_key_filter { get; } = new RegexFilterVM<Row>(r => r.security_key.ToString(), nameof(Row.security_key));
|
||||
|
||||
public IDataGridColumnPersistentFilter<Row, string> market_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MarketDescription : "", nameof(Row.market_description));
|
||||
public IDataGridColumnPersistentFilter<Row, string> cleared_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ClearedAlias : "", nameof(Row.cleared_alias));
|
||||
public IDataGridColumnPersistentFilter<Row, string> product_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ProductName : "", nameof(Row.product_description));
|
||||
public IDataGridColumnPersistentFilter<Row, string> hub_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.HubAlias : "", nameof(Row.hub_alias));
|
||||
public IDataGridColumnPersistentFilter<Row, string> strip_name_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.StripName : "", nameof(Row.strip_name));
|
||||
public IDataGridColumnPersistentFilter<Row, string> mic_code_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MicCode : "", nameof(Row.mic_code));
|
||||
|
||||
public IDataGridColumnPersistentFilter<Row, string> market_description_filter { get; } = new RegexFilterVM<Row>(r => r.market_description, nameof(Row.market_description));
|
||||
public IDataGridColumnPersistentFilter<Row, string> cleared_alias_filter { get; } = new RegexFilterVM<Row>(r => r.cleared_alias, nameof(Row.cleared_alias));
|
||||
public IDataGridColumnPersistentFilter<Row, string> product_description_filter { get; } = new RegexFilterVM<Row>(r => r.product_description, nameof(Row.product_description));
|
||||
public IDataGridColumnPersistentFilter<Row, string> hub_alias_filter { get; } = new RegexFilterVM<Row>(r => r.hub_alias, nameof(Row.hub_alias));
|
||||
public IDataGridColumnPersistentFilter<Row, string> strip_name_filter { get; } = new RegexFilterVM<Row>(r => r.strip_name, nameof(Row.strip_name));
|
||||
public IDataGridColumnPersistentFilter<Row, string> mic_code_filter { get; } = new RegexFilterVM<Row>(r => r.mic_code, nameof(Row.mic_code));
|
||||
|
||||
IEnumerable<IDataGridColumnFilter<Row>> _filters =>
|
||||
_filter_getters.Select(fn => fn(this));
|
||||
|
||||
public FilterStates get_filter_states() {
|
||||
return new FilterStates {
|
||||
|
||||
@@ -22,12 +22,15 @@ namespace bts.shell.time_and_sales {
|
||||
.to_safe();
|
||||
|
||||
readonly TradeMessageImmutable _trade;
|
||||
readonly Func<SecurityImmutable?, uint> _get_display_qty_divisor;
|
||||
uint? _display_size;
|
||||
|
||||
public TimeAndSalesRowVM(int row_key, SecurityImmutable security, TradeMessageImmutable trade) {
|
||||
public TimeAndSalesRowVM(int row_key, SecurityImmutable security, TradeMessageImmutable trade, Func<SecurityImmutable?, uint> get_display_qty_divisor) {
|
||||
this.row_key = row_key;
|
||||
this.security = security;
|
||||
_trade = trade;
|
||||
trade_key = trade.Key;
|
||||
_get_display_qty_divisor = get_display_qty_divisor ?? new Func<SecurityImmutable?, uint>(sec => 1);
|
||||
}
|
||||
|
||||
public event Action invalidated;
|
||||
@@ -39,6 +42,8 @@ namespace bts.shell.time_and_sales {
|
||||
public DateTime time => ((ulong)_trade.Time).lldatetime().ToLocalTime();
|
||||
public double price => _trade.price();
|
||||
public uint size => _trade.Size;
|
||||
public uint display_size => _display_size ??
|
||||
(_display_size = size.safe_divide(_get_display_qty_divisor(security))).Value;
|
||||
public AggressorSide aggressor_side => _trade.AggressorSide;
|
||||
|
||||
public void raise_invalidated() =>
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace bts.shell.time_and_sales {
|
||||
_refresh_filter,
|
||||
_refresh_sort,
|
||||
_refresh_selected_rows;
|
||||
readonly Func<SecurityImmutable?, uint> _get_display_qty_divisor;
|
||||
IComparer<TimeAndSalesRowVM> _comparer_backing;
|
||||
Func<TimeAndSalesRowVM, bool> _filter_backing;
|
||||
ImmutableHashSet<TimeAndSalesRowVM> _selected_rows = ImmutableHashSet<TimeAndSalesRowVM>.Empty;
|
||||
@@ -58,8 +59,9 @@ namespace bts.shell.time_and_sales {
|
||||
_filter_change_count,
|
||||
_sort_change_count;
|
||||
|
||||
public TimeAndSalesVM(IWindowSettingsManager<GridSettings> settings_manager) {
|
||||
public TimeAndSalesVM(IWindowSettingsManager<GridSettings> settings_manager, Func<SecurityImmutable?, uint> get_display_qty_divisor) {
|
||||
_sort_manager = new ColumnSortManager(columns.WrapSelector(c => c as ISortableDIYGridColumn, null), TimeAndSalesView.Columns.RowKey.ToString());
|
||||
_get_display_qty_divisor = get_display_qty_divisor ?? new Func<SecurityImmutable?, uint>(sec => 1);
|
||||
_comparer_backing = _sort_manager.BuildComparer(_get_column_comparer);
|
||||
_log = new LoggerContext(Log.Ui, $"Time And Sales VM ({Interlocked.Increment(ref _id_counter)})", null);
|
||||
_settings_manager = settings_manager;
|
||||
@@ -95,7 +97,7 @@ namespace bts.shell.time_and_sales {
|
||||
System.Windows.HorizontalAlignment.Right);
|
||||
add_column(
|
||||
TimeAndSalesView.Columns.Size,
|
||||
r => r.size.ToString(),
|
||||
r => r.display_size.ToString(),
|
||||
DefaultColumnHeaders.Size,
|
||||
DefaultColumnWidths.Size,
|
||||
size_filter,
|
||||
@@ -304,7 +306,7 @@ namespace bts.shell.time_and_sales {
|
||||
}
|
||||
var all_rows_bd = _all_rows.ToBuilder();
|
||||
foreach (var t in trades) {
|
||||
var r = _row_factory.create(t.security, t.trade);
|
||||
var r = _row_factory.create(t.security, t.trade, _get_display_qty_divisor);
|
||||
all_rows_bd.Add(r);
|
||||
if (_filter_backing == null || _filter_backing(r))
|
||||
rows.InsertSorted(r, _comparer);
|
||||
@@ -400,11 +402,11 @@ namespace bts.shell.time_and_sales {
|
||||
// the row key must be unique to each row in the collection
|
||||
int _row_id_counter;
|
||||
|
||||
public TimeAndSalesRowVM create(TradeInfo trade_info) =>
|
||||
new TimeAndSalesRowVM(_row_id_counter++, trade_info.security, trade_info.trade);
|
||||
public TimeAndSalesRowVM create(TradeInfo trade_info, Func<SecurityImmutable?, uint> get_display_qty_divisor) =>
|
||||
new TimeAndSalesRowVM(_row_id_counter++, trade_info.security, trade_info.trade, get_display_qty_divisor);
|
||||
|
||||
public TimeAndSalesRowVM create(SecurityImmutable security, TradeMessageImmutable trade) =>
|
||||
new TimeAndSalesRowVM(_row_id_counter++, security, trade);
|
||||
public TimeAndSalesRowVM create(SecurityImmutable security, TradeMessageImmutable trade, Func<SecurityImmutable?, uint> get_display_qty_divisor) =>
|
||||
new TimeAndSalesRowVM(_row_id_counter++, security, trade, get_display_qty_divisor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ namespace bts.shell.time_and_sales {
|
||||
readonly TimeAndSalesWindowFactory _factory;
|
||||
readonly IWindowSettingsManager<GridSettings> _settings_manager;
|
||||
readonly bool _can_pause_updates;
|
||||
readonly Func<SecurityImmutable?, uint> _get_display_qty_divisor;
|
||||
TimeAndSalesVM _vm;
|
||||
TimeAndSalesModel _model;
|
||||
SecurityCriteria _security_criteria_backing;
|
||||
@@ -35,12 +36,21 @@ namespace bts.shell.time_and_sales {
|
||||
SparkMenuDisplayVM _menu_display_vm;
|
||||
IDisposable _subscriptions;
|
||||
|
||||
public TimeAndSalesWindow(TimeAndSalesWindowFactory factory, IBus bus, MDConsumer consumer, TimeAndSalesSettings state, ImageSource icon, IWindowSettingsManager<GridSettings> settings_manager, bool can_pause_updates)
|
||||
public TimeAndSalesWindow(
|
||||
TimeAndSalesWindowFactory factory,
|
||||
IBus bus,
|
||||
MDConsumer consumer,
|
||||
TimeAndSalesSettings state,
|
||||
ImageSource icon,
|
||||
IWindowSettingsManager<GridSettings> settings_manager,
|
||||
bool can_pause_updates,
|
||||
Func<SecurityImmutable?, uint> get_display_qty_divisor)
|
||||
: base("Time And Sales", bus, factory, SparkCommands.ShowTimeAndSales.icon) {
|
||||
Log.assert(state != null);
|
||||
_consumer = consumer;
|
||||
_factory = factory;
|
||||
_state = state ?? TimeAndSalesSettings.Default(new SecurityCriteria());
|
||||
_get_display_qty_divisor = get_display_qty_divisor ?? new Func<SecurityImmutable?, uint>(sec => 1);
|
||||
if (_state.security_criteria == null)
|
||||
_state = _state.with(new SecurityCriteria());
|
||||
_security_criteria_backing = _state.security_criteria;
|
||||
@@ -86,7 +96,7 @@ namespace bts.shell.time_and_sales {
|
||||
_settings_manager.instance_settings = _state.grid;
|
||||
_menu_display_vm = new SparkMenuDisplayVM();
|
||||
_menu_display_vm.load_from(_state.menu_display);
|
||||
_vm = new TimeAndSalesVM(_settings_manager);
|
||||
_vm = new TimeAndSalesVM(_settings_manager, _get_display_qty_divisor);
|
||||
var selected_rows_src = Observable.Create<ImmutableHashSet<TimeAndSalesRowVM>>(o => {
|
||||
_vm.selected_rows_changed += o.OnNext;
|
||||
return Disposable.Create(() => _vm.selected_rows_changed -= o.OnNext);
|
||||
|
||||
@@ -20,12 +20,15 @@ namespace bts.shell {
|
||||
readonly MDConsumer _consumer;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
readonly TimeAndSalesConfig _time_and_sales_conf;
|
||||
readonly ISecurityAuxiliaryDataModel _security_auxiliary_model;
|
||||
IReadOnlyDictionary<uint, SecurityMD> _securities = new Dictionary<uint, SecurityMD>();
|
||||
|
||||
public TimeAndSalesWindowFactory(IShellModule module, IBus bus, MDConsumer consumer, UserSettingsHub settings_hub) : base(module, "TimeAndSales") {
|
||||
public TimeAndSalesWindowFactory(IShellModule module, IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, ISecurityAuxiliaryDataModel security_auxiliary_model)
|
||||
: base(module, "TimeAndSales") {
|
||||
_bus = bus;
|
||||
_consumer = consumer;
|
||||
_settings_hub = settings_hub;
|
||||
_security_auxiliary_model = security_auxiliary_model;
|
||||
_time_and_sales_conf = new TimeAndSalesConfig(Configuration.get<bts.config.TimeAndSalesOptions>());
|
||||
_dispose = Disposable.Create(
|
||||
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(s => _securities = s),
|
||||
@@ -38,7 +41,8 @@ namespace bts.shell {
|
||||
public override Task<IShellWindow> load(TimeAndSalesSettings state) =>
|
||||
Task.FromResult<IShellWindow>(new TimeAndSalesWindow(this, _bus, _consumer, state, SparkCommands.PickTimeAndSales.icon,
|
||||
new WindowSettingsManager<GridSettings>(_settings_hub, state.grid, s => s.time_and_sales_grid, (s, g) => s.with(time_and_sales:g)),
|
||||
_time_and_sales_conf.can_pause_updates));
|
||||
_time_and_sales_conf.can_pause_updates,
|
||||
_security_auxiliary_model.get_display_quantity_divisor));
|
||||
|
||||
public async void show(SecurityCriteria criteria) {
|
||||
var w = await load(new TimeAndSalesSettings(criteria));
|
||||
|
||||
@@ -549,24 +549,48 @@
|
||||
</u:BindingUpdateGroupCollection>
|
||||
</u:Behavior.BindingUpdateGroups>
|
||||
</TextBox>
|
||||
<TextBlock
|
||||
<StackPanel
|
||||
x:Name="PART_TickFactors_Label"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
Text="Ticks"
|
||||
IsEnabled="True"
|
||||
Orientation="Horizontal"
|
||||
Visibility="{Binding LegTuples.Count, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource IntNotZeroVisibilityConverter}}"
|
||||
/>
|
||||
<c:TickFactorsPicker
|
||||
>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
Text="Ticks ("
|
||||
/>
|
||||
<CheckBox
|
||||
Margin="1,0,0,0"
|
||||
IsChecked="{Binding UseFractionalTicks, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
VerticalAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Padding="0"
|
||||
>
|
||||
fractional
|
||||
</CheckBox>
|
||||
<TextBlock VerticalAlignment="Center" Margin="1,0,0,0">)</TextBlock>
|
||||
</StackPanel>
|
||||
<Grid
|
||||
x:Name="PART_TickFactors_Picker"
|
||||
Grid.Column="3"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsEnabled="True"
|
||||
SelectedTickFactors="{Binding TickFactors, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
Visibility="{Binding LegTuples.Count, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource IntNotZeroVisibilityConverter}}"
|
||||
/>
|
||||
>
|
||||
<c:TickFactorsPicker
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectedTickFactors="{Binding TickFactors, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
|
||||
Visibility="{Binding UseFractionalTicks, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static cv:BooleanToVisibilityConverter.HiddenInstance}}"
|
||||
/>
|
||||
<c:TickUpDownInt
|
||||
HorizontalAlignment="Stretch"
|
||||
Minimum="1"
|
||||
Value="{Binding IntegerTickSize, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Visibility="{Binding UseFractionalTicks, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource FalseVisibilityConverter}}"
|
||||
/>
|
||||
</Grid>
|
||||
<TextBlock
|
||||
x:Name="PART_Pricing_Label"
|
||||
Grid.Column="4"
|
||||
|
||||
@@ -52,6 +52,26 @@ namespace mbts.ui.spark {
|
||||
public TickFactors TickFactors { get => GetValue(TickFactorsProperty) as TickFactors; set => SetValue(TickFactorsProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region UseFractionalTicks
|
||||
public static readonly DependencyProperty UseFractionalTicksProperty = WPFHelper.create_dp<bool, ExecutionStrategyEditor>(
|
||||
nameof(UseFractionalTicks), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, true);
|
||||
|
||||
public bool UseFractionalTicks {
|
||||
get => (bool)GetValue(UseFractionalTicksProperty);
|
||||
set => SetValue(UseFractionalTicksProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IntegerTickSize
|
||||
public static readonly DependencyProperty IntegerTickSizeProperty = WPFHelper.create_dp<int, ExecutionStrategyEditor>(
|
||||
nameof(IntegerTickSize), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 1);
|
||||
|
||||
public int IntegerTickSize {
|
||||
get => (int)GetValue(IntegerTickSizeProperty);
|
||||
set => SetValue(IntegerTickSizeProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IsNew
|
||||
public static readonly DependencyProperty IsNewProperty =
|
||||
DependencyProperty.Register(
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
ContractPickerModel="{Binding contract_picker_model}"
|
||||
HedgePricing="{Binding hedge_pricing}"
|
||||
HedgePricingItemsSource="{Binding hedge_pricing_options}"
|
||||
UseFractionalTicks="{Binding use_fractional_ticks}"
|
||||
IntegerTickSize="{Binding integer_tick_size}"
|
||||
/>
|
||||
<c:DialogSkirtOKCancel Grid.Row="1" OKCommand="{Binding commit}" CloseOnOK="False"/>
|
||||
</Grid>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using bts.collections;
|
||||
using bts.shell;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
@@ -30,8 +29,16 @@ namespace mbts.spark {
|
||||
readonly Property<bool> _has_working_orders, _can_add_leg;
|
||||
|
||||
Security _synthetic;
|
||||
bool _set_name_automatically, _set_ticks_automatically, _set_quantities_automatically, _set_price_mults_automatically, _is_setting_quantities, _is_setting_price_mults;
|
||||
bool
|
||||
_set_name_automatically,
|
||||
_set_ticks_automatically,
|
||||
_set_quantities_automatically,
|
||||
_set_price_mults_automatically,
|
||||
_is_setting_quantities,
|
||||
_is_setting_price_mults,
|
||||
_use_fractional_ticks = true;
|
||||
TickFactors _tick_factors;
|
||||
int _integer_tick_size = 1;
|
||||
ExecutionStrategyImmutable _strategy;
|
||||
HedgePricing _hedge_pricing;
|
||||
|
||||
@@ -44,6 +51,10 @@ namespace mbts.spark {
|
||||
: this(synthetic, strategy, false) {
|
||||
ExceptionAssert.InvalidOperation.That(synthetic.Id != 0, "This constructor may only be used for already-existing synthetic securities (id must be non-zero).");
|
||||
_tick_factors = TickUtils.get_regional_ticks(synthetic.ContractSpec).low_tick;
|
||||
if (TickUtils.is_integer_tick(synthetic.ContractSpec)) {
|
||||
_use_fractional_ticks = false;
|
||||
_integer_tick_size = DoubleHelper.to_optional_int32(synthetic.ContractSpec.LowTick) ?? 0;
|
||||
}
|
||||
title = $"Edit '{synthetic.Synthetic.Name}'";
|
||||
icon = Icons.ModifySynthetic;
|
||||
_has_working_orders = backing(nameof(has_working_orders), security_has_working_orders.OnDispatcher());
|
||||
@@ -131,14 +142,14 @@ namespace mbts.spark {
|
||||
flash_error_msg("Name already taken", SparkUtils.UI.MessageFlashDuration, nameof(name)).ignore();
|
||||
valid = false;
|
||||
}
|
||||
if (_tick_factors == null || !_tick_factors.is_valid()) {
|
||||
flash_error_msg("Please select ticks", SparkUtils.UI.MessageFlashDuration, nameof(tick_factors)).ignore();
|
||||
if (use_fractional_ticks ? (_tick_factors == null || !_tick_factors.is_valid()) : integer_tick_size == 0) {
|
||||
flash_error_msg("Please select ticks", SparkUtils.UI.MessageFlashDuration, use_fractional_ticks ? nameof(tick_factors) : nameof(integer_tick_size)).ignore();
|
||||
valid = false;
|
||||
}
|
||||
if (valid) {
|
||||
_synthetic.Synthetic.Legs.SetItems(leg_tuples.Select(l => l.leg));
|
||||
_synthetic.Symbol = _deduce_symbol(_synthetic);
|
||||
_synthetic.ContractSpec = _tick_factors.to_spec();
|
||||
_synthetic.ContractSpec = use_fractional_ticks ? _tick_factors.to_spec() : TickUtils.create_integer_tick_spec(integer_tick_size);
|
||||
uint? id = await model.create_synthetic(new SecurityImmutable(_synthetic));
|
||||
if (id.HasValue && id != 0) {
|
||||
if (await model.save_strategy(id.Value, get_strategy())) {
|
||||
@@ -295,6 +306,20 @@ namespace mbts.spark {
|
||||
public bool has_working_orders => _has_working_orders.Value;
|
||||
public bool is_new { get; }
|
||||
public bool is_new_leg_popup_open { get; set; }
|
||||
public bool use_fractional_ticks {
|
||||
get => _use_fractional_ticks;
|
||||
set {
|
||||
if (Set(ref _use_fractional_ticks, value))
|
||||
_set_ticks_automatically = false;
|
||||
}
|
||||
}
|
||||
public int integer_tick_size {
|
||||
get => _integer_tick_size;
|
||||
set {
|
||||
if (Set(ref _integer_tick_size, value))
|
||||
_set_ticks_automatically = false;
|
||||
}
|
||||
}
|
||||
public bool can_add_leg => _can_add_leg.Value;
|
||||
public SyntheticContract.Types.PriceCalc price_calc { get => _synthetic.Synthetic.PriceCalc; set => _synthetic.Synthetic.PriceCalc = value; }
|
||||
public ObservableCollection<LegSpreaderParamsTuple> leg_tuples { get; } = new ObservableCollection<LegSpreaderParamsTuple>();
|
||||
@@ -323,6 +348,16 @@ namespace mbts.spark {
|
||||
_strategy = strategy;
|
||||
hedge_pricing = strategy.HedgePricing;
|
||||
leg_tuples.SetItems(_get_legs(_synthetic, strategy));
|
||||
if (synthetic.HasContractSpec) {
|
||||
/// assign the fields <see cref="_use_fractional_ticks"/> and <see cref="_integer_tick_size"/>,
|
||||
/// rather than the properties <see cref="use_fractional_ticks"/> and <see cref="integer_tick_size"/>
|
||||
/// because the setters of the former flip <see cref="_set_ticks_automatically"/> to false if the value changes.
|
||||
/// This behavior causes the ticks to be automatically deduced as legs are added/removed,
|
||||
/// until the user explicitly enters a different value (as the properties are bound to tick input controls in the view).
|
||||
Set(ref _use_fractional_ticks, !TickUtils.is_integer_tick(synthetic.ContractSpec), nameof(use_fractional_ticks));
|
||||
if (!use_fractional_ticks)
|
||||
Set(ref _integer_tick_size, DoubleHelper.to_optional_int32(synthetic.ContractSpec.LowTick) ?? 0, nameof(integer_tick_size));
|
||||
}
|
||||
RaisePropertyChanged(nameof(name), nameof(tick_factors));
|
||||
}
|
||||
|
||||
|
||||
@@ -236,12 +236,20 @@ namespace mbts.spark {
|
||||
get => algo_root;
|
||||
set => algo_root = value;
|
||||
}
|
||||
IAlgoVM IAlgoParamVM<IAlgoVM>.display_value {
|
||||
get => algo_root;
|
||||
set => algo_root = value;
|
||||
}
|
||||
bool IAlgoParamVM.has_value => _algo_root != null;
|
||||
bool IAlgoParamVM.is_enabled { get; set; } = true;
|
||||
object IAlgoParamVM.value {
|
||||
get => algo_root;
|
||||
set => algo_root = (IAlgoVM)value;
|
||||
}
|
||||
object IAlgoParamVM.display_value {
|
||||
get => algo_root;
|
||||
set => algo_root = (IAlgoVM)value;
|
||||
}
|
||||
AlgoParamPermissions _permissions = AlgoParamPermissions.ReadWrite;
|
||||
AlgoParamPermissions IAlgoParamVM.permissions { get => _permissions; set => _permissions = value; }
|
||||
|
||||
|
||||
+40
-4
@@ -199,18 +199,30 @@
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{u:BTSColor MarketData.bid}"
|
||||
Text="{Binding DataContext.bbo.bid_size, RelativeSource={RelativeSource AncestorType=usp:AlgoEditorGridView}}"
|
||||
ToolTip="Best Bid Size"
|
||||
/>
|
||||
>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}">
|
||||
<Binding Path="DataContext.bbo.bid_size" RelativeSource="{RelativeSource AncestorType=usp:AlgoEditorGridView}" Mode="OneWay"/>
|
||||
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:AlgoEditorGridView}" Mode="OneWay"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock
|
||||
x:Name="ask_text"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="4,0"
|
||||
Foreground="{u:BTSColor MarketData.ask}"
|
||||
Text="{Binding DataContext.bbo.ask_size, RelativeSource={RelativeSource AncestorType=usp:AlgoEditorGridView}}"
|
||||
ToolTip="Best Ask Size"
|
||||
/>
|
||||
>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}">
|
||||
<Binding Path="DataContext.bbo.ask_size" RelativeSource="{RelativeSource AncestorType=usp:AlgoEditorGridView}" Mode="OneWay"/>
|
||||
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:AlgoEditorGridView}" Mode="OneWay"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<DataTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding path_disambiguation}" Value="{x:Static sys:String.Empty}">
|
||||
@@ -272,6 +284,14 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<!--TODO: replace all the param.value bindings with param.display_value.
|
||||
The reason this has not already been done is that the display_value property is new,
|
||||
and only necessary for the size param as of 6/8/2020 (slightly more uniform code doesn't warrant rocking the boat).-->
|
||||
<u:TemplateSelectorKeyValue Key="{x:Static usp:AlgoParamTemplateKeys.Size}">
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding param.display_value}"/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
</usp:AlgoParamTemplateSelector.KeyedTemplates>
|
||||
<!--Bool-->
|
||||
<usp:AlgoParamTemplateSelector.BoolTemplate>
|
||||
@@ -360,6 +380,22 @@
|
||||
/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<!--TODO: replace all the param.value bindings with param.display_value.
|
||||
The reason this has not already been done is that the display_value property is new,
|
||||
and only necessary for the size param as of 6/8/2020 (slightly more uniform code doesn't warrant rocking the boat).-->
|
||||
<u:TemplateSelectorKeyValue Key="{x:Static usp:AlgoParamTemplateKeys.Size}">
|
||||
<DataTemplate>
|
||||
<c:TickUpDownNullableInt Focusable="False" HorizontalAlignment="Stretch" Value="{Binding param.display_value, UpdateSourceTrigger=PropertyChanged}" ArrowKeyTickModifiers="Ctrl">
|
||||
<c:TickUpDownNullableInt.CommandBindings>
|
||||
<CommandBinding Command="{x:Static u:NavigationHelper.MoveFocusCommand}" Executed="{x:Static u:NavigationHelper.KeyboardMoveFocusExecuted}"/>
|
||||
</c:TickUpDownNullableInt.CommandBindings>
|
||||
<c:TickUpDownNullableInt.InputBindings>
|
||||
<KeyBinding Key="Up" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Up}"/>
|
||||
<KeyBinding Key="Down" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Down}"/>
|
||||
</c:TickUpDownNullableInt.InputBindings>
|
||||
</c:TickUpDownNullableInt>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
</usp:AlgoParamTemplateSelector.KeyedTemplates>
|
||||
<!--Bool-->
|
||||
<usp:AlgoParamTemplateSelector.BoolTemplate>
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace mbts.spark {
|
||||
add_col(Columns.Account, DefaultColumnHeaders.Account, r => r.account, dv_filter_rx(_model.factory.subscribe_accounts().Select(d => d.Values.Ordered()), r => r.account), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Account);
|
||||
add_col(Columns.Exchange, DefaultColumnHeaders.Exchange, r => r.exchange.ToString(), dv_filter(EnumHelper.get_values<Venue>().ToArray(), r => r.security.exchange_or_default()), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Exchange);
|
||||
add_col(Columns.TimeInForce, DefaultColumnHeaders.TimeInForce, r => r.time_in_force.ToString(), dv_filter(EnumHelper.get_values<TimeInForce>().ToArray(), r => r.order?.time_in_force() ?? TimeInForce.Undefined), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.TimeInForce);
|
||||
add_col(Columns.Size, DefaultColumnHeaders.Size, r => r.size.ToString(), new DoubleMinMax<FillTuple>(r => r.fill.LastShares), DataGridFilterKnownTemplates.UIntMinMax, DefaultColumnWidths.Size);
|
||||
add_col(Columns.Size, DefaultColumnHeaders.Size, r => r.display_size.ToString(), new DoubleMinMax<FillTuple>(r => r.fill.LastShares), DataGridFilterKnownTemplates.UIntMinMax, DefaultColumnWidths.Size);
|
||||
add_col(Columns.Price, DefaultColumnHeaders.Price, r => r.format_price(r.price), new DoubleMinMax<FillTuple>(r => r.fill.LastPrice), DataGridFilterKnownTemplates.DoubleMinMax, DefaultColumnWidths.Price);
|
||||
add_col(Columns.FillKind, DefaultColumnHeaders.FillKind, r => r.fill_kind.ToString(), dv_filter(EnumHelper.get_values<FillKind>().ToArray(), r => r.fill.FillKind), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.FillKind);
|
||||
add_col(Columns.FillStatus, DefaultColumnHeaders.FillStatus, r => r.fill_status.ToString(), dv_filter(EnumHelper.get_values<FillStatus>().ToArray(), r => r.fill.FillStatus), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.FillKind);
|
||||
@@ -286,6 +286,7 @@ namespace mbts.spark {
|
||||
Color? _root_algo_badge_color;
|
||||
OrderMetadata _order_metadata;
|
||||
bool _is_selected;
|
||||
double? _display_size;
|
||||
|
||||
public Row(FillTuple fill_tuple, OrdersModel model) :
|
||||
base(fill_tuple.security ?? SecurityDataHelper.Defaults.SecurityImmutable) {
|
||||
@@ -306,6 +307,8 @@ namespace mbts.spark {
|
||||
public Side side => _fill.Side;
|
||||
public double price => _fill.LastPrice;
|
||||
public double size => _fill.LastShares;
|
||||
public double display_size => _display_size ??
|
||||
(_display_size = size.safe_divide(_model.security_auxiliary_model.get_display_quantity_divisor(security))).Value;
|
||||
public FillKind fill_kind => _fill.FillKind;
|
||||
public FillStatus fill_status => _fill.FillStatus;
|
||||
public string fill_key => _fill.FillKey;
|
||||
|
||||
@@ -87,6 +87,15 @@ namespace mbts.spark {
|
||||
public static bool operator !=(LadderColumnSettings x, LadderColumnSettings y) => !(x == y);
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class LadderOrderLimits : IEquatable<LadderOrderLimits> {
|
||||
[ProtoMember(1)]
|
||||
public ToggleInt32Proto max_settlement_cross_ticks { get; set; }
|
||||
|
||||
public bool Equals(LadderOrderLimits other) =>
|
||||
other != null && max_settlement_cross_ticks == other.max_settlement_cross_ticks;
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class HistogramBarAnimation : IEquatable<HistogramBarAnimation> {
|
||||
// this class is 100% mutable (there are literally 0 readonly members),
|
||||
@@ -314,6 +323,8 @@ namespace mbts.spark {
|
||||
// Settings are liable to change in the near future, so an internal IEqualityComparer simply won't do as it risks becoming out of date.
|
||||
[ProtoContract]
|
||||
class LadderSettings : IEquatable<LadderSettings> {
|
||||
// RESERVED ProtoMember indices
|
||||
// 900-999: dj settings
|
||||
const double _default_order_entry_panel_width = 110.0;
|
||||
const double _default_zoom_factor = 1.0;
|
||||
const bool _default_show_warn_on_cross = true;
|
||||
@@ -592,6 +603,9 @@ namespace mbts.spark {
|
||||
[ProtoMember(61), DefaultValue(true)]
|
||||
public bool status_message_visible { get; set; }
|
||||
|
||||
[ProtoMember(900)]
|
||||
public LadderOrderLimits ladder_order_limits { get; set; }
|
||||
|
||||
public void sanitize() =>
|
||||
sanitize(this);
|
||||
|
||||
@@ -697,7 +711,8 @@ namespace mbts.spark {
|
||||
a.enforce_max_clip == b.enforce_max_clip &&
|
||||
a.max_clip == b.max_clip &&
|
||||
a.price_display_format == b.price_display_format &&
|
||||
a.status_message_visible == b.status_message_visible;
|
||||
a.status_message_visible == b.status_message_visible &&
|
||||
a.ladder_order_limits.safe_equals(b.ladder_order_limits);
|
||||
|
||||
public bool Equals(LadderSettings other) =>
|
||||
profile_equals(this, other) &&
|
||||
|
||||
@@ -100,6 +100,7 @@ namespace bts.shell {
|
||||
ShellWindowVM shell_window_vm = null;
|
||||
IShellWindow shell_window = null;
|
||||
var title_source = new LazyObservable<string>(() => ShellWindowExtensions.literal_title_source(shell_window, shell_window_vm));
|
||||
LadderVM vm = null;
|
||||
var view = new Lazy<UIElement>(() => {
|
||||
// resources
|
||||
var model = new LadderModel(md, _orders_model, _mbo);
|
||||
@@ -113,7 +114,6 @@ namespace bts.shell {
|
||||
/// In response to that event, 'needs_resync' is set to true. This causes 'last_value' to be refreshed when the next caller requests the latest settings
|
||||
/// (i.e., an IObserver subscribing to the settings_source, or the settings-dialog viewmodel).
|
||||
bool initialized = false, needs_resync = false;
|
||||
LadderVM vm = null;
|
||||
var settings_subject = new Subject<LadderSettings>();
|
||||
var settings_source = Observable.Create<LadderSettings>(o => {
|
||||
ensure_synced();
|
||||
@@ -272,10 +272,11 @@ namespace bts.shell {
|
||||
(v, s) => save(_get_state(((LadderView)v).vm, settings_id), s),
|
||||
icon: Icons.Ladder,
|
||||
thread_id: _thread_id_provider.next_id(),
|
||||
vm => {
|
||||
shell_window_vm = vm;
|
||||
swvm => {
|
||||
shell_window_vm = swvm;
|
||||
title_source.connect();
|
||||
});
|
||||
},
|
||||
key_event_args => vm?.key_down(key_event_args));
|
||||
}
|
||||
|
||||
async void _show_windows(IImmutableSet<uint> securities) {
|
||||
|
||||
@@ -4,6 +4,7 @@ using bts.shell.orderbook;
|
||||
using mbts.persist;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
@@ -169,7 +170,8 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
private void _set_badge_text() {
|
||||
text_lines = BadgeRender.format(side, quantity, filled, _current_badge_style);
|
||||
var fmt = MDDisplayHelper.get_value_formatter(_owner.display_quantity_divisor);
|
||||
text_lines = BadgeRender.format(side, quantity, filled, _current_badge_style, fmt);
|
||||
RaisePropertyChanged(nameof(text_lines));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.persist.algo;
|
||||
using bts.proto;
|
||||
using bts.shell.orderbook;
|
||||
using bts.utils;
|
||||
using mbts.collections;
|
||||
@@ -44,7 +45,8 @@ namespace mbts.spark {
|
||||
LadderSettings.NetChangeTerms _price_format;
|
||||
LadderSettings.PriceDisplayFormat _price_display_format;
|
||||
double _order_entry_panel_width, _zoom_factor;
|
||||
bool _auto_clear, _show_warn_on_cross, _enforce_max_clip;
|
||||
bool _auto_clear, _show_warn_on_cross, _enforce_max_clip, _enforce_max_settlement_cross_ticks;
|
||||
int _max_settlement_cross_ticks;
|
||||
LadderSettings _current;
|
||||
LadderColumnSettingsVM
|
||||
_orders = new OrdersColumnSettingsVM(),
|
||||
@@ -190,6 +192,8 @@ namespace mbts.spark {
|
||||
public bool show_warn_on_cross { get => _show_warn_on_cross; set => Set(ref _show_warn_on_cross, value); }
|
||||
public bool enforce_max_clip { get => _enforce_max_clip; set => Set(ref _enforce_max_clip, value); }
|
||||
public uint max_clip { get => _max_clip.Value; set => _max_clip.Value = value; }
|
||||
public bool enforce_max_settlement_cross_ticks { get => _enforce_max_settlement_cross_ticks; set => Set(ref _enforce_max_settlement_cross_ticks, value); }
|
||||
public int max_settlement_cross_ticks { get => _max_settlement_cross_ticks; set => Set(ref _max_settlement_cross_ticks, value); }
|
||||
|
||||
// favorites
|
||||
public FavoriteAlgosVM favorite_algos_vm { get; }
|
||||
@@ -370,6 +374,10 @@ namespace mbts.spark {
|
||||
show_warn_on_cross = settings.show_warn_on_cross;
|
||||
enforce_max_clip = settings.enforce_max_clip;
|
||||
max_clip = settings.max_clip;
|
||||
if (settings.ladder_order_limits != null) {
|
||||
enforce_max_settlement_cross_ticks = settings.ladder_order_limits.max_settlement_cross_ticks.is_enabled;
|
||||
max_settlement_cross_ticks = settings.ladder_order_limits.max_settlement_cross_ticks.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,6 +456,10 @@ namespace mbts.spark {
|
||||
clone.show_warn_on_cross = show_warn_on_cross;
|
||||
clone.enforce_max_clip = enforce_max_clip;
|
||||
clone.max_clip = max_clip;
|
||||
if (clone.ladder_order_limits == null)
|
||||
clone.ladder_order_limits = new LadderOrderLimits();
|
||||
clone.ladder_order_limits.max_settlement_cross_ticks =
|
||||
new ToggleInt32Proto(max_settlement_cross_ticks, enforce_max_settlement_cross_ticks);
|
||||
return clone;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.proto;
|
||||
using bts.shell;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
@@ -8,6 +8,7 @@ using mbts.co2;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -66,6 +67,8 @@ namespace mbts.spark {
|
||||
current_settings = init;
|
||||
this.settings = settings;
|
||||
_equality = TickUtils.tick_equality_comparer(_model.security.ContractSpec);
|
||||
display_quantity_divisor = model.order_gateway.security_auxiliary_model.get_display_quantity_divisor(model.security);
|
||||
size_calculator.display_divisor = display_quantity_divisor;
|
||||
_slot_price_vs_orders = new Dictionary<double, LadderOrderVM>(_equality);
|
||||
this.order_properties_vm = order_properties_vm;
|
||||
_high = backing(nameof(high), double.NaN);
|
||||
@@ -257,7 +260,11 @@ namespace mbts.spark {
|
||||
|
||||
public IBus bus => _bus;
|
||||
|
||||
public event Action on_tick, request_settings_dialog, settings_invalidated;
|
||||
public event Action
|
||||
on_tick,
|
||||
request_settings_dialog,
|
||||
request_recenter,
|
||||
settings_invalidated;
|
||||
|
||||
public MDConsumerHandle md => _model.md;
|
||||
// we return only entries who have piqs and whose piqs are not uint.Max
|
||||
@@ -292,6 +299,7 @@ namespace mbts.spark {
|
||||
remove => _on_disposed_impl -= value;
|
||||
}
|
||||
public SecurityImmutable security => _model.security;
|
||||
public uint display_quantity_divisor { get; }
|
||||
public LadderSettings current_settings { get; private set; }
|
||||
public double high => _high.Value;
|
||||
public double low => _low.Value;
|
||||
@@ -369,14 +377,14 @@ namespace mbts.spark {
|
||||
get {
|
||||
string rv = null;
|
||||
if (show_day_position)
|
||||
rv = SparkUtils.fmt_position(day_position);
|
||||
rv = SparkUtils.fmt_position(day_position / display_quantity_divisor);
|
||||
if (show_raw_day_position) {
|
||||
var raw_day_text = SparkUtils.fmt_position(raw_day_position);
|
||||
var raw_day_text = SparkUtils.fmt_position(raw_day_position / display_quantity_divisor);
|
||||
rv = rv == null ? raw_day_text : $"{rv} / {raw_day_text}";
|
||||
}
|
||||
if (show_account_position) {
|
||||
var acc_text = SparkUtils.fmt_position(position);
|
||||
rv = rv == null ? SparkUtils.fmt_position(position) : $"{rv} ({acc_text})";
|
||||
var acc_text = SparkUtils.fmt_position(position / display_quantity_divisor);
|
||||
rv = rv == null ? acc_text : $"{rv} ({acc_text})";
|
||||
}
|
||||
return rv ?? "";
|
||||
}
|
||||
@@ -444,15 +452,27 @@ namespace mbts.spark {
|
||||
public void key_down(KeyEventArgs e) {
|
||||
if (order_properties_vm == null)
|
||||
return;
|
||||
if (KeyboardExtensions.IsDown(Key.Space)) {
|
||||
_bus?.post(SparkTags.RequestRecenterLadder, null);
|
||||
}
|
||||
// the ctrl modifier requirement helps avoid keyboard-input collisions w/ algo-templates popup
|
||||
if (!KeyboardExtensions.IsCtrlDown()) return;
|
||||
var new_algo = order_properties_vm.algo_definitions.FirstOrDefault(o => o.algo_root.key_match(e));
|
||||
if (new_algo != null) {
|
||||
order_properties_vm.selected_algo_definition = new_algo;
|
||||
e.Handled = true;
|
||||
switch (e.Key) {
|
||||
case Key.Space:
|
||||
if (KeyboardExtensions.IsShiftDown()) {
|
||||
// only recenter this ladder
|
||||
request_recenter?.Invoke();
|
||||
} else {
|
||||
// recenter all ladders
|
||||
_bus?.post(SparkTags.RequestRecenterLadder, null);
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
default:
|
||||
// the ctrl modifier requirement helps avoid keyboard-input collisions w/ algo-templates popup
|
||||
if (KeyboardExtensions.IsCtrlDown()) {
|
||||
var new_algo = order_properties_vm.algo_definitions.FirstOrDefault(o => o.algo_root.key_match(e));
|
||||
if (new_algo != null) {
|
||||
order_properties_vm.selected_algo_definition = new_algo;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,6 +506,10 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public async Task send_order_async(Side side, uint size, double price) {
|
||||
if (!_validate_price(side, price, out var invalid_price_msg)) {
|
||||
status_update(new Activity { security = security, message = invalid_price_msg, category = ActivityLogCategory.order });
|
||||
return;
|
||||
}
|
||||
var order = order_properties_vm.create(side);
|
||||
if (order.CustomDef != null) {
|
||||
if (order.CustomDef.Stop != null) {
|
||||
@@ -517,6 +541,23 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
bool _validate_price(Side side, double price, out string msg) {
|
||||
msg = null;
|
||||
if (!price.is_real()) {
|
||||
msg = "not a real number";
|
||||
} else if (settlement.is_real()) {
|
||||
var ticks_from_settle = security.ContractSpec.tickdiff(price, settlement);
|
||||
if (ticks_from_settle.HasValue) {
|
||||
var max_cross = (current_settings?.ladder_order_limits?.max_settlement_cross_ticks)?.as_nullable();
|
||||
if (max_cross.HasValue) {
|
||||
if (side == Side.Sell ? ticks_from_settle < (-1 * max_cross.Value) : ticks_from_settle > max_cross.Value)
|
||||
msg = $"exceeds settle cross ticks limit ({ticks_from_settle.Value})";
|
||||
}
|
||||
}
|
||||
}
|
||||
return msg == null;
|
||||
}
|
||||
|
||||
void _update_header() {
|
||||
bool last_updated = md.get_last_trade(ref _last_trade_time, out NativePriceSize last, out DateTime time, out ulong volume);
|
||||
if (_misc_time != _model.md.misc_timestamp || last_updated) {
|
||||
|
||||
@@ -46,12 +46,12 @@ namespace mbts.spark {
|
||||
void _reset_ladder_vm() {
|
||||
if (_value == null && !_security_md.HasValue)
|
||||
return;
|
||||
Disposable.replace(ref _value,
|
||||
_security_md.HasValue ?
|
||||
LadderVM.create_readonly(_bus,
|
||||
new LadderModel(security_md.Value, _model, _model.mbo_service,
|
||||
new Func<OrderSecurity, bool>(osec => _selected_order_tree.Contains(osec.order.Key))),
|
||||
Observable.Return(LadderSettings.sanitize(LadderSettings.Default))) : null);
|
||||
_value?.Dispose();
|
||||
_value = _security_md.HasValue ?
|
||||
LadderVM.create_readonly(_bus,
|
||||
new LadderModel(security_md.Value, _model, _model.mbo_service,
|
||||
new Func<OrderSecurity, bool>(osec => _selected_order_tree.Contains(osec.order.Key))),
|
||||
Observable.Return(LadderSettings.sanitize(LadderSettings.Default))) : null;
|
||||
RaisePropertyChanged(nameof(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace mbts.spark {
|
||||
readonly RefValueContainer<IEnumerable<VolumeAtPriceLevelImmutable>> _volume_container =
|
||||
new RefValueContainer<IEnumerable<VolumeAtPriceLevelImmutable>>();
|
||||
|
||||
IDisposable _settings_sub, _volume_sub, _tick_sub;
|
||||
IDisposable _settings_sub, _volume_sub, _tick_sub, _vm_sub;
|
||||
ScrollViewer _scrollViewer;
|
||||
LadderSettings _settings;
|
||||
LadderVM _vm;
|
||||
@@ -66,7 +66,8 @@ namespace mbts.spark {
|
||||
double _last_origin_price;
|
||||
MarketBreakLines _mkt_break_lines;
|
||||
NativePriceSize _last_trade;
|
||||
Func<NativePriceSize, string> _fmt_size = MDDisplayHelper.get_formatter_nps(MarketSizeDisplay.get_default());
|
||||
Func<NativePriceSize, string> _fmt_mkt_depth_item_impl = MDDisplayHelper.get_formatter_nps(MarketSizeDisplay.get_default());
|
||||
Func<uint, string> _fmt_mkt_quantity_impl = v => v.ToString();
|
||||
int _old_tooltip_row = int.MaxValue;
|
||||
NativePriceSize? _old_last_trade;
|
||||
string _direction_arrow = string.Empty;
|
||||
@@ -468,7 +469,7 @@ namespace mbts.spark {
|
||||
security_id: _vm.model.security.Id,
|
||||
price_level: price_for_point(point)
|
||||
);
|
||||
var model = new MBOPopupModel(_vm.model.mbo_service, _vm.model.security, query);
|
||||
var model = new MBOPopupModel(_vm.model.mbo_service, _vm.model.security, query, fmt_mbo_qty);
|
||||
// Load the first set of orders so we know if there are any before showing the popup
|
||||
var result = await model.load_orders();
|
||||
if (result.success && result.value.orders.Count > 0) {
|
||||
@@ -489,6 +490,8 @@ namespace mbts.spark {
|
||||
};
|
||||
view.RequestClose += (o, _) => popup.IsOpen = false;
|
||||
}
|
||||
string fmt_mbo_qty(int value) =>
|
||||
value < 0 ? $"-{_fmt_mkt_quantity((uint)value)}" : _fmt_mkt_quantity((uint)value);
|
||||
}
|
||||
|
||||
protected override void OnMouseDoubleClick(MouseButtonEventArgs e) {
|
||||
@@ -534,14 +537,44 @@ namespace mbts.spark {
|
||||
_size_from_row(row, _price_list, _asks, _vm.security.ContractSpec);
|
||||
|
||||
uint _volume_from_row(int row) {
|
||||
if (_price_list != null) {
|
||||
var price = _price_list[row];
|
||||
var vapls = _volume_container.read_new_value();
|
||||
if (vapls != null) {
|
||||
return vapls.SelectFirstOrDefault(v => MathUtils.epsilon_equals(v.Price, price), v => v.Volume);
|
||||
}
|
||||
const int max_tries = 3;
|
||||
var errors = new List<string>(0);
|
||||
int num_tries = 0;
|
||||
for (; num_tries < max_tries; num_tries++) {
|
||||
try {
|
||||
var rv = impl();
|
||||
if (num_tries > 0) {
|
||||
log_errors(true);
|
||||
}
|
||||
return rv;
|
||||
} catch (Exception ex) {
|
||||
errors.Add(ex.ToString());
|
||||
}
|
||||
}
|
||||
log_errors(false);
|
||||
return 0;
|
||||
|
||||
void log_errors(bool succeeded) {
|
||||
// Only log distinct errors to avoid useless noise.
|
||||
// It's likely that the exception will always be the same (IndexOutOfRange),
|
||||
// however, this bug is not fully understood, so assume nothing.
|
||||
var error_msg = string.Join("; ", errors.Distinct());
|
||||
if (succeeded) {
|
||||
Log.Ui.error($"Ladder '{security}' got volume for row at index '{row}' after {num_tries + 1} attempts (price_list array length is {_price_list.Length}). The following exceptions were observed: {error_msg}");
|
||||
} else {
|
||||
Log.Ui.error($"Ladder '{security}' failed to get volume for row at index '{row}' after {max_tries} attempts (price_list array length is {_price_list.Length}): {error_msg}");
|
||||
}
|
||||
}
|
||||
|
||||
uint impl() {
|
||||
if (_price_list.SafeTryGetAt(row, out var price)) {
|
||||
var vapls = _volume_container.read_new_value();
|
||||
if (vapls != null) {
|
||||
return vapls.SelectFirstOrDefault(v => MathUtils.epsilon_equals(v.Price, price), v => v.Volume);
|
||||
}
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
|
||||
@@ -626,7 +659,7 @@ namespace mbts.spark {
|
||||
}
|
||||
void render_market_col(NativePriceSize[] depth) {
|
||||
var mid = LadderRender.index_of(_price_list, _last_origin_price, _price_comparer);
|
||||
LadderRender.render_sizes(column.visual_tup.visual, column.visual_tup.glyph_context, mid, _price_list, depth, _settings.row_height, security.ContractSpec, column.settings, _fmt_size);
|
||||
LadderRender.render_sizes(column.visual_tup.visual, column.visual_tup.glyph_context, mid, _price_list, depth, _settings.row_height, security.ContractSpec, column.settings, _fmt_mkt_depth_item);
|
||||
}
|
||||
}
|
||||
_apply_column_visual_offset();
|
||||
@@ -664,10 +697,22 @@ namespace mbts.spark {
|
||||
_price_comparer = Comparer<double>.Create((x, y) => security.ContractSpec.tick_equals(x, y) ? 0 : y.CompareTo(x));
|
||||
_price_dirty = true;
|
||||
_piq_dirty = true;
|
||||
_vm.on_tick += _tick;
|
||||
// **use local vm to protect against re-assignment,
|
||||
// so the events are unhooked from the same instance to which they were added
|
||||
// when the subs are disposed.
|
||||
var local_vm = _vm;
|
||||
local_vm.on_tick += _tick;
|
||||
local_vm.request_recenter += on_request_recenter;
|
||||
_settings_sub = _vm.settings.OnDispatcher().Subscribe(_settings_updated);
|
||||
_volume_sub = _vm.model.order_gateway.md_auxiliary_model.security_volume_at_price(security.Id).Subscribe(t => _volume_container.write_value(t.state.Values));
|
||||
_tick_sub = Disposable.Create(() => _vm.on_tick -= _tick);
|
||||
// **unhook from local_vm.request_recenter.
|
||||
// todo: organize these subs into a single entity, prob _vm_subs.
|
||||
// This hasn't already been done bc the ladder a bit fragile, so changes must be carefully implemented,
|
||||
// with meticulous attention to detail.
|
||||
_tick_sub = Disposable.Create(() => local_vm.on_tick -= _tick);
|
||||
_vm_sub = Disposable.Create(() => local_vm.request_recenter -= on_request_recenter);
|
||||
void on_request_recenter() =>
|
||||
RecenterCommand?.ExecuteIfCan(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,7 +749,7 @@ namespace mbts.spark {
|
||||
_hide_tool_tip();
|
||||
}
|
||||
|
||||
tooltip_view.Content = $"{volume}";
|
||||
tooltip_view.Content = _fmt_mkt_quantity(volume);
|
||||
tooltip_view.IsOpen = true;
|
||||
this.ToolTip = tooltip_view;
|
||||
} else {
|
||||
@@ -820,7 +865,7 @@ namespace mbts.spark {
|
||||
if (last_trade_changed) {
|
||||
var last_index = LadderRender.index_of(_price_list, _last_trade.price, _price_comparer);
|
||||
if (last_index > 0)
|
||||
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column);
|
||||
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column, _fmt_mkt_quantity);
|
||||
}
|
||||
if (depth_changed)
|
||||
_refresh_sizes();
|
||||
@@ -862,21 +907,21 @@ namespace mbts.spark {
|
||||
void _refresh_last_trade() {
|
||||
var last_index = LadderRender.index_of(_price_list, _last_trade.price, _price_comparer);
|
||||
if (last_index > 0)
|
||||
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column);
|
||||
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column, _fmt_mkt_quantity);
|
||||
}
|
||||
|
||||
void _refresh_piq() {
|
||||
if (!_settings.piq_column.show)
|
||||
return;
|
||||
int origin_index = LadderRender.index_of(_price_list, _mkt_break_lines.origin, _price_comparer);
|
||||
LadderRender.render_piqs(_piq_vis_tup.visual, _last_vis_tup.glyph_context, origin_index, _price_list, _vm.price_vs_piq, _settings.row_height, security.ContractSpec, _settings.piq_column);
|
||||
LadderRender.render_piqs(_piq_vis_tup.visual, _last_vis_tup.glyph_context, origin_index, _price_list, _vm.price_vs_piq, _settings.row_height, security.ContractSpec, _settings.piq_column, _fmt_mkt_quantity);
|
||||
}
|
||||
|
||||
void _refresh_sizes() {
|
||||
Log.assert_d(_price_list != null);
|
||||
int origin_index = LadderRender.index_of(_price_list, _mkt_break_lines.origin, _price_comparer);
|
||||
LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _bids, _settings.row_height, security.ContractSpec, _settings.bid_column, _fmt_size);
|
||||
LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _asks, _settings.row_height, security.ContractSpec, _settings.ask_column, _fmt_size);
|
||||
LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _bids, _settings.row_height, security.ContractSpec, _settings.bid_column, _fmt_mkt_depth_item);
|
||||
LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _asks, _settings.row_height, security.ContractSpec, _settings.ask_column, _fmt_mkt_depth_item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -898,7 +943,17 @@ namespace mbts.spark {
|
||||
if (vapls != null) {
|
||||
Log.assert_d(_price_list != null);
|
||||
var last_index = LadderRender.index_of(_price_list, _last_trade.price, _price_comparer);
|
||||
LadderRender.render_volumes(_volume_vis_tup.visual, _volume_vis_tup.glyph_context, _price_list, vapls, _settings.row_height, security.ContractSpec, _settings.volume_column, last_index, _get_last_arrow_string());
|
||||
LadderRender.render_volumes(
|
||||
_volume_vis_tup.visual,
|
||||
_volume_vis_tup.glyph_context,
|
||||
_price_list,
|
||||
vapls,
|
||||
_settings.row_height,
|
||||
security.ContractSpec,
|
||||
_settings.volume_column,
|
||||
last_index,
|
||||
_get_last_arrow_string(),
|
||||
_fmt_mkt_quantity);
|
||||
_old_last_trade = _last_trade;
|
||||
}
|
||||
|
||||
@@ -921,7 +976,7 @@ namespace mbts.spark {
|
||||
}
|
||||
// regardless of if we've arrived here via a new Last (and new direction arrow)
|
||||
// or we're reusing the old one, this format bit is the same
|
||||
return $"{_direction_arrow} {_last_trade.size} ";
|
||||
return $"{_direction_arrow} {_fmt_mkt_quantity(_last_trade.size)} ";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1026,7 +1081,7 @@ namespace mbts.spark {
|
||||
double best_bid = _get_top_price(_bids);
|
||||
double best_ask = _get_top_price(_asks);
|
||||
return double.IsNaN(best_bid) ?
|
||||
double.IsNaN(best_ask) ? _vm.low : security.ContractSpec.tickdown(best_ask) :
|
||||
double.IsNaN(best_ask) ? _vm.settlement : security.ContractSpec.tickdown(best_ask) :
|
||||
best_bid;
|
||||
}
|
||||
|
||||
@@ -1034,7 +1089,7 @@ namespace mbts.spark {
|
||||
double best_bid = _get_top_price(_bids);
|
||||
double best_ask = _get_top_price(_asks);
|
||||
return double.IsNaN(best_ask) ?
|
||||
double.IsNaN(best_bid) ? security.ContractSpec.tickdown(_vm.high) : best_bid :
|
||||
double.IsNaN(best_bid) ? _vm.settlement : best_bid :
|
||||
security.ContractSpec.tickdown(best_ask);
|
||||
}
|
||||
|
||||
@@ -1042,7 +1097,7 @@ namespace mbts.spark {
|
||||
double best_bid = _get_top_price(_bids);
|
||||
double best_ask = _get_top_price(_asks);
|
||||
return double.IsNaN(best_bid) ?
|
||||
double.IsNaN(best_ask) ? 0.5 * (_vm.low + _vm.high) : security.ContractSpec.tickdown(best_ask) :
|
||||
double.IsNaN(best_ask) ? _vm.settlement : security.ContractSpec.tickdown(best_ask) :
|
||||
double.IsNaN(best_ask) ? best_bid : TickUtils.mid_tick(security.ContractSpec, best_bid, best_ask);
|
||||
}
|
||||
|
||||
@@ -1079,7 +1134,9 @@ namespace mbts.spark {
|
||||
GlyphContext glyph_context(LadderColumnSettings cs) =>
|
||||
GlyphContext.create(cs.font_family, cs.font_weight);
|
||||
|
||||
_fmt_size = MDDisplayHelper.get_formatter_nps(_settings.size_display);
|
||||
var mkt_qty_display_divisor = _vm?.display_quantity_divisor ?? 1;
|
||||
_fmt_mkt_depth_item_impl = MDDisplayHelper.get_formatter_nps(_settings.size_display, mkt_qty_display_divisor);
|
||||
_fmt_mkt_quantity_impl = MDDisplayHelper.get_value_formatter(mkt_qty_display_divisor);
|
||||
_set_columns();
|
||||
_apply_column_visual_offset();
|
||||
if (prev_row_height != _settings.row_height)
|
||||
@@ -1128,6 +1185,22 @@ namespace mbts.spark {
|
||||
}
|
||||
visible_columns_changed?.Invoke();
|
||||
}
|
||||
|
||||
// This is designed to be a redundant null check for _fmt_mkt_depth_item_impl.
|
||||
string _fmt_mkt_depth_item(NativePriceSize value) {
|
||||
// Assign the impl delegate to a local variable, just in case it is reassigned on another thread.
|
||||
// This shouldn't happen, but better to avoid the potential null ref just in case it does. Expect the unexpected.
|
||||
var impl = _fmt_mkt_depth_item_impl;
|
||||
return impl is null ? MDDisplayHelper.get_formatter_nps(MarketSizeDisplay.get_default())?.ToString() ?? "Error" : impl(value);
|
||||
}
|
||||
|
||||
// This is designed to be a redundant null check for _fmt_mkt_quantity_impl.
|
||||
string _fmt_mkt_quantity(uint value) {
|
||||
// Assign the impl delegate to a local variable, just in case it is reassigned on another thread.
|
||||
// This shouldn't happen, but better to avoid the potential null ref just in case it does. Expect the unexpected.
|
||||
var impl = _fmt_mkt_quantity_impl;
|
||||
return impl is null ? value.ToString() : impl(value);
|
||||
}
|
||||
|
||||
void _init_tactile_adorner() {
|
||||
Disposable.dispose(ref _tactile_adorner);
|
||||
|
||||
@@ -12,20 +12,24 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using bts.utils;
|
||||
|
||||
namespace mbts.spark {
|
||||
static class BadgeRender {
|
||||
// IMPORTANT: the return values for this method MUST BE IN SYNC with the "format" method, else "measure_height" and "LadderRender.measure_row_height" will be broken.
|
||||
static int _line_count_for_style(LadderSettings.BadgeStyle badge_style) => badge_style == LadderSettings.BadgeStyle.Alternate ? 2 : 1;
|
||||
// IMPORTANT: at the risk of being redundant, see above comment.
|
||||
public static IReadOnlyList<string> format(Side side, uint quantity, uint filled, LadderSettings.BadgeStyle style) {
|
||||
public static IReadOnlyList<string> format(Side side, uint quantity, uint filled, LadderSettings.BadgeStyle style, Func<uint, string> fmt_value = null) {
|
||||
if (fmt_value is null)
|
||||
fmt_value = FormattingDelegates.default_string<uint>();
|
||||
switch (style) {
|
||||
case LadderSettings.BadgeStyle.Alternate:
|
||||
var working_qty = quantity > filled ? quantity - filled : 0;
|
||||
return new[] {
|
||||
$"{side.abbreviate()}:{filled}",
|
||||
$"W:{quantity - filled}"
|
||||
$"{side.abbreviate()}:{fmt_value(filled)}",
|
||||
$"W:{fmt_value(working_qty)}"
|
||||
};
|
||||
default: return new[] { $"{filled}/{quantity}" };
|
||||
default: return new[] { $"{fmt_value(filled)}/{fmt_value(quantity)}" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,16 +197,17 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public static void render_volumes(
|
||||
DrawingVisual dv,
|
||||
GlyphContext glyph_context,
|
||||
double[] prices,
|
||||
IEnumerable<VolumeAtPriceLevelImmutable> data,
|
||||
double row_height,
|
||||
ContractSpecificationImmutable spec,
|
||||
VolumeColumnSettings settings,
|
||||
int last_index,
|
||||
string last_size_and_direction
|
||||
) {
|
||||
DrawingVisual dv,
|
||||
GlyphContext glyph_context,
|
||||
double[] prices,
|
||||
IEnumerable<VolumeAtPriceLevelImmutable> data,
|
||||
double row_height,
|
||||
ContractSpecificationImmutable spec,
|
||||
VolumeColumnSettings settings,
|
||||
int last_index,
|
||||
string last_size_and_direction,
|
||||
Func<uint, string> fmt_value) {
|
||||
//
|
||||
if (!data.Any()) {
|
||||
_clear_visual(dv);
|
||||
return;
|
||||
@@ -224,6 +229,8 @@ namespace mbts.spark {
|
||||
_clear_visual(dv);
|
||||
return;
|
||||
}
|
||||
if (fmt_value is null)
|
||||
fmt_value = FormattingDelegates.default_string<uint>();
|
||||
// inflate the max so that the graph never fills the available space
|
||||
double max = data.Max(v => v.Volume) * 1.1;
|
||||
_render_cells(dv, row_height, data.Select(d => (d.Price, d)), settings.general, prices, spec, render_cell);
|
||||
@@ -255,11 +262,11 @@ namespace mbts.spark {
|
||||
}
|
||||
x_bar += width;
|
||||
if (show_numbers)
|
||||
numbers_text.Add(v.ToString());
|
||||
numbers_text.Add(fmt_value(v));
|
||||
}
|
||||
|
||||
if (settings.total.show_numbers) {
|
||||
numbers_text.Add(vap.Volume.ToString());
|
||||
numbers_text.Add(fmt_value(vap.Volume));
|
||||
}
|
||||
if (settings.show_ltq && last_trade_here) {
|
||||
numbers_text.Clear();
|
||||
@@ -309,19 +316,33 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
internal static void render_last(DrawingVisual dv, GlyphContext glyph_context, NativePriceSize last, int index, double row_height, LadderColumnSettings settings) {
|
||||
internal static void render_last(DrawingVisual dv, GlyphContext glyph_context, NativePriceSize last, int index, double row_height, LadderColumnSettings settings, Func<uint, string> fmt_value) {
|
||||
if (fmt_value is null)
|
||||
fmt_value = FormattingDelegates.default_string<uint>();
|
||||
using (var dc = dv.RenderOpen()) {
|
||||
var gr = format_text(last.size.ToString(), index, row_height, settings, glyph_context);
|
||||
var gr = format_text(fmt_value(last.size), index, row_height, settings, glyph_context);
|
||||
dc.DrawGlyphRun(settings.foreground.rgb_brush(), gr);
|
||||
}
|
||||
}
|
||||
|
||||
// this is basically a copy of redner_sizes up above
|
||||
internal static void render_piqs(DrawingVisual dv, GlyphContext glyph_context, int origin_index, double[] prices, IEnumerable<(double price, uint piq)> pvp, double row_height, ContractSpecificationImmutable spec, LadderColumnSettings settings) {
|
||||
internal static void render_piqs(
|
||||
DrawingVisual dv,
|
||||
GlyphContext glyph_context,
|
||||
int origin_index,
|
||||
double[] prices,
|
||||
IEnumerable<(double price, uint piq)> pvp,
|
||||
double row_height,
|
||||
ContractSpecificationImmutable spec,
|
||||
LadderColumnSettings settings,
|
||||
Func<uint, string> fmt_value) {
|
||||
//
|
||||
using (var dc = dv.RenderOpen()) {
|
||||
using (_draw_with_clip(dc, settings, row_height, prices.Length)) {
|
||||
if (!prices.within_range(origin_index) || pvp.is_null_or_empty())
|
||||
return;
|
||||
if (fmt_value is null)
|
||||
fmt_value = FormattingDelegates.default_string<uint>();
|
||||
var foreground = settings.foreground.rgb_brush();
|
||||
int curr_index = origin_index;
|
||||
double curr_price = prices[curr_index];
|
||||
@@ -332,7 +353,7 @@ namespace mbts.spark {
|
||||
curr_index += tick_diff;
|
||||
curr_price = price;
|
||||
if (curr_index >= 0 && curr_index < prices.Length) {
|
||||
var gr = format_text(entry.piq.ToString(), curr_index, row_height, settings, glyph_context);
|
||||
var gr = format_text(fmt_value(entry.piq), curr_index, row_height, settings, glyph_context);
|
||||
dc.DrawGlyphRun(foreground, gr);
|
||||
} else break;
|
||||
}
|
||||
@@ -371,4 +392,4 @@ namespace mbts.spark {
|
||||
using (dv.RenderOpen()) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1019,45 +1019,47 @@
|
||||
<Grid HorizontalAlignment="Left" Margin="6">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush1}}"/>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush1}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="50"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- market cross -->
|
||||
<CheckBox Name="warn" Grid.Column="0" FlowDirection="RightToLeft" IsChecked="{Binding show_warn_on_cross}">
|
||||
<CheckBox.Resources>
|
||||
<Style TargetType="{x:Type Path}">
|
||||
<Setter Property="FlowDirection" Value="LeftToRight"/>
|
||||
</Style>
|
||||
</CheckBox.Resources>
|
||||
<CheckBox Grid.Column="0" VerticalContentAlignment="Center" IsChecked="{Binding show_warn_on_cross}">
|
||||
<TextBlock
|
||||
Style="{StaticResource EnableDisableFeatureText}"
|
||||
IsEnabled="{Binding show_warn_on_cross}"
|
||||
Text="Warn on Market Cross"
|
||||
/>
|
||||
</CheckBox>
|
||||
<TextBlock Grid.Column="1"
|
||||
Style="{StaticResource EnableDisableFeatureText}"
|
||||
IsEnabled="{Binding ElementName=warn, Path=IsChecked}"
|
||||
Text="Warn on Market Cross" />
|
||||
<!-- max order qty -->
|
||||
<CheckBox Name="enforce_max_clip" Grid.Row="1" Grid.Column="0" FlowDirection="RightToLeft" IsChecked="{Binding enforce_max_clip}">
|
||||
<CheckBox.Resources>
|
||||
<Style TargetType="{x:Type Path}">
|
||||
<Setter Property="FlowDirection" Value="LeftToRight"/>
|
||||
</Style>
|
||||
</CheckBox.Resources>
|
||||
<CheckBox Grid.Row="2" Grid.Column="0" VerticalContentAlignment="Center" IsChecked="{Binding enforce_max_clip}">
|
||||
<TextBlock
|
||||
Style="{StaticResource EnableDisableFeatureText}"
|
||||
IsEnabled="{Binding enforce_max_clip}"
|
||||
Text="Max Order Quantity"
|
||||
/>
|
||||
</CheckBox>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1"
|
||||
Style="{StaticResource EnableDisableFeatureText}"
|
||||
IsEnabled="{Binding ElementName=enforce_max_clip, Path=IsChecked}"
|
||||
MinWidth="{Binding ElementName=warn, Path=ActualWidth}"
|
||||
Text="Max Order Quantity"/>
|
||||
<c:TickUpDownUInt Grid.Row="1" Grid.Column="2"
|
||||
IsEnabled="{Binding ElementName=enforce_max_clip, Path=IsChecked}"
|
||||
Value="{Binding max_clip}" />
|
||||
<c:TickUpDownUInt
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
IsEnabled="{Binding enforce_max_clip}"
|
||||
Value="{Binding max_clip}"
|
||||
Width="60"
|
||||
/>
|
||||
<CheckBox Grid.Row="4" VerticalContentAlignment="Center" IsChecked="{Binding enforce_max_settlement_cross_ticks}">
|
||||
<TextBlock
|
||||
Style="{StaticResource EnableDisableFeatureText}"
|
||||
IsEnabled="{Binding enforce_max_settlement_cross_ticks}"
|
||||
Text="Max Ticks from Settlement"
|
||||
/>
|
||||
</CheckBox>
|
||||
<c:TickUpDownInt Grid.Row="4" Grid.Column="2" IsEnabled="{Binding enforce_max_settlement_cross_ticks}" Value="{Binding max_settlement_cross_ticks}"/>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="Mini Order Book">
|
||||
|
||||
@@ -426,9 +426,26 @@
|
||||
Text="{Binding left_panel_width, Converter={StaticResource VolumeTextConverter}}"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_volume, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
<TextBlock Grid.Column="2" Grid.Row="5" Text="{Binding volume, Mode=OneWay}" FontWeight="Bold"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_volume, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Grid.Row="5"
|
||||
FontWeight="Bold"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_volume, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding>
|
||||
<MultiBinding.Converter>
|
||||
<converters:MultiValueConverterPipe>
|
||||
<x:Static MemberType="{x:Type converters:DynamicDoubleDivisorConverter}" Member="Instance"/>
|
||||
<x:Static MemberType="{x:Type converters:ValueConverters}" Member="SimpleString"/>
|
||||
</converters:MultiValueConverterPipe>
|
||||
</MultiBinding.Converter>
|
||||
<Binding Path="volume" Mode="OneWay"/>
|
||||
<Binding Path="display_quantity_divisor" Mode="OneTime"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<!-- Account combobox -->
|
||||
<ComboBox DataContext="{Binding order_properties_vm}" Margin="2,10,2,2"
|
||||
@@ -507,7 +524,7 @@
|
||||
mbts:TextBoxBehavior.IncrementDecrementOnMouseClick="True"
|
||||
HorizontalContentAlignment="Right"
|
||||
Margin="2"
|
||||
Text="{Binding size_calculator.value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Text="{Binding size_calculator.display_value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
ContextMenu="{x:Null}"
|
||||
/>
|
||||
<!-- Qty quick buttons -->
|
||||
@@ -522,7 +539,14 @@
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Button Margin="2" Command="{Binding Path=DataContext.size_calculator.click, RelativeSource={RelativeSource AncestorType=ItemsControl}}" CommandParameter="{Binding}" Content="{Binding}" Focusable="False"/>
|
||||
<Button Margin="2" Command="{Binding Path=DataContext.size_calculator.click, RelativeSource={RelativeSource AncestorType=ItemsControl}}" Content="{Binding}" Focusable="False">
|
||||
<Button.CommandParameter>
|
||||
<MultiBinding Converter="{x:Static converters:DynamicDoubleMultiplierConverter.Instance}">
|
||||
<Binding/>
|
||||
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=spark:LadderView}"/>
|
||||
</MultiBinding>
|
||||
</Button.CommandParameter>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
@@ -93,6 +93,7 @@ namespace mbts.spark {
|
||||
_ui_scaler.MouseDoubleClick += _ui_scaler_MouseDoubleClick;
|
||||
this.PreviewMouseWheel += LadderView_PreviewMouseWheel;
|
||||
this.PreviewMouseDown += LadderView_PreviewMouseDown;
|
||||
PreviewKeyDown += (s, e) => vm.key_down(e);
|
||||
}
|
||||
|
||||
IDisposable _connect_left_panel_scroll() {
|
||||
|
||||
@@ -3,6 +3,7 @@ using bts.api.position;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -25,24 +26,27 @@ namespace bts.shell.manual_fills {
|
||||
readonly Property<bool> _is_dirty;
|
||||
readonly Property<EditableItemState> _editable_item_state;
|
||||
readonly DelegateProperty<bool> _is_valid;
|
||||
readonly DisplayQuantityMultiplier _size = new DisplayQuantityMultiplier();
|
||||
readonly Func<SecurityImmutable?, uint> _get_display_qty_divisor;
|
||||
SecurityImmutable? _security;
|
||||
AccountTuple? _account;
|
||||
UserTuple? _user;
|
||||
Side _side;
|
||||
uint _size;
|
||||
double _price;
|
||||
bool _is_selected;
|
||||
ManualFillData? _data;
|
||||
IDisposable _committed_snapshot;
|
||||
|
||||
public ManualFillRow(ManualFillData data) {
|
||||
public ManualFillRow(ManualFillData data, Func<SecurityImmutable?, uint> get_display_qty_divisor) {
|
||||
_data = data;
|
||||
_get_display_qty_divisor = get_display_qty_divisor ?? new Func<SecurityImmutable?, uint>(sec => 1);
|
||||
key = data.key;
|
||||
_security = data.value.security;
|
||||
_account = data.value.account;
|
||||
_user = data.value.user;
|
||||
_side = data.value.manual_fill.Side;
|
||||
_size = data.value.manual_fill.Quantity;
|
||||
_size.actual_value = data.value.manual_fill.Quantity;
|
||||
_size.conversion_data = new QuantityConversionData(_get_display_qty_divisor(data.value.security), QuantityConversionData.OpType.DivideActual);
|
||||
_price = data.value.manual_fill.Price;
|
||||
if (placeholder_key.HasValue) {
|
||||
_is_dirty = backing(nameof(is_dirty), false);
|
||||
@@ -53,6 +57,7 @@ namespace bts.shell.manual_fills {
|
||||
_editable_item_state = backing(nameof(editable_item_state), _is_dirty, ch => ch.newval ? EditableItemState.Modified : EditableItemState.None);
|
||||
}
|
||||
_is_valid = backing(nameof(is_valid), () => _validate(this));
|
||||
_dispose = _size.connect_owner(this, nameof(size), nameof(display_size));
|
||||
}
|
||||
|
||||
public event Action<bool> is_selected_changed;
|
||||
@@ -65,8 +70,10 @@ namespace bts.shell.manual_fills {
|
||||
public SecurityImmutable? security {
|
||||
get => _security;
|
||||
set {
|
||||
if (_set(ref _security, value))
|
||||
if (_set(ref _security, value)) {
|
||||
_size.conversion_data = new QuantityConversionData(_get_display_qty_divisor(value), QuantityConversionData.OpType.DivideActual);
|
||||
RaisePropertyChanged(nameof(spec));
|
||||
}
|
||||
}
|
||||
}
|
||||
public ContractSpecificationImmutable? spec => security.spec_or_default();
|
||||
@@ -75,7 +82,8 @@ namespace bts.shell.manual_fills {
|
||||
public UserTuple? user { get => _user; set => _set(ref _user, value); }
|
||||
// side/size/price
|
||||
public Side side { get => _side; set => _set(ref _side, value); }
|
||||
public uint size { get => _size; set => _set(ref _size, value); }
|
||||
public uint size { get => _size.actual_value; set => _size.actual_value = value; }
|
||||
public uint display_size { get => _size.display_value; set => _size.display_value = value; }
|
||||
public double price { get => _price; set => _set(ref _price, value); }
|
||||
// is selected
|
||||
public bool is_selected {
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<ComboBox Grid.Row="2" Grid.Column="2" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Accounts}}" SelectedItem="{Binding account}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="4" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Users}}" SelectedItem="{Binding user}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="6" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Sides}}" SelectedItem="{Binding side}" Width="48"/>
|
||||
<c:TickUpDownUInt Grid.Row="2" Grid.Column="8" MinWidth="40" Value="{Binding size}"/>
|
||||
<c:TickUpDownUInt Grid.Row="2" Grid.Column="8" MinWidth="40" Value="{Binding display_size}"/>
|
||||
<usp:PriceEntryControl Grid.Row="2" Grid.Column="10" MinWidth="55" Value="{Binding price}" ContractSpec="{Binding spec}"/>
|
||||
</Grid>
|
||||
<c:DialogSkirt Grid.Row="1" Margin="0,6,0,0">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using bts.api.core;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.position;
|
||||
using bts.collections;
|
||||
using mbts.spark;
|
||||
@@ -103,5 +104,8 @@ namespace bts.shell.manual_fills {
|
||||
|
||||
public async Task<bool> update_fills(IEnumerable<ManualFill> fills) =>
|
||||
await _model.update_manual_fills(fills);
|
||||
|
||||
public uint get_display_qty_divisor(SecurityImmutable? security) =>
|
||||
_model.security_auxiliary_model.get_display_quantity_divisor(security);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.position;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
@@ -67,7 +68,7 @@ namespace bts.shell.manual_fills {
|
||||
RXCollection.connect(_persisted_rows,
|
||||
_model.manual_fills.OnDispatcher(),
|
||||
Observable.Return(Comparer<ManualFillData>.Create((a, b) => a.key.CompareTo(b.key))),
|
||||
mfd => new ManualFillRow(mfd),
|
||||
mfd => new ManualFillRow(mfd, model.get_display_qty_divisor),
|
||||
mfr => mfr.data),
|
||||
_bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(m => {
|
||||
security_map = new FilteredDictionary<uint, SecurityMD>(m, smd => smd.security.ContractCase == Security.ContractOneofCase.Future);
|
||||
@@ -125,7 +126,8 @@ namespace bts.shell.manual_fills {
|
||||
new ManualFillKey(_placeholder_id_counter++),
|
||||
new ManualFillTuple(new ManualFillImmutable(new ManualFill {
|
||||
Side = Side.Buy
|
||||
}), null, accounts.FirstOrDefault(), users.FirstOrDefault())));
|
||||
}), null, accounts.FirstOrDefault(), users.FirstOrDefault())),
|
||||
_model.get_display_qty_divisor);
|
||||
|
||||
void _add_fill(ManualFillRow new_row) =>
|
||||
_placeholder_rows.InsertSorted((new_row ?? (new_row = row_factory())).placeholder_key.HasValue ?
|
||||
@@ -209,11 +211,20 @@ namespace bts.shell.manual_fills {
|
||||
} else {
|
||||
add_imported_fills();
|
||||
}
|
||||
void add_imported_fills() =>
|
||||
_placeholder_rows.InsertSorted(parsed_fills.Select(pmf => new ManualFillRow(new ManualFillData(new ManualFillKey(_placeholder_id_counter++), new ManualFillTuple(new ManualFillImmutable(pmf.result),
|
||||
pmf.result.SecurityKey != 0 && security_map.TryGetValue(pmf.result.SecurityKey, out var smd) ? smd.security : (SecurityImmutable?)null,
|
||||
accounts.FirstOrDefault(a => a?.id == pmf.result.AccountId),
|
||||
users.FirstOrDefault(a => a?.id == pmf.result.UserId))))), _row_key_comparer);
|
||||
void add_imported_fills() {
|
||||
_placeholder_rows.InsertSorted(parsed_fills.Select(mk_row), _row_key_comparer);
|
||||
|
||||
ManualFillRow mk_row(ParsedManualFill pmf) {
|
||||
var data = new ManualFillData(
|
||||
new ManualFillKey(_placeholder_id_counter++),
|
||||
new ManualFillTuple(
|
||||
new ManualFillImmutable(pmf.result),
|
||||
pmf.result.SecurityKey != 0 && security_map.TryGetValue(pmf.result.SecurityKey, out var smd) ? smd.security : (SecurityImmutable?)null,
|
||||
accounts.FirstOrDefault(a => a?.id == pmf.result.AccountId),
|
||||
users.FirstOrDefault(a => a?.id == pmf.result.UserId)));
|
||||
return new ManualFillRow(data, _model.get_display_qty_divisor);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
BTSMessageBox.Error($"Error importing fills: {ex}.", ManualFillsWindowFactory.window_name);
|
||||
}
|
||||
|
||||
@@ -212,12 +212,12 @@
|
||||
<DataGridTemplateColumn c:DataGridEx.CellKey="{x:Static Size, MemberType={u:Nested usp:ManualFillsView.Columns}}" Header="Qty" SortMemberPath="side">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding size}" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding display_size}" VerticalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
<DataGridTemplateColumn.CellEditingTemplate>
|
||||
<DataTemplate>
|
||||
<c:TickUpDownUInt MinWidth="40" Value="{Binding size, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<c:TickUpDownUInt MinWidth="40" Value="{Binding display_size, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellEditingTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
@@ -597,19 +597,19 @@ namespace mbts.spark {
|
||||
|
||||
public class SizeColumn : OrderbookColumnBase<UIntMinMaxState>, IRenderColumn<OrderBookRowVM> {
|
||||
public SizeColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, UIntMinMaxState> filter)
|
||||
: base(Columns.Size, "Qty", r => r.quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.quantity)) { }
|
||||
: base(Columns.Size, "Qty", r => r.display_quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.quantity)) { }
|
||||
|
||||
public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) =>
|
||||
CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.quantity.ToString(), position, row_height,
|
||||
CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.display_quantity.ToString(), position, row_height,
|
||||
RenderUtils.get_side_brush(row), null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding);
|
||||
}
|
||||
|
||||
public class FilledColumn : OrderbookColumnBase<UIntMinMaxState>, IRenderColumn<OrderBookRowVM> {
|
||||
public FilledColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, UIntMinMaxState> filter)
|
||||
: base(Columns.Filled, "Filled Qty", r => r.filled_quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.filled_quantity)) { }
|
||||
: base(Columns.Filled, "Filled Qty", r => r.display_filled_quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.filled_quantity)) { }
|
||||
|
||||
public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) =>
|
||||
CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.filled_quantity.ToString(), position, row_height,
|
||||
CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.display_filled_quantity.ToString(), position, row_height,
|
||||
RenderUtils.get_side_brush(row), null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding);
|
||||
}
|
||||
|
||||
@@ -1163,7 +1163,7 @@ namespace mbts.spark {
|
||||
|
||||
public override FrameworkElement view_factory() {
|
||||
var tick_up_down = new TickUpDownNullableUIntCommit();
|
||||
tick_up_down.SetBinding(TickUpDownNullableUIntCommit.SourceValueProperty, nameof(OrderBookRowVM.prospective_quantity));
|
||||
tick_up_down.SetBinding(TickUpDownNullableUIntCommit.SourceValueProperty, nameof(OrderBookRowVM.prospective_display_quantity));
|
||||
tick_up_down.SetBinding(TickUpDownNullableUIntCommit.OnCommitProperty, nameof(OrderBookRowVM.modify_order_quantity_andor_price));
|
||||
tick_up_down.InputBindings.Add(_create_keyinput_binding(Key.Enter, nameof(OrderBookRowVM.modify_order_quantity_andor_price)));
|
||||
tick_up_down.InputBindings.Add(_create_keyinput_binding(Key.Escape, nameof(OrderBookRowVM.revert_prospective_order_quantity_andor_price)));
|
||||
|
||||
@@ -29,16 +29,16 @@ namespace mbts.spark {
|
||||
int _depth_level;
|
||||
SecurityImmutable? _security => order_tuple.order.security;
|
||||
// for mini-ob
|
||||
uint _prospective_quantity;
|
||||
DisplayQuantityMultiplier _prospective_qty_translator_backing;
|
||||
uint _display_qty_divisor_backing;
|
||||
bool _display_qty_divisor_dirty = true;
|
||||
double _prospective_price;
|
||||
Action<bool> _on_selection_changed;
|
||||
|
||||
public OrderBookRowVM(OrderBookVM owner, OrderSecurityRootPair order_tuple, bool is_expanded, Action<bool> on_selection_changed) {
|
||||
_owner = owner;
|
||||
this.order_tuple = order_tuple;
|
||||
is_visible = !is_child;
|
||||
_is_expanded = is_expanded;
|
||||
prospective_quantity = this.quantity;
|
||||
prospective_price = this.price;
|
||||
_on_selection_changed = on_selection_changed;
|
||||
}
|
||||
@@ -95,14 +95,18 @@ namespace mbts.spark {
|
||||
public string last_price_text => _fmt_price(last_price);
|
||||
public string avg_price_text => _fmt_price(avg_price);
|
||||
public uint quantity => order.quantity();
|
||||
public uint display_quantity => quantity.safe_divide(_display_qty_divisor);
|
||||
public uint filled_quantity => order.filled();
|
||||
public uint display_filled_quantity => filled_quantity.safe_divide(_display_qty_divisor);
|
||||
public string last_msg => order.last_msg();
|
||||
// piq
|
||||
public uint piq => order_tuple.order.piq ?? uint.MaxValue;
|
||||
public string piq_text => piq == uint.MaxValue ? string.Empty : piq.ToString();
|
||||
public string piq_text => piq == uint.MaxValue ? string.Empty : piq.safe_divide(_display_qty_divisor).ToString();
|
||||
|
||||
public bool is_non_terminal => order.is_non_terminal();
|
||||
public bool is_attachable { get => _is_attachable; set => SetVisual(ref _is_attachable, value); }
|
||||
public bool is_detachable { get => _is_detachable; set => SetVisual(ref _is_detachable, value); }
|
||||
public string filled_status_desc => order.filled_status_description(security.spec_or_default());
|
||||
public string filled_status_desc => order.filled_status_description(security.spec_or_default(), _display_qty_divisor);
|
||||
|
||||
// Root Algo
|
||||
public CustomOrderDefinition.CustomOneofCase root_algo_order_type => order_tuple.root.algo.value.CustomCase;
|
||||
@@ -113,7 +117,6 @@ namespace mbts.spark {
|
||||
// Tree
|
||||
public bool is_parent { get => _is_parent; set => Set(ref _is_parent, value); }
|
||||
public bool is_child => order.has_parent();
|
||||
public bool is_visible { get; set; }
|
||||
public bool is_expanded {
|
||||
get => _is_expanded;
|
||||
set {
|
||||
@@ -131,12 +134,12 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
public uint prospective_quantity {
|
||||
get => _prospective_quantity;
|
||||
set {
|
||||
if (Set(ref _prospective_quantity, value)) {
|
||||
RaisePropertyChanged(nameof(is_qty_dirty));
|
||||
}
|
||||
}
|
||||
get => _prospective_qty_translator.actual_value;
|
||||
set => _prospective_qty_translator.actual_value = value;
|
||||
}
|
||||
public uint prospective_display_quantity {
|
||||
get => _prospective_qty_translator.display_value;
|
||||
set => _prospective_qty_translator.display_value = value;
|
||||
}
|
||||
public double prospective_price {
|
||||
get => _prospective_price;
|
||||
@@ -226,6 +229,29 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
uint _display_qty_divisor {
|
||||
get {
|
||||
_sync_display_qty_if_dirty();
|
||||
return _display_qty_divisor_backing;
|
||||
}
|
||||
}
|
||||
|
||||
IDisplayTranslator<uint> _prospective_qty_translator {
|
||||
get {
|
||||
if (_prospective_qty_translator_backing == null) {
|
||||
_prospective_qty_translator_backing = new DisplayQuantityMultiplier {
|
||||
conversion_data = new QuantityConversionData(_display_qty_divisor, QuantityConversionData.OpType.DivideActual),
|
||||
actual_value = quantity
|
||||
};
|
||||
_prospective_qty_translator.value_changed += () =>
|
||||
RaisePropertyChanged(nameof(is_qty_dirty));
|
||||
} else {
|
||||
_sync_display_qty_if_dirty();
|
||||
}
|
||||
return _prospective_qty_translator_backing;
|
||||
}
|
||||
}
|
||||
|
||||
#region IHierarchicalRow
|
||||
bool IHierarchicalRow.is_parent => is_parent;
|
||||
bool IHierarchicalRow.is_expanded => is_expanded;
|
||||
@@ -269,22 +295,35 @@ namespace mbts.spark {
|
||||
if ((Columns.OrderType & dirty) != 0)
|
||||
RaisePropertyChanged(nameof(order_type), nameof(order_metadata));
|
||||
// quantity
|
||||
if ((Columns.Size & dirty) != 0) {
|
||||
RaisePropertyChanged(nameof(quantity), nameof(is_qty_dirty));
|
||||
// we need to update the prospective_quantity here b/c an update could have come in while the row was in edit mode
|
||||
// via the Mini OB update column mechanism (vs the EditableQty column)
|
||||
// prospecive price does not need to be updated in the same manner b/c if the price is changed, the whole
|
||||
// row will no longer match the filter
|
||||
prospective_quantity = quantity;
|
||||
}
|
||||
// price
|
||||
if ((Columns.Price & dirty) != 0) {
|
||||
RaisePropertyChanged(nameof(price));
|
||||
RaisePropertyChanged(nameof(is_price_dirty));
|
||||
}
|
||||
// security
|
||||
if ((Columns.Security & dirty) != 0)
|
||||
RaisePropertyChanged(nameof(security), nameof(security_text));
|
||||
if ((Columns.Security & dirty) != 0) {
|
||||
_display_qty_divisor_dirty = true;
|
||||
RaisePropertyChanged(
|
||||
// security props
|
||||
nameof(security),
|
||||
nameof(security_text),
|
||||
// qty props
|
||||
nameof(display_quantity),
|
||||
nameof(prospective_quantity),
|
||||
nameof(prospective_display_quantity),
|
||||
nameof(is_qty_dirty),
|
||||
//
|
||||
nameof(piq_text),
|
||||
nameof(display_filled_quantity));
|
||||
}
|
||||
if ((Columns.Size & dirty) != 0) {
|
||||
RaisePropertyChanged(nameof(quantity), nameof(display_quantity));
|
||||
if (_prospective_qty_translator_backing != null)
|
||||
_prospective_qty_translator_backing.actual_value = quantity;
|
||||
}
|
||||
if ((Columns.Filled & dirty) != 0) {
|
||||
RaisePropertyChanged(nameof(filled_quantity), nameof(display_filled_quantity));
|
||||
}
|
||||
// badge fill
|
||||
if (old_order_tuple.order.meta?.badge_color != order_tuple.order.meta?.badge_color) {
|
||||
// invalidate by setting to null (will be re-instantiated in the public getter)
|
||||
@@ -361,6 +400,15 @@ namespace mbts.spark {
|
||||
security.Value.ContractSpec.format(v) :
|
||||
Math.Round(v, _unknown_security_price_num_fractional_digits, MidpointRounding.AwayFromZero).ToString(_unknown_security_price_format_string);
|
||||
|
||||
void _sync_display_qty_if_dirty() {
|
||||
if (_display_qty_divisor_dirty) {
|
||||
_display_qty_divisor_dirty = false;
|
||||
_display_qty_divisor_backing = _owner.get_display_qty_divisor(security);
|
||||
if (_prospective_qty_translator_backing != null)
|
||||
_prospective_qty_translator_backing.conversion_data = new QuantityConversionData(_display_qty_divisor_backing, QuantityConversionData.OpType.DivideActual);
|
||||
}
|
||||
}
|
||||
|
||||
void RaisePropertyChanged(params string[] props) {
|
||||
var pc = PropertyChanged;
|
||||
if (pc == null)
|
||||
@@ -399,6 +447,7 @@ namespace mbts.spark {
|
||||
Columns.LastMessage |
|
||||
Columns.Piq,
|
||||
filled_status_desc_mask =
|
||||
Columns.Security |
|
||||
Columns.Filled |
|
||||
Columns.AvgFilled |
|
||||
Columns.Status;
|
||||
|
||||
@@ -51,6 +51,7 @@ namespace mbts.spark {
|
||||
readonly OrderParamsAggregator _selected_params_mon;
|
||||
readonly CommonValueMonitor<SecurityImmutable?> _selected_security_mon = new CommonValueMonitor<SecurityImmutable?>();
|
||||
readonly RightPanelModel _right_panel = new RightPanelModel();
|
||||
readonly CustomStateSynchronizer _custom_state_sync;
|
||||
OrderBookFillsGridSettings _cached_fills_settings = OrderBookStartupData.DefaultFillsGrid;
|
||||
double
|
||||
_raw_right_panel_width = OrderBookStartupData.Default.right_panel_width,
|
||||
@@ -124,6 +125,8 @@ namespace mbts.spark {
|
||||
_row_collection.CollectionChanged += _row_collection_CollectionChanged;
|
||||
_current_row = backing<OrderBookRowVM>(nameof(current_row));
|
||||
_columns.filter_changed += _on_filter_changed;
|
||||
_custom_state_sync = new CustomStateSynchronizer(this);
|
||||
// commands
|
||||
collapse_all_rows_command = new DelegateCommand(_row_collection.collapse_all);
|
||||
expand_all_rows_command = new DelegateCommand(_row_collection.expand_all);
|
||||
var modify_selected_orders_async_cmd = new AsyncCommand(_modify_selected_orders_async, () => _selected_params_mon.all_active && order_mod_vm.is_dirty);
|
||||
@@ -188,6 +191,7 @@ namespace mbts.spark {
|
||||
security_plurality = p;
|
||||
RaisePropertyChanged(nameof(security_plurality));
|
||||
}),
|
||||
_current_row.Subscribe(_ => _custom_state_sync.sync()),
|
||||
orders_source.OnDispatcher().Subscribe(t => {
|
||||
if (_selected_rows.Count == 0) {
|
||||
_cached_orders = t.state;
|
||||
@@ -207,6 +211,8 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
}
|
||||
_custom_state_sync.sync(true);
|
||||
orders_changed?.Invoke();
|
||||
}),
|
||||
model.on_orders_can_attach.OnDispatcher().Subscribe(table => {
|
||||
_cached_attachable = new MappedDictionary<uint, KeyValuePair<uint, bool>, bool>(table.state, kv => kv.Value);
|
||||
@@ -375,7 +381,7 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public event Action<OrderRecordImmutable?> selected_order_changed;
|
||||
public event Action begin_attaching, end_attaching;
|
||||
public event Action begin_attaching, end_attaching, orders_changed;
|
||||
public event Action<(IEnumerable, bool)> request_selection;
|
||||
|
||||
public ObservableAppendCollection<DIYGrid.ColumnBase> columns { get; }
|
||||
@@ -395,6 +401,7 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
public OrderRecordImmutable? current_order => current_row?.order;
|
||||
public ObservableCollection<NamedValueVM> current_algo_custom_state { get; } = new ObservableCollection<NamedValueVM>();
|
||||
public AlgoDefinitionVM algo_vm => order_mod_vm.algo_vm;
|
||||
public OrderParamsVM order_mod_vm { get; }
|
||||
public FillsVMContainer fills_vm { get; } = new FillsVMContainer();
|
||||
@@ -474,6 +481,28 @@ namespace mbts.spark {
|
||||
public IObservable<bool> subscribe_show_fills() => _show_fills.observe_current(true);
|
||||
public IObservable<bool> subscribe_show_ladder() => _show_ladder.observe_current(true);
|
||||
public IObservable<bool> subscribe_show_order_params() => _show_order_params.observe_current(true);
|
||||
public IObservable<OrderSecurityAlgo?> subscribe_single_selected_order() =>
|
||||
Observable.Create<OrderSecurityAlgo?>(o => {
|
||||
OrderSecurityAlgo? last_fired = null;
|
||||
orders_changed += notify;
|
||||
_current_row.Changed += on_current_row_changed;
|
||||
notify();
|
||||
return Disposable.Create(dispose);
|
||||
// local methods
|
||||
void notify() {
|
||||
var next = current_row == null ? (OrderSecurityAlgo?)null : current_row.order_tuple.order;
|
||||
if (next != last_fired)
|
||||
o.OnNext(last_fired = next);
|
||||
}
|
||||
|
||||
void on_current_row_changed(OrderBookRowVM oldval, OrderBookRowVM newval) =>
|
||||
notify();
|
||||
|
||||
void dispose() {
|
||||
orders_changed -= notify;
|
||||
_current_row.Changed -= on_current_row_changed;
|
||||
}
|
||||
});
|
||||
|
||||
public void row_selection_changed(SelectionChangedEventArgs e) {
|
||||
try {
|
||||
@@ -503,6 +532,7 @@ namespace mbts.spark {
|
||||
_can_attach_selected_orders.refresh();
|
||||
_can_detach_selected_orders.refresh();
|
||||
selected_order_changed?.Invoke(current_order);
|
||||
_custom_state_sync.sync();
|
||||
} catch (Exception ex) {
|
||||
// [John]I observed a strange null-reference exception in this callback when the launcher was shutting down (see https://bluetradesys.atlassian.net/browse/SPARK-2089)
|
||||
Log.Ui.error($"[{nameof(OrderBookVM)}] Error handling selection change: {ex}");
|
||||
@@ -609,6 +639,9 @@ namespace mbts.spark {
|
||||
|
||||
public OrderMetadata get_metadata(OrderBookRowVM row) => _model.factory.get_metadata(row.order.actual_order_type());
|
||||
|
||||
internal uint get_display_qty_divisor(SecurityImmutable? security) =>
|
||||
_model.security_auxiliary_model.get_display_quantity_divisor(security);
|
||||
|
||||
internal void show_order_modify(OrderBookRowVM row) =>
|
||||
_bus.post(SparkTags.ShowModifyOrderTag, row.order_key);
|
||||
|
||||
@@ -723,13 +756,52 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
class CustomStateSynchronizer {
|
||||
readonly OrderBookVM _vm;
|
||||
OrderBookRowVM _synced_row;
|
||||
|
||||
public CustomStateSynchronizer(OrderBookVM vm) =>
|
||||
_vm = vm;
|
||||
|
||||
public void sync(bool force_refresh = false) {
|
||||
var row = _vm.current_row != null &&
|
||||
_vm.selected_rows.Select(r => r.order_tuple.root.order.Key).Distinct().TryGetSingle(out uint droot) &&
|
||||
_vm.current_row.order_tuple.root.order.Key == droot ?
|
||||
_vm.current_row : null;
|
||||
if (force_refresh || row != _synced_row) {
|
||||
_synced_row = row;
|
||||
_refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void _refresh() {
|
||||
_vm.current_algo_custom_state.Clear();
|
||||
if (_synced_row == null)
|
||||
return;
|
||||
var dcs = _synced_row.order.dynamic_custom_state();
|
||||
// while dcs is not expected to be null, best to play it safe and double check.
|
||||
if (dcs.is_null_or_empty()) {
|
||||
var descendent_rows = _vm._row_collection.get_descendent_rows(_synced_row.order_key);
|
||||
if (_vm._row_collection.filter != null)
|
||||
descendent_rows = descendent_rows.Where(child => _vm._row_collection.filter(child.order_tuple));
|
||||
foreach (var dr in descendent_rows) {
|
||||
dcs = dr.order.dynamic_custom_state();
|
||||
if (!dcs.is_null_or_empty())
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach (var p in dcs.empty_if_null().OrderBy(kv => kv.Key))
|
||||
_vm.current_algo_custom_state.Add(new NamedValueVM(p.Key, DynamicOrderHelper.fmt_param(p.Value)));
|
||||
}
|
||||
}
|
||||
|
||||
public class OrderParamsVM : DisposableViewModelBase {
|
||||
readonly SelectorProperty<AccountTuple, AccountTuple?> _accounts;
|
||||
readonly OrderBookVM _owner;
|
||||
readonly Property<bool> _is_dirty;
|
||||
IRaiseCanExecuteChangedCommand _payup_bid_cmd, _payup_ask_cmd;
|
||||
IAlgoParamVM<double> _price_param;
|
||||
IAlgoParamVM<uint?> _size_param;
|
||||
AlgoParamVM<uint?> _size_param;
|
||||
AlgoParamPermissions
|
||||
_price_permissions = AlgoParamPermissions.ReadOnly | AlgoParamPermissions.Write,
|
||||
_size_permissions = AlgoParamPermissions.ReadOnly | AlgoParamPermissions.Write;
|
||||
@@ -766,7 +838,9 @@ namespace mbts.spark {
|
||||
_price_param = null;
|
||||
_size_param = null;
|
||||
} else {
|
||||
_size_param = new AlgoParamVM<uint?>("size", "Size", owner._selected_params_mon.size) {
|
||||
_size_param = new AlgoParamVM<uint?>("size", "Size", owner._selected_params_mon.size, EqualityComparer<uint?>.Default,
|
||||
to_display_value: v => v.HasValue ? v.Value / display_quantity_divisor : v,
|
||||
from_display_value: v => v.HasValue ? v.Value * display_quantity_divisor : v) {
|
||||
permissions = _size_permissions & _param_permissions
|
||||
};
|
||||
var price_param = new AlgoParamVM<double>("price", "Price", owner._selected_params_mon.price, spec?.tick_equality_comparer() ?? MathUtils.epsilon_eq_comparer) {
|
||||
@@ -809,10 +883,11 @@ namespace mbts.spark {
|
||||
get => _security_md;
|
||||
set {
|
||||
if (Set(ref _security_md, value)) {
|
||||
RaisePropertyChanged(nameof(security), nameof(spec));
|
||||
RaisePropertyChanged(nameof(security), nameof(spec), nameof(display_quantity_divisor));
|
||||
bbo.security_md = value;
|
||||
_payup_bid_cmd.RaiseCanExecuteChanged();
|
||||
_payup_ask_cmd.RaiseCanExecuteChanged();
|
||||
_size_param?.refresh_display_value();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -834,6 +909,7 @@ namespace mbts.spark {
|
||||
public AlgoDefinitionVM algo_vm { get; }
|
||||
public IReadOnlyObservableCollection<AlgoParamRowVM> param_rows { get; }
|
||||
public BBOVM bbo { get; }
|
||||
public uint display_quantity_divisor => _owner.get_display_qty_divisor(security);
|
||||
public ICommand send_order { get; }
|
||||
public ICommand payup_bid_cmd => _payup_bid_cmd;
|
||||
public ICommand payup_ask_cmd => _payup_ask_cmd;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using bts.api;
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.shell;
|
||||
using bts.shell.fills;
|
||||
@@ -6,9 +7,11 @@ using bts.shell.orderbook;
|
||||
using bts.spark;
|
||||
using bts.view;
|
||||
using mbts.persist;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utilities;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
@@ -24,7 +27,10 @@ namespace mbts.spark {
|
||||
const string _window_type = "OrderBook", _default_title = "Order Book";
|
||||
readonly INotificationService _notification_service;
|
||||
readonly IWindowSettingsManager<OrderBookGridSettings> _settings_manager;
|
||||
readonly bool _can_edit_specific_algo_params;
|
||||
IDisposable _attachment_exit_sub;
|
||||
// initialized in thread_init
|
||||
List<IDisposable> _thread_init_resources;
|
||||
OrderBookStartupData _state;
|
||||
OrdersModel _model;
|
||||
OrderBookVM _vm;
|
||||
@@ -34,13 +40,14 @@ namespace mbts.spark {
|
||||
SparkCommand[] _commands;
|
||||
|
||||
public OrderBookWindow(OrdersModel model, OrderBookWindowFactory factory, IBus bus, INotificationService notification_service,
|
||||
OrderBookStartupData state, ImageSource icon, IWindowSettingsManager<OrderBookGridSettings> settings_manager)
|
||||
OrderBookStartupData state, ImageSource icon, IWindowSettingsManager<OrderBookGridSettings> settings_manager, bool can_edit_specific_algo_params)
|
||||
: base(_default_title, bus, factory, SparkCommands.ShowOrders.icon) {
|
||||
_model = model;
|
||||
_factory = factory;
|
||||
_notification_service = notification_service;
|
||||
_state = state;
|
||||
_settings_manager = settings_manager;
|
||||
_can_edit_specific_algo_params = can_edit_specific_algo_params;
|
||||
thread_id = config.Configuration.get<bts.config.MultiThreadedWindows>().order_book ? "OrderBook" : null;
|
||||
}
|
||||
|
||||
@@ -64,12 +71,14 @@ namespace mbts.spark {
|
||||
public string thread_id { get; }
|
||||
|
||||
public override void Dispose() {
|
||||
_commands.OfType<IDisposable>().DisposeAll();
|
||||
Disposable.dispose(_thread_init_resources);
|
||||
Disposable.dispose(ref _attachment_exit_sub);
|
||||
Disposable.dispose(ref _vm);
|
||||
}
|
||||
|
||||
public void thread_init() {
|
||||
Log.assert(_thread_init_resources == null, $"{nameof(_thread_init_resources)} must be initialized in thread_init.");
|
||||
_thread_init_resources = new List<IDisposable>();
|
||||
_menu_display_vm = new SparkMenuDisplayVM();
|
||||
if (_state != null)
|
||||
_menu_display_vm.load_from(_state.menu_display);
|
||||
@@ -147,6 +156,15 @@ namespace mbts.spark {
|
||||
can_execute: () => _vm.selected_orders.Any(x => passes_recreate_search_criteria(x))
|
||||
);
|
||||
recreate_selected_orders_command.group = CommandGroups.Modify;
|
||||
ICommand edit_algo_params_cmd;
|
||||
if (_can_edit_specific_algo_params) {
|
||||
_thread_init_resources.Add(CommandHelper.bind_param_source(
|
||||
new DialogCommand<OrderSecurityAlgo?, Window>(mk_algo_params_dialog, has_algo_params),
|
||||
_vm.subscribe_single_selected_order(),
|
||||
out edit_algo_params_cmd, true));
|
||||
} else {
|
||||
edit_algo_params_cmd = DelegateCommand.empty;
|
||||
}
|
||||
|
||||
var attach_selected_orders_command = new SparkCommandCommand(
|
||||
"Attach To Selected",
|
||||
@@ -194,14 +212,22 @@ namespace mbts.spark {
|
||||
show_grid_settings_command.group = CommandGroups.Auxiliary;
|
||||
|
||||
_commands = new SparkCommand[] {
|
||||
// modify commands
|
||||
toggle_show_right_panel_command,
|
||||
attach_selected_orders_command,
|
||||
detach_selected_orders_command,
|
||||
recreate_selected_orders_command,
|
||||
new SparkCommandCommand("Algo Params...", Icons.Algo, edit_algo_params_cmd) {
|
||||
group = CommandGroups.Modify,
|
||||
show_in_toolbar = false,
|
||||
show_in_context_menu = _can_edit_specific_algo_params
|
||||
},
|
||||
// cancel commands
|
||||
cancel_selected_orders_command,
|
||||
cancel_buy_orders_command,
|
||||
cancel_sell_orders_command,
|
||||
cancel_all_orders_command,
|
||||
|
||||
toggle_collapse_all_command,
|
||||
toggle_expand_all_command,
|
||||
show_grid_settings_command,
|
||||
@@ -210,9 +236,12 @@ namespace mbts.spark {
|
||||
hidden_cmd(new SparkCommandCommand(OrderBookView.CommandDescriptions.RevertOrders, null, _vm.revert_selected_orders_cmd, OrderBookView.KeyboardShortcuts.RevertOrders)),
|
||||
_menu_display_vm.spark_cmd
|
||||
};
|
||||
_vm.as_demuxer().subscribe<bool>(nameof(OrderBookVM.show_right_panel))
|
||||
.Subscribe(_ => toggle_show_right_panel_command.title = get_toggle_show_right_panel_cmd_title());
|
||||
_thread_init_resources.AddRange(_commands.OfType<IDisposable>());
|
||||
_thread_init_resources.Add(_vm.as_demuxer()
|
||||
.subscribe<bool>(nameof(OrderBookVM.show_right_panel))
|
||||
.Subscribe(_ => toggle_show_right_panel_command.title = get_toggle_show_right_panel_cmd_title()));
|
||||
_vm.selected_order_changed += vm_selected_order_changed;
|
||||
_thread_init_resources.Add(Disposable.Create(() => _vm.selected_order_changed -= vm_selected_order_changed));
|
||||
// local methods
|
||||
string get_toggle_show_right_panel_cmd_title() =>
|
||||
_vm.show_right_panel ? OrderBookView.CommandDescriptions.HideRightPanel : OrderBookView.CommandDescriptions.ShowRightPanel;
|
||||
@@ -234,6 +263,36 @@ namespace mbts.spark {
|
||||
attach_selected_orders_command.raiseCanExecuteChanged();
|
||||
detach_selected_orders_command.raiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
bool has_algo_params(OrderSecurityAlgo? order) =>
|
||||
order.HasValue && !OrderDataHelper.get_metadata(order.Value.algo.value.CustomCase).NoProps;
|
||||
|
||||
Window mk_algo_params_dialog(OrderSecurityAlgo? order) {
|
||||
if (!has_algo_params(order))
|
||||
return null;
|
||||
var algo_def = AlgoDefinitionVM.create(_model, order.Value.algo, editing_permissions: AlgoEditingPermissions.AllowEditValues);
|
||||
var algo_params_vm = new OrderBookAlgoParamsVM(algo_def, order.Value.order.is_non_terminal());
|
||||
var dialog = new OrderBookAlgoParamsDialog {
|
||||
Content = algo_params_vm
|
||||
};
|
||||
ui.WindowStyle.SetTitleContent(dialog, algo_def);
|
||||
dialog.Closed += (s, e) => {
|
||||
Disposable.dispose(ref algo_params_vm);
|
||||
dialog = null;
|
||||
};
|
||||
algo_params_vm.RequestCommit += commit;
|
||||
algo_params_vm.RequestClose += (s, e) => close_dialog();
|
||||
return dialog;
|
||||
|
||||
void close_dialog() {
|
||||
dialog?.Close();
|
||||
dialog = null;
|
||||
}
|
||||
void commit(CustomOrderDefinitionImmutable value) {
|
||||
_model.update_order(order.Value.order, new_algo: value).ignore();
|
||||
close_dialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnParentViewCreated(SparkWindowView parent_view) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using bts.api;
|
||||
using bts.shell.orderbook;
|
||||
using bts.spark;
|
||||
using mbts.config;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
@@ -15,8 +16,11 @@ namespace bts.shell {
|
||||
readonly INotificationServiceDataCache _notification_service_data_cache;
|
||||
readonly OrdersModel _model;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
readonly OrderBookConfig _config;
|
||||
|
||||
public OrderBookWindowFactory(IShellModule owner, IBus bus, INotificationService notification_service, INotificationServiceDataCache notification_service_data_cache, OrdersModel model, UserSettingsHub settings_hub) : base(owner, "SparkOrderBookWindow") {
|
||||
var raw_config = Configuration.get<bts.config.OrderBookOptions>();
|
||||
_config = new OrderBookConfig(raw_config.can_edit_specific_algo_params);
|
||||
_bus = bus;
|
||||
_notification_service = notification_service;
|
||||
_notification_service_data_cache = notification_service_data_cache;
|
||||
@@ -29,7 +33,8 @@ namespace bts.shell {
|
||||
var settings_manager = new WindowSettingsManager<OrderBookGridSettings>(_settings_hub, state.grid_settings_wrapper(),
|
||||
us => us.order_book_grid ?? OrderBookStartupData.DefaultGrid,
|
||||
(us, s) => us.with(order_book_grid: s));
|
||||
var w = new OrderBookWindow(_model, this, _bus, _notification_service, state, SparkCommands.ShowOrders.icon, settings_manager.as_non_shared());
|
||||
var w = new OrderBookWindow(_model, this, _bus, _notification_service, state,
|
||||
SparkCommands.ShowOrders.icon, settings_manager.as_non_shared(), _config.can_edit_specific_algo_params);
|
||||
return Task.FromResult<IShellWindow>(w);
|
||||
}
|
||||
|
||||
@@ -37,5 +42,12 @@ namespace bts.shell {
|
||||
var w = await load(startup_data);
|
||||
_bus.post(SparkTags.ShowWindow, w);
|
||||
}
|
||||
|
||||
readonly struct OrderBookConfig {
|
||||
public readonly bool can_edit_specific_algo_params;
|
||||
|
||||
public OrderBookConfig(bool can_edit_specific_algo_params) =>
|
||||
this.can_edit_specific_algo_params = can_edit_specific_algo_params;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<Window
|
||||
x:Class="bts.shell.orderbook.OrderBookAlgoParamsDialog"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:sp="clr-namespace:mbts.spark"
|
||||
xmlns:ob="clr-namespace:bts.shell.orderbook"
|
||||
xmlns:usp="clr-namespace:mbts.ui.spark"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
u:WindowStyle.SizeToContent="WidthAndHeight"
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
Style="{StaticResource BTSWindowTitleTemplate}"
|
||||
>
|
||||
<u:WindowStyle.TitleContentTemplate>
|
||||
<DataTemplate DataType="{x:Type sp:AlgoDefinitionVM}">
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
||||
<usp:OrderTypeItem Margin="4,0" DisplayName="{Binding name, StringFormat='{}{0} Params'}" BadgeFill="{Binding badge_fill}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</u:WindowStyle.TitleContentTemplate>
|
||||
<Window.ContentTemplate>
|
||||
<DataTemplate DataType="{x:Type ob:OrderBookAlgoParamsVM}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<usp:AlgoEditorGridView
|
||||
Width="390"
|
||||
Margin="5"
|
||||
AlgoParamRows="{Binding param_rows}"
|
||||
CanUserChangeAlgoRoot="False"
|
||||
/>
|
||||
<c:DialogSkirt Grid.Row="1" RightItems="{Binding dialog_skirt}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</Window.ContentTemplate>
|
||||
</Window>
|
||||
@@ -0,0 +1,49 @@
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
|
||||
namespace bts.shell.orderbook {
|
||||
/// <summary>
|
||||
/// Interaction logic for OrderBookAlgoParamsDialog.xaml
|
||||
/// </summary>
|
||||
partial class OrderBookAlgoParamsDialog : Window {
|
||||
public OrderBookAlgoParamsDialog() =>
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
class OrderBookAlgoParamsVM : DisposableViewModelBase, IRequestViewClose {
|
||||
readonly ObservableAggregateCollection<AlgoParamRowVM> _param_rows = new ObservableAggregateCollection<AlgoParamRowVM>();
|
||||
readonly DialogSkirtItem _commit, _cancel;
|
||||
|
||||
public OrderBookAlgoParamsVM(AlgoDefinitionVM algo_definition, bool can_user_edit) {
|
||||
this.algo_definition = algo_definition;
|
||||
_dispose = AlgoParamRowsHelper.connect(algo_definition, _param_rows, false);
|
||||
_commit = new DialogSkirtItem("OK", new DelegateCommand(commit, () => can_user_edit)) { is_default = true };
|
||||
_cancel = new DialogSkirtItem("Cancel", new DelegateCommand(close)) { is_cancel = true };
|
||||
// local methods
|
||||
void commit() {
|
||||
RequestCommit?.Invoke(new CustomOrderDefinitionImmutable(algo_definition.algo_root.created_assigned()));
|
||||
close();
|
||||
}
|
||||
void close() => RequestClose?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public event EventHandler RequestClose;
|
||||
public event Action<CustomOrderDefinitionImmutable> RequestCommit;
|
||||
|
||||
public AlgoDefinitionVM algo_definition { get; }
|
||||
public IReadOnlyObservableCollection<AlgoParamRowVM> param_rows => _param_rows;
|
||||
public IEnumerable<DialogSkirtItem> dialog_skirt {
|
||||
get {
|
||||
yield return _commit;
|
||||
yield return _cancel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="{x:Static usp:OrderParamsPanel.BestBidSizeButtonStyleKey}" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Command" Value="{Binding DataContext.size_calculator.set_value_command, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
|
||||
<Setter Property="Command" Value="{Binding DataContext.size_calculator.set_actual_value_command, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
|
||||
<Setter Property="CommandParameter" Value="{Binding BestBidSize, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
|
||||
<Setter Property="ToolTip">
|
||||
<Setter.Value>
|
||||
@@ -82,7 +82,7 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="{x:Static usp:OrderParamsPanel.BestAskSizeButtonStyleKey}" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Command" Value="{Binding DataContext.size_calculator.set_value_command, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
|
||||
<Setter Property="Command" Value="{Binding DataContext.size_calculator.set_actual_value_command, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
|
||||
<Setter Property="CommandParameter" Value="{Binding BestAskSize, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
|
||||
<Setter Property="ToolTip">
|
||||
<Setter.Value>
|
||||
@@ -101,7 +101,7 @@
|
||||
<TextBlock.Inlines>
|
||||
<Run Text="{Binding security, Mode=OneWay}" FontWeight="Bold"/>
|
||||
<Run Text="{Binding side, Mode=OneWay}" Foreground="{Binding side, Converter={StaticResource SideToBrushConverter}}"/>
|
||||
<Run Text="{Binding quantity, Mode=OneWay}"/>
|
||||
<Run Text="{Binding display_quantity, Mode=OneWay}"/>
|
||||
<Run>@</Run>
|
||||
<Run>
|
||||
<Run.Text>
|
||||
@@ -484,6 +484,7 @@
|
||||
<Border Padding="12,12,12,6">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
@@ -495,7 +496,45 @@
|
||||
AlgoParamRows="{Binding param_rows}"
|
||||
IsReadOnly="{Binding DataContext.can_user_modify_selected_orders, ElementName=Root, Converter={StaticResource NotConverter}}"
|
||||
/>
|
||||
<StackPanel Grid.Row="1" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,8,0,0" Orientation="Horizontal">
|
||||
<c:ExpanderToggleButton
|
||||
x:Name="algo_state_toggle"
|
||||
Grid.Row="1"
|
||||
BorderThickness="0"
|
||||
Content="Algo State"
|
||||
HorizontalContentAlignment="Left"
|
||||
Margin="0,4,0,0"
|
||||
VerticalAlignment="Top"
|
||||
Visibility="{Binding current_algo_custom_state.Count, Converter={StaticResource IntNotZeroVisibilityConverter}}"
|
||||
/>
|
||||
<c:DIYGrid Grid.Row="1" Rows="{Binding current_algo_custom_state}" Margin="0,28,0,4">
|
||||
<c:DIYGrid.Visibility>
|
||||
<MultiBinding>
|
||||
<MultiBinding.Converter>
|
||||
<cv:MultiValueConverterPipe>
|
||||
<cv:BooleanAndConverter/>
|
||||
<cv:BooleanToVisibilityConverter/>
|
||||
</cv:MultiValueConverterPipe>
|
||||
</MultiBinding.Converter>
|
||||
<Binding Path="current_algo_custom_state.Count" Mode="OneWay" Converter="{StaticResource IntNotZeroConverter}"/>
|
||||
<Binding Path="IsChecked" ElementName="algo_state_toggle"/>
|
||||
</MultiBinding>
|
||||
</c:DIYGrid.Visibility>
|
||||
<c:DIYGrid.Columns>
|
||||
<c:DIYGridColumnCollection>
|
||||
<c:DIYGridColumn id="Name" Header="Param" Width="176"/>
|
||||
<c:DIYGridColumn id="Value" Header="Value" Width="176"/>
|
||||
</c:DIYGridColumnCollection>
|
||||
</c:DIYGrid.Columns>
|
||||
<c:DIYGrid.RowTemplate>
|
||||
<DataTemplate>
|
||||
<Grid c:DIYGrid.SyncWithColumnGeometry="True">
|
||||
<c:TextCell c:DIYGrid.ColumnKey="Name" Text="{Binding name}"/>
|
||||
<c:TextCell c:DIYGrid.ColumnKey="Value" Text="{Binding value}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</c:DIYGrid.RowTemplate>
|
||||
</c:DIYGrid>
|
||||
<StackPanel Grid.Row="2" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,8,0,0" Orientation="Horizontal">
|
||||
<Button x:Name="modify_orders_button" Command="{Binding modify_selected_orders_cmd}">
|
||||
<Button.Margin>
|
||||
<Thickness Right="{StaticResource DialogSkirtButtonPadding.Right}"/>
|
||||
|
||||
@@ -5,6 +5,7 @@ using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
@@ -140,15 +141,18 @@ namespace bts.shell.pnl {
|
||||
public PositionPriceSize summary =>
|
||||
new PositionPriceSize(long_data.working_total, long_data.avg_price, short_data.working_total, short_data.avg_price);
|
||||
|
||||
public static WorkingOrderData Create(IEnumerable<OrderRecordImmutable> orders) =>
|
||||
orders.Aggregate(Empty, (seed, order) => seed.Add(order));
|
||||
public static WorkingOrderData Create(IEnumerable<OrderRecordImmutable> orders, QuantityConversionData qty_conversion) =>
|
||||
Create(orders.Select(o => (o, qty_conversion)));
|
||||
|
||||
public WorkingOrderData Add(OrderRecordImmutable order) {
|
||||
public static WorkingOrderData Create(IEnumerable<(OrderRecordImmutable order, QuantityConversionData qty_conversion)> orders) =>
|
||||
orders.Aggregate(Empty, (seed, tup) => seed.Add(tup.order, tup.qty_conversion));
|
||||
|
||||
public WorkingOrderData Add(OrderRecordImmutable order, QuantityConversionData qty_conversion) {
|
||||
if (order.working_quantity() <= 0)
|
||||
return new WorkingOrderData(long_data, short_data);
|
||||
switch (order.side()) {
|
||||
case Side.Buy: return new WorkingOrderData(long_data.Add(order), short_data);
|
||||
case Side.Sell: return new WorkingOrderData(long_data, short_data.Add(order));
|
||||
case Side.Buy: return new WorkingOrderData(long_data.Add(order, qty_conversion), short_data);
|
||||
case Side.Sell: return new WorkingOrderData(long_data, short_data.Add(order, qty_conversion));
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side");
|
||||
}
|
||||
}
|
||||
@@ -165,18 +169,21 @@ namespace bts.shell.pnl {
|
||||
}
|
||||
|
||||
readonly struct WorkingOrderSideData {
|
||||
public readonly double avg_price;
|
||||
public readonly ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data;
|
||||
public readonly double avg_price, display_avg_price;
|
||||
public readonly ImmutableSortedDictionary<double, ImmutableDictionary<uint, (OrderRecordImmutable order, QuantityConversionData qty_conversion)>> data;
|
||||
public readonly int working_total;
|
||||
public readonly int display_working_total;
|
||||
|
||||
WorkingOrderSideData(double avg_price, int working_total, ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data) {
|
||||
WorkingOrderSideData(double avg_price, int working_total, double display_avg_price, int display_working_total, ImmutableSortedDictionary<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>> data) {
|
||||
this.avg_price = avg_price;
|
||||
this.working_total = working_total;
|
||||
this.display_avg_price = display_avg_price;
|
||||
this.data = data;
|
||||
this.display_working_total = display_working_total;
|
||||
}
|
||||
|
||||
public static WorkingOrderSideData EmptyLong => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(new ReverseComparer<double>(OrderDataHelper.PriceComparer)));
|
||||
public static WorkingOrderSideData EmptyShort => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(OrderDataHelper.PriceComparer));
|
||||
public static WorkingOrderSideData EmptyLong => new WorkingOrderSideData(double.NaN, 0, double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>>(new ReverseComparer<double>(OrderDataHelper.PriceComparer)));
|
||||
public static WorkingOrderSideData EmptyShort => new WorkingOrderSideData(double.NaN, 0, double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>>(OrderDataHelper.PriceComparer));
|
||||
|
||||
public double top_price =>
|
||||
data.Count > 0 ?
|
||||
@@ -185,23 +192,35 @@ namespace bts.shell.pnl {
|
||||
|
||||
public int top_volume =>
|
||||
data.Count > 0 ?
|
||||
data.Values.First().Values.Sum(o => o.working_quantity()) :
|
||||
data.Values.First().Values.Sum(o => o.order.working_quantity()) :
|
||||
0;
|
||||
|
||||
public WorkingOrderSideData Add(OrderRecordImmutable order_record) {
|
||||
uint order = order_record.Key;
|
||||
public int display_top_volume =>
|
||||
data.Count > 0 ?
|
||||
data.Values.First().Values.Sum(o => o.qty_conversion.to_display(o.order.working_quantity())) :
|
||||
0;
|
||||
|
||||
public WorkingOrderSideData Add(OrderRecordImmutable order_record) =>
|
||||
Add(order_record, new QuantityConversionData());
|
||||
|
||||
public WorkingOrderSideData Add(OrderRecordImmutable order_record, QuantityConversionData qty_conversion) {
|
||||
int working_qty = order_record.working_quantity();
|
||||
double price = order_record.price();
|
||||
if (working_qty <= 0)
|
||||
throw new ArgumentException(nameof(working_qty));
|
||||
uint order = order_record.Key;
|
||||
int dworking_qty = qty_conversion.to_display(working_qty);
|
||||
double price = order_record.price();
|
||||
if (working_total == 0) {
|
||||
return new WorkingOrderSideData(price, working_qty, data.Add(price, ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
|
||||
Log.assert(display_working_total == 0);
|
||||
return new WorkingOrderSideData(price, working_qty, display_avg_price, dworking_qty, data.Add(price, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>.Empty.Add(order, (order_record, qty_conversion))));
|
||||
} else return new WorkingOrderSideData(
|
||||
avg_price + ((price - avg_price) * ((double)working_qty / (working_total + working_qty))),
|
||||
working_total + working_qty,
|
||||
display_avg_price + ((price - display_avg_price) * ((double)dworking_qty / (display_working_total + dworking_qty))),
|
||||
display_working_total + dworking_qty,
|
||||
data.SetItem(price, data.TryGetValue(price, out var curr) ?
|
||||
curr.Add(order, order_record) :
|
||||
ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
|
||||
curr.Add(order, (order_record, qty_conversion)) :
|
||||
ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>.Empty.Add(order, (order_record, qty_conversion))));
|
||||
}
|
||||
|
||||
public WorkingOrderSideData Remove(uint order, double price) {
|
||||
@@ -210,22 +229,30 @@ namespace bts.shell.pnl {
|
||||
throw new KeyNotFoundException(nameof(price));
|
||||
if (!wdata.TryGetValue(order, out var worder))
|
||||
throw new KeyNotFoundException(nameof(order));
|
||||
var wqty = worder.working_quantity();
|
||||
var wqty = worder.order.working_quantity();
|
||||
var dwqty = worder.qty_conversion.to_display(wqty);
|
||||
wdata = wdata.Remove(order);
|
||||
if (wdata.Count == 0)
|
||||
tmp_data = tmp_data.Remove(price);
|
||||
if (tmp_data.Count == 0)
|
||||
return new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(data.KeyComparer));
|
||||
return new WorkingOrderSideData(double.NaN, 0, double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>>(data.KeyComparer));
|
||||
return new WorkingOrderSideData(
|
||||
avg_price - ((price - avg_price) * (wqty / (working_total - wqty))),
|
||||
working_total - wqty,
|
||||
display_avg_price - ((price - display_avg_price) * (dwqty / (display_working_total - dwqty))),
|
||||
display_working_total - dwqty,
|
||||
tmp_data);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
if (data.Count > 0) {
|
||||
var best_order = data.Values.First().Values.First();
|
||||
return OrderUtils.order_collection_summary(best_order.price(), avg_price, best_order.working_quantity(), working_total, SecurityDataHelper.Defaults.ContractSpecificationImmutable);
|
||||
return OrderUtils.order_collection_summary(
|
||||
best_order.order.price(),
|
||||
display_avg_price,
|
||||
best_order.qty_conversion.to_display(best_order.order.working_quantity()),
|
||||
display_working_total,
|
||||
SecurityDataHelper.Defaults.ContractSpecificationImmutable);
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
@@ -248,6 +275,7 @@ namespace bts.shell.pnl {
|
||||
|
||||
public readonly PNLKey key;
|
||||
public readonly SecurityImmutable? security;
|
||||
public readonly QuantityConversionData qty_conversion;
|
||||
public readonly string account_name;
|
||||
public readonly PNLCalculationType calc_type;
|
||||
public readonly PositionPriceSize day, raw_day, sod;
|
||||
@@ -273,7 +301,8 @@ namespace bts.shell.pnl {
|
||||
WorkingOrderData? working = null,
|
||||
SecurityImmutable? security = null,
|
||||
string account_name = null,
|
||||
PNLCalculationType calc_type = PNLCalculationType.Mid) {
|
||||
PNLCalculationType calc_type = PNLCalculationType.Mid,
|
||||
QuantityConversionData? qty_conversion = null) {
|
||||
//
|
||||
this.key = key;
|
||||
this.cpp = cpp;
|
||||
@@ -288,6 +317,7 @@ namespace bts.shell.pnl {
|
||||
this.security = security;
|
||||
this.account_name = account_name;
|
||||
this.calc_type = calc_type;
|
||||
this.qty_conversion = qty_conversion ?? new QuantityConversionData();
|
||||
}
|
||||
|
||||
[Flags]
|
||||
@@ -368,6 +398,10 @@ namespace bts.shell.pnl {
|
||||
MathUtils.weighted_average(
|
||||
sod_long_avg_price_delta_raw, sod.long_pos,
|
||||
sod_short_avg_price_delta_raw, sod.short_pos);
|
||||
// display
|
||||
public double display_sod_net_pos => qty_conversion.to_display(sod_net_pos);
|
||||
public double display_sod_long_pos => qty_conversion.to_display(sod_long_pos);
|
||||
public double display_sod_short_pos => qty_conversion.to_display(sod_short_pos);
|
||||
|
||||
/// Day
|
||||
public double day_pnl => cpp *
|
||||
@@ -395,11 +429,19 @@ namespace bts.shell.pnl {
|
||||
MathUtils.weighted_average(
|
||||
day_long_avg_price_delta_raw, day.long_pos,
|
||||
day_short_avg_price_delta_raw, day.short_pos);
|
||||
// display
|
||||
public double display_day_net_pos => qty_conversion.to_display(day_net_pos);
|
||||
public double display_day_long_pos => qty_conversion.to_display(day_long_pos);
|
||||
public double display_day_short_pos => qty_conversion.to_display(day_short_pos);
|
||||
|
||||
/// Raw Day
|
||||
public double raw_day_long_pos => raw_day.long_pos;
|
||||
public double raw_day_short_pos => raw_day.short_pos;
|
||||
public double raw_day_net_pos => raw_day.net_pos;
|
||||
// display
|
||||
public double display_raw_day_net_pos => qty_conversion.to_display(raw_day_net_pos);
|
||||
public double display_raw_day_long_pos => qty_conversion.to_display(raw_day_long_pos);
|
||||
public double display_raw_day_short_pos => qty_conversion.to_display(raw_day_short_pos);
|
||||
|
||||
/// Total
|
||||
public double total_pnl => DoubleUtil.nan_to_zero_sum(sod_pnl, day_pnl);
|
||||
@@ -428,6 +470,10 @@ namespace bts.shell.pnl {
|
||||
MathUtils.weighted_average(
|
||||
total_long_avg_price_delta_raw, total_long_pos,
|
||||
total_short_avg_price_delta_raw, total_short_pos);
|
||||
// display
|
||||
public double display_total_net_pos => qty_conversion.to_display(total_net_pos);
|
||||
public double display_total_long_pos => qty_conversion.to_display(total_long_pos);
|
||||
public double display_total_short_pos => qty_conversion.to_display(total_short_pos);
|
||||
|
||||
#region Working
|
||||
// long
|
||||
@@ -453,6 +499,10 @@ namespace bts.shell.pnl {
|
||||
MathUtils.weighted_average(
|
||||
working_long_avg_price_delta_raw, working_long_pos,
|
||||
working_short_avg_price_delta_raw, working_short_pos);
|
||||
// display
|
||||
public double display_working_net_pos => qty_conversion.to_display(working_net_pos);
|
||||
public double display_working_long_pos => qty_conversion.to_display(working_long_pos);
|
||||
public double display_working_short_pos => qty_conversion.to_display(working_short_pos);
|
||||
#endregion
|
||||
|
||||
WaterfallPriceFlags _waterfall_flags() {
|
||||
@@ -508,7 +558,8 @@ namespace bts.shell.pnl {
|
||||
settlement_price.Equals(other.settlement_price) &&
|
||||
calc_type.Equals(other.calc_type) &&
|
||||
raw_day == other.raw_day &&
|
||||
working.summary == other.working.summary;
|
||||
working.summary == other.working.summary &&
|
||||
qty_conversion == other.qty_conversion;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLRowData other && Equals(other);
|
||||
@@ -523,7 +574,7 @@ namespace bts.shell.pnl {
|
||||
}
|
||||
|
||||
static class PNLRowDataFactory {
|
||||
public static PNLRowData create((SecurityImmutable? sec, PNLUnitImmutable value) t) =>
|
||||
public static PNLRowData create((SecurityImmutable? sec, PNLUnitImmutable value, QuantityConversionData qty_conversion) t) =>
|
||||
new PNLRowData(new PNLKey(t.value.Pos.AccountId, t.value.Pos.SecurityId),
|
||||
t.value.CashValue,
|
||||
t.value.BidPrice,
|
||||
@@ -535,7 +586,8 @@ namespace bts.shell.pnl {
|
||||
t.value.Pos.raw_day_pps(),
|
||||
WorkingOrderData.Empty,
|
||||
t.sec,
|
||||
t.value.Pos.Account);
|
||||
t.value.Pos.Account,
|
||||
qty_conversion: t.qty_conversion);
|
||||
}
|
||||
|
||||
static class PNLDataExtensions {
|
||||
@@ -554,6 +606,7 @@ namespace bts.shell.pnl {
|
||||
working ?? pnl_data.working,
|
||||
security ?? pnl_data.security,
|
||||
account_name ?? pnl_data.account_name,
|
||||
calc_type ?? pnl_data.calc_type);
|
||||
calc_type ?? pnl_data.calc_type,
|
||||
qty_conversion: pnl_data.qty_conversion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,15 @@ namespace bts.shell.pnl {
|
||||
decimal sod_long_pos,
|
||||
decimal sod_short_pos,
|
||||
decimal working_long_pos = 0,
|
||||
decimal working_short_pos = 0) {
|
||||
decimal working_short_pos = 0,
|
||||
decimal? display_day_long_pos = null,
|
||||
decimal? display_day_short_pos = null,
|
||||
decimal? display_raw_day_long_pos = null,
|
||||
decimal? display_raw_day_short_pos = null,
|
||||
decimal? display_sod_long_pos = null,
|
||||
decimal? display_sod_short_pos = null,
|
||||
decimal? display_working_long_pos = null,
|
||||
decimal? display_working_short_pos = null) {
|
||||
//
|
||||
this.day_pnl = day_pnl;
|
||||
this.day_long_pos = day_long_pos;
|
||||
@@ -28,6 +36,15 @@ namespace bts.shell.pnl {
|
||||
this.sod_short_pos = sod_short_pos;
|
||||
this.working_long_pos = working_long_pos;
|
||||
this.working_short_pos = working_short_pos;
|
||||
// display
|
||||
this.display_day_long_pos = display_day_long_pos ?? day_long_pos;
|
||||
this.display_day_short_pos = display_day_short_pos ?? day_short_pos;
|
||||
this.display_raw_day_long_pos = display_raw_day_long_pos ?? raw_day_long_pos;
|
||||
this.display_raw_day_short_pos = display_raw_day_short_pos ?? raw_day_short_pos;
|
||||
this.display_sod_long_pos = display_sod_long_pos ?? sod_long_pos;
|
||||
this.display_sod_short_pos = display_sod_short_pos ?? sod_short_pos;
|
||||
this.display_working_long_pos = display_working_long_pos ?? working_long_pos;
|
||||
this.display_working_short_pos = display_working_short_pos ?? working_short_pos;
|
||||
}
|
||||
|
||||
// sod
|
||||
@@ -35,24 +52,48 @@ namespace bts.shell.pnl {
|
||||
public decimal sod_long_pos { get; }
|
||||
public decimal sod_short_pos { get; }
|
||||
public decimal sod_net_pos => sod_long_pos - sod_short_pos;
|
||||
// sod display
|
||||
public decimal display_sod_long_pos { get; }
|
||||
public decimal display_sod_short_pos { get; }
|
||||
public decimal display_sod_net_pos => display_sod_long_pos - display_sod_short_pos;
|
||||
|
||||
// day
|
||||
public decimal day_pnl { get; }
|
||||
public decimal day_long_pos { get; }
|
||||
public decimal day_short_pos { get; }
|
||||
public decimal day_net_pos => day_long_pos - day_short_pos;
|
||||
// display
|
||||
public decimal display_day_long_pos { get; }
|
||||
public decimal display_day_short_pos { get; }
|
||||
public decimal display_day_net_pos => display_day_long_pos - display_day_short_pos;
|
||||
|
||||
// raw day
|
||||
public decimal raw_day_long_pos { get; }
|
||||
public decimal raw_day_short_pos { get; }
|
||||
public decimal raw_day_net_pos => raw_day_long_pos - raw_day_short_pos;
|
||||
// display
|
||||
public decimal display_raw_day_long_pos { get; }
|
||||
public decimal display_raw_day_short_pos { get; }
|
||||
public decimal display_raw_day_net_pos => display_raw_day_long_pos - display_raw_day_short_pos;
|
||||
|
||||
// total
|
||||
public decimal total_pnl => sod_pnl + day_pnl;
|
||||
public decimal total_net_pos => total_long_pos - total_short_pos;
|
||||
public decimal total_long_pos => sod_long_pos + day_long_pos;
|
||||
public decimal total_short_pos => sod_short_pos + day_short_pos;
|
||||
// display
|
||||
public decimal display_total_long_pos => display_sod_long_pos + display_day_long_pos;
|
||||
public decimal display_total_short_pos => display_sod_short_pos + display_day_short_pos;
|
||||
public decimal display_total_net_pos => display_total_long_pos - display_total_short_pos;
|
||||
|
||||
// working
|
||||
public decimal working_long_pos { get; }
|
||||
public decimal working_net_pos => working_long_pos - working_short_pos;
|
||||
public decimal working_short_pos { get; }
|
||||
public decimal working_net_pos => working_long_pos - working_short_pos;
|
||||
// display
|
||||
public decimal display_working_long_pos { get; }
|
||||
public decimal display_working_short_pos { get; }
|
||||
public decimal display_working_net_pos => display_working_long_pos - display_working_short_pos;
|
||||
|
||||
public bool Equals(PNLNormalizedData other) =>
|
||||
day_pnl == other.day_pnl &&
|
||||
@@ -64,6 +105,17 @@ namespace bts.shell.pnl {
|
||||
sod_long_pos == other.sod_long_pos &&
|
||||
sod_short_pos == other.sod_short_pos &&
|
||||
working_long_pos == other.working_long_pos &&
|
||||
working_short_pos == other.working_short_pos &&
|
||||
// display
|
||||
sod_long_pos == other.sod_long_pos &&
|
||||
sod_short_pos == other.sod_short_pos &&
|
||||
day_long_pos == other.day_long_pos &&
|
||||
day_short_pos == other.day_short_pos &&
|
||||
raw_day_long_pos == other.raw_day_long_pos &&
|
||||
raw_day_short_pos == other.raw_day_short_pos &&
|
||||
total_long_pos == other.total_long_pos &&
|
||||
total_short_pos == other.total_short_pos &&
|
||||
working_long_pos == other.working_long_pos &&
|
||||
working_short_pos == other.working_short_pos;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
@@ -102,7 +154,16 @@ namespace bts.shell.pnl {
|
||||
a.sod_long_pos + b.sod_long_pos,
|
||||
a.sod_short_pos + b.sod_short_pos,
|
||||
a.working_long_pos + b.working_long_pos,
|
||||
a.working_short_pos + b.working_short_pos);
|
||||
a.working_short_pos + b.working_short_pos,
|
||||
// display
|
||||
a.display_day_long_pos + b.display_day_long_pos,
|
||||
a.display_day_short_pos + b.display_day_short_pos,
|
||||
a.display_raw_day_long_pos + b.display_raw_day_long_pos,
|
||||
a.display_raw_day_short_pos + b.display_raw_day_short_pos,
|
||||
a.display_sod_long_pos + b.display_sod_long_pos,
|
||||
a.display_sod_short_pos + b.display_sod_short_pos,
|
||||
a.display_working_long_pos + b.display_working_long_pos,
|
||||
a.display_working_short_pos + b.display_working_short_pos);
|
||||
|
||||
public static PNLNormalizedData operator -(PNLNormalizedData a, PNLNormalizedData b) =>
|
||||
new PNLNormalizedData(
|
||||
@@ -115,6 +176,15 @@ namespace bts.shell.pnl {
|
||||
a.sod_long_pos - b.sod_long_pos,
|
||||
a.sod_short_pos - b.sod_short_pos,
|
||||
a.working_long_pos - b.working_long_pos,
|
||||
a.working_short_pos - b.working_short_pos);
|
||||
a.working_short_pos - b.working_short_pos,
|
||||
// display
|
||||
a.display_day_long_pos - b.display_day_long_pos,
|
||||
a.display_day_short_pos - b.display_day_short_pos,
|
||||
a.display_raw_day_long_pos - b.display_raw_day_long_pos,
|
||||
a.display_raw_day_short_pos - b.display_raw_day_short_pos,
|
||||
a.display_sod_long_pos - b.display_sod_long_pos,
|
||||
a.display_sod_short_pos - b.display_sod_short_pos,
|
||||
a.display_working_long_pos - b.display_working_long_pos,
|
||||
a.display_working_short_pos - b.display_working_short_pos);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace bts.shell.pnl {
|
||||
public readonly DIYGridAnimationModel<PNLView.Columns, TRow> animation_model = new DIYGridAnimationModel<PNLView.Columns, TRow>(TimeSpan.FromSeconds(0.3));
|
||||
|
||||
static readonly Func<WorkingOrderSideData, string> _fmt_working_order_data =
|
||||
d => OrderUtils.order_collection_summary(d.top_price, d.top_volume, d.working_total);
|
||||
d => OrderUtils.order_collection_summary(d.top_price, d.display_top_volume, d.display_working_total);
|
||||
|
||||
static readonly IComparer<WorkingOrderSideData> _working_total_comparer =
|
||||
Comparer<int>.Default.wrap<int, WorkingOrderSideData>(d => d.working_total);
|
||||
|
||||
@@ -31,40 +31,40 @@ namespace bts.shell.pnl {
|
||||
security_type_cell = _cell_map.add(ColumnKeys.SecurityType, d => d.security?.ContractCase ?? Security.ContractOneofCase.None);
|
||||
exchange_cell = _cell_map.add(ColumnKeys.Exchange, d => d.security?.Exchange ?? Venue.Unset);
|
||||
symbol_cell = _cell_map.add(ColumnKeys.Symbol, d => d.security?.Symbol ?? string.Empty);
|
||||
security_cell = _cell_map.add(ColumnKeys.Security, d => d.security, null, EqualityComparer<SecurityImmutable?>.Default, Comparer<SecurityImmutable>.Default.to_nullable());
|
||||
security_cell = _cell_map.add(ColumnKeys.Security, d => (d.security), null, EqualityComparer<SecurityImmutable?>.Default, Comparer<SecurityImmutable>.Default.to_nullable());
|
||||
security_key = _cell_map.add(ColumnKeys.SecurityKey, d => d.key.security_id);
|
||||
cpp = _cell_map.add(ColumnKeys.CPP, d => d.cpp);
|
||||
// sod
|
||||
sod_pnl = _cell_map.add_pnl_double(ColumnKeys.SODPNL, d => d.sod_pnl);
|
||||
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
|
||||
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
|
||||
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.display_sod_net_pos);
|
||||
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.display_sod_long_pos);
|
||||
sod_long_avg_price = _cell_map.add_long_price(ColumnKeys.SODLongAvgPrice, d => d.sod_long_avg_price_raw);
|
||||
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
|
||||
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.display_sod_short_pos);
|
||||
sod_short_avg_price = _cell_map.add_short_price(ColumnKeys.SODShortAvgPrice, d => d.sod_short_avg_price_raw);
|
||||
// day
|
||||
day_pnl = _cell_map.add_pnl_double(ColumnKeys.DayPNL, d => d.day_pnl);
|
||||
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
|
||||
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
|
||||
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.display_day_net_pos);
|
||||
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.display_day_long_pos);
|
||||
day_long_avg_price = _cell_map.add_long_price(ColumnKeys.DayLongAvgPrice, d => d.day_long_avg_price_raw);
|
||||
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
|
||||
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.display_day_short_pos);
|
||||
day_short_avg_price = _cell_map.add_short_price(ColumnKeys.DayShortAvgPrice, d => d.day_short_avg_price_raw);
|
||||
has_day_trades = _cell_map.add(ColumnKeys.HasDayTrades, d => d.has_day_trades, _fmt_has_day_trades);
|
||||
// raw day
|
||||
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.raw_day_net_pos);
|
||||
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.raw_day_long_pos);
|
||||
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.raw_day_short_pos);
|
||||
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.display_raw_day_net_pos);
|
||||
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.display_raw_day_long_pos);
|
||||
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.display_raw_day_short_pos);
|
||||
// working
|
||||
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
|
||||
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.display_working_net_pos);
|
||||
working_long_pos = _cell_map.add_working_long(ColumnKeys.WorkingLongPos, d => d.working.long_data, d => d.bid_price);
|
||||
working_long_avg_price = _cell_map.add_long_price(ColumnKeys.WorkingLongAvgPrice, d => d.working_long_avg_price_raw);
|
||||
working_short_pos = _cell_map.add_working_short(ColumnKeys.WorkingShortPos, r => r.working.short_data, r => r.ask_price);
|
||||
working_short_avg_price = _cell_map.add_short_price(ColumnKeys.WorkingShortAvgPrice, d => d.working_short_avg_price_raw);
|
||||
// total
|
||||
total_pnl = _cell_map.add_pnl_double(ColumnKeys.TotalPNL, d => d.total_pnl);
|
||||
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.total_net_pos);
|
||||
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.total_long_pos);
|
||||
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.display_total_net_pos);
|
||||
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.display_total_long_pos);
|
||||
total_long_avg_price = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPrice, d => d.total_long_avg_price_raw);
|
||||
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.total_short_pos);
|
||||
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.display_total_short_pos);
|
||||
total_short_avg_price = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPrice, d => d.total_short_avg_price_raw);
|
||||
// market
|
||||
bid_price = _cell_map.add_price_double(ColumnKeys.BidPrice, d => d.bid_price);
|
||||
@@ -112,7 +112,16 @@ namespace bts.shell.pnl {
|
||||
sod_long,
|
||||
sod_short,
|
||||
DoubleUtil.round_to_decimal(data.working_long_pos),
|
||||
DoubleUtil.round_to_decimal(data.working_short_pos));
|
||||
DoubleUtil.round_to_decimal(data.working_short_pos),
|
||||
// display
|
||||
DoubleUtil.round_to_decimal(data.display_day_long_pos),
|
||||
DoubleUtil.round_to_decimal(data.display_day_short_pos),
|
||||
DoubleUtil.round_to_decimal(data.display_raw_day_long_pos),
|
||||
DoubleUtil.round_to_decimal(data.display_raw_day_short_pos),
|
||||
DoubleUtil.round_to_decimal(data.display_sod_long_pos),
|
||||
DoubleUtil.round_to_decimal(data.display_sod_short_pos),
|
||||
DoubleUtil.round_to_decimal(data.display_working_long_pos),
|
||||
DoubleUtil.round_to_decimal(data.display_working_short_pos));
|
||||
if (_normalized_data != nd) {
|
||||
_normalized_data = nd;
|
||||
updated?.Invoke(nd);
|
||||
@@ -206,4 +215,4 @@ namespace bts.shell.pnl {
|
||||
public override string ToString() =>
|
||||
$"account: {account_name} exchange: {exchange} symbol: {symbol} security: {security} buy_quantity: {day_long_pos} sell_quantity: {day_short_pos} pnl: {total_pnl}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,27 +112,27 @@ namespace bts.shell.pnl {
|
||||
});
|
||||
// sod
|
||||
sod_pnl = _cell_map.add_pnl(ColumnKeys.SODPNL, d => d.sod_pnl);
|
||||
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
|
||||
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
|
||||
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
|
||||
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.display_sod_net_pos);
|
||||
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.display_sod_long_pos);
|
||||
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.display_sod_short_pos);
|
||||
// day
|
||||
day_pnl = _cell_map.add_pnl(ColumnKeys.DayPNL, d => d.day_pnl);
|
||||
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
|
||||
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
|
||||
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
|
||||
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.display_day_net_pos);
|
||||
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.display_day_long_pos);
|
||||
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.display_day_short_pos);
|
||||
// raw day
|
||||
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.raw_day_net_pos);
|
||||
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.raw_day_long_pos);
|
||||
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.raw_day_short_pos);
|
||||
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.display_raw_day_net_pos);
|
||||
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.display_raw_day_long_pos);
|
||||
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.display_raw_day_short_pos);
|
||||
// total
|
||||
total_pnl = _cell_map.add(ColumnKeys.TotalPNL, d => d.total_pnl, FormattingDelegates.Price<decimal>.Value);
|
||||
total_net_pos = _cell_map.add(ColumnKeys.TotalNetPos, d => d.total_net_pos);
|
||||
total_long_pos = _cell_map.add(ColumnKeys.TotalLongPos, d => d.total_long_pos);
|
||||
total_short_pos = _cell_map.add(ColumnKeys.TotalShortPos, d => d.total_short_pos);
|
||||
total_net_pos = _cell_map.add(ColumnKeys.TotalNetPos, d => d.display_total_net_pos);
|
||||
total_long_pos = _cell_map.add(ColumnKeys.TotalLongPos, d => d.display_total_long_pos);
|
||||
total_short_pos = _cell_map.add(ColumnKeys.TotalShortPos, d => d.display_total_short_pos);
|
||||
// working
|
||||
working_net_pos = _cell_map.add(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
|
||||
working_long_pos = _cell_map.add(ColumnKeys.WorkingLongPos, d => d.working_long_pos);
|
||||
working_short_pos = _cell_map.add(ColumnKeys.WorkingShortPos, d => d.working_short_pos);
|
||||
working_net_pos = _cell_map.add(ColumnKeys.WorkingNetPos, d => d.display_working_net_pos);
|
||||
working_long_pos = _cell_map.add(ColumnKeys.WorkingLongPos, d => d.display_working_long_pos);
|
||||
working_short_pos = _cell_map.add(ColumnKeys.WorkingShortPos, d => d.display_working_short_pos);
|
||||
|
||||
_cell_map.on_row(data);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using bts.api;
|
||||
using bts.shell;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using mbts.config;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
@@ -114,6 +115,7 @@ namespace mbts.ui {
|
||||
void dispose_core() {
|
||||
Disposable.dispose(ref _settings_sub);
|
||||
Disposable.dispose(ref _toast_controller);
|
||||
Disposable.dispose(ref _sound_player);
|
||||
if (_dispatcher != null && !_dispatcher.HasShutdownStarted) {
|
||||
_dispatcher.BeginInvokeShutdown(DispatcherPriority.Background);
|
||||
}
|
||||
@@ -158,15 +160,25 @@ namespace mbts.ui {
|
||||
}
|
||||
}
|
||||
|
||||
class SoundNotificationPlayer {
|
||||
readonly IReadOnlyDictionary<SoundNotification.SystemSound, NAudioPlayer> _players;
|
||||
|
||||
public static SoundNotificationPlayer create() => new SoundNotificationPlayer(_all_players());
|
||||
class SoundNotificationPlayer : IDisposable {
|
||||
IReadOnlyDictionary<SoundNotification.SystemSound, NAudioPlayer> _players;
|
||||
|
||||
SoundNotificationPlayer(IReadOnlyDictionary<SoundNotification.SystemSound, NAudioPlayer> players) =>
|
||||
_players = players;
|
||||
|
||||
public static SoundNotificationPlayer create() =>
|
||||
new SoundNotificationPlayer(_all_players());
|
||||
|
||||
public void Dispose() {
|
||||
if (_players != null) {
|
||||
_players.Values.DisposeAll();
|
||||
_players = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void play_sounds(IEnumerable<SoundNotification> sounds) {
|
||||
if (_players == null)
|
||||
return;
|
||||
foreach (var s in sounds) {
|
||||
switch (s.sound_type_case) {
|
||||
case SoundNotification.SoundTypeOneofCase.System: _players.GetValueOrDefault(s.system_sound.Value)?.play(); break;
|
||||
@@ -177,7 +189,8 @@ namespace mbts.ui {
|
||||
}
|
||||
|
||||
static IReadOnlyDictionary<SoundNotification.SystemSound, NAudioPlayer> _all_players() =>
|
||||
EnumHelper.get_values<SoundNotification.SystemSound>().ToReadOnlyDictionary(s => s, NAudioPlayer.create);
|
||||
EnumHelper.get_values<SoundNotification.SystemSound>()
|
||||
.ToReadOnlyDictionaryWhere<SoundNotification.SystemSound, NAudioPlayer>(NAudioPlayer.try_create);
|
||||
}
|
||||
|
||||
// This class adopts to role of view-controller in the mvc paradigm, rather than view-model in mvvm.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using bts.shell;
|
||||
using bts;
|
||||
using bts.shell;
|
||||
using mbts.ui.utility;
|
||||
using NAudio.Wave;
|
||||
using System;
|
||||
@@ -6,29 +7,71 @@ using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.ui {
|
||||
class NAudioPlayer {
|
||||
class NAudioPlayer : IDisposable {
|
||||
// number of milliseconds to delay playing sound,
|
||||
// during which time all subsequent requests to play the same sound will be ignored.
|
||||
const int _delay_ms = 75;
|
||||
readonly int _start_position;
|
||||
IDisposable _stream_in;
|
||||
WaveOutEvent _wave_out = new WaveOutEvent();
|
||||
WaveFileReader _wave_reader;
|
||||
bool _is_playing, _is_play_requested;
|
||||
|
||||
public NAudioPlayer(Stream uwave_stream, int start_position = 0) {
|
||||
var ms = new MemoryStream();
|
||||
uwave_stream.CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
_wave_reader = new WaveFileReader(ms);
|
||||
/// <paramref name="stream_in"/> should not be disposed by the caller,
|
||||
/// as NAudioPlayer handles that in its own Dispose method;
|
||||
/// this constructor is private is to prevent such misuse.
|
||||
NAudioPlayer(Stream stream_in, int start_position = 0) {
|
||||
_stream_in = stream_in;
|
||||
_wave_reader = new WaveFileReader(stream_in);
|
||||
_wave_reader.Position = _start_position = (start_position < _wave_reader.Length ? start_position : 0);
|
||||
_wave_out.Init(_wave_reader);
|
||||
_wave_out.PlaybackStopped += _on_playback_stopped;
|
||||
}
|
||||
|
||||
public static NAudioPlayer create(SoundNotification.SystemSound system_sound) {
|
||||
var stream = _get_stream(system_sound);
|
||||
var sb = EnumHelper.get_attribute<SoundNotification.SystemSound, SilenceBufferAttribute>(system_sound);
|
||||
return sb != null ? new NAudioPlayer(stream, sb.start_position) : new NAudioPlayer(stream);
|
||||
/// <see cref="_playback_stopped"/> is a proxy/buffer for the _wave_out.PlaybackStopped event.
|
||||
/// It serves a couple purposes:
|
||||
/// 1) The main callback (<see cref="_on_playback_stopped(object, StoppedEventArgs)"/>)
|
||||
/// is guarenteed to be executed first (though it would be anyways, since that
|
||||
/// callback is hooked up in the constructor, the proxy event makes it obvious).
|
||||
/// 2) <see cref="_playback_stopped"/> is only raised after playback is truly complete,
|
||||
/// meaning there are no pending requests to repeat the sound.
|
||||
event Action _playback_stopped;
|
||||
|
||||
public static bool try_create(SoundNotification.SystemSound system_sound, out NAudioPlayer result) {
|
||||
if (_try_get_stream(system_sound, out var stream)) {
|
||||
var sb = EnumHelper.get_attribute<SoundNotification.SystemSound, SilenceBufferAttribute>(system_sound);
|
||||
result = sb != null ? new NAudioPlayer(stream, sb.start_position) : new NAudioPlayer(stream);
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void play(SoundNotification.SystemSound system_sound) {
|
||||
if (try_create(system_sound, out var player)) {
|
||||
try {
|
||||
var temp = player;
|
||||
temp._playback_stopped += temp.Dispose;
|
||||
player.play();
|
||||
player = null;
|
||||
} catch (Exception ex) {
|
||||
Log.Ui.warn($"Encountered exception playing the system sound '{system_sound}': {ex}.");
|
||||
} finally {
|
||||
player?.Dispose();
|
||||
}
|
||||
} else {
|
||||
Log.Ui.warn($"Unable to play the system sound '{system_sound}': unexpected sound type (did not find an audio stream that corresponds to this enum value).");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (_wave_out != null) {
|
||||
_wave_out.PlaybackStopped -= _on_playback_stopped;
|
||||
_wave_out.Dispose();
|
||||
_wave_out = null;
|
||||
}
|
||||
Disposable.dispose(ref _wave_reader);
|
||||
Disposable.dispose(ref _stream_in);
|
||||
}
|
||||
|
||||
public void play() {
|
||||
@@ -46,6 +89,8 @@ namespace mbts.ui {
|
||||
if (_is_play_requested) {
|
||||
_is_play_requested = false;
|
||||
_play_core();
|
||||
} else {
|
||||
_playback_stopped?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,18 +101,26 @@ namespace mbts.ui {
|
||||
_is_playing = false;
|
||||
}
|
||||
|
||||
static Stream _get_stream(SoundNotification.SystemSound system_sound) {
|
||||
static bool _try_get_stream(SoundNotification.SystemSound system_sound, out Stream stream) {
|
||||
UnmanagedMemoryStream um_stream;
|
||||
switch (system_sound) {
|
||||
case SoundNotification.SystemSound.GameCorrect: return bts.Properties.Resources.game_sound_correct;
|
||||
case SoundNotification.SystemSound.GameIncorrect: return bts.Properties.Resources.game_sound_wrong;
|
||||
case SoundNotification.SystemSound.GameOverArcade: return bts.Properties.Resources.game_over_arcade;
|
||||
case SoundNotification.SystemSound.Whistle: return bts.Properties.Resources.whistle;
|
||||
case SoundNotification.SystemSound.BellCounter: return bts.Properties.Resources.bell_counter;
|
||||
case SoundNotification.SystemSound.Powerup: return bts.Properties.Resources.powerup;
|
||||
case SoundNotification.SystemSound.CashRegister: return bts.Properties.Resources.cash_register_purchase;
|
||||
case SoundNotification.SystemSound.Reload: return bts.Properties.Resources.reload_1911;
|
||||
default: throw new NotImplementedException($"Unknown {nameof(SoundNotification.SystemSound)}: {system_sound}.");
|
||||
case SoundNotification.SystemSound.GameCorrect: um_stream = bts.Properties.Resources.game_sound_correct; break;
|
||||
case SoundNotification.SystemSound.GameIncorrect: um_stream = bts.Properties.Resources.game_sound_wrong; break;
|
||||
case SoundNotification.SystemSound.GameOverArcade: um_stream = bts.Properties.Resources.game_over_arcade; break;
|
||||
case SoundNotification.SystemSound.Whistle: um_stream = bts.Properties.Resources.whistle; break;
|
||||
case SoundNotification.SystemSound.BellCounter: um_stream = bts.Properties.Resources.bell_counter; break;
|
||||
case SoundNotification.SystemSound.Powerup: um_stream = bts.Properties.Resources.powerup; break;
|
||||
case SoundNotification.SystemSound.CashRegister: um_stream = bts.Properties.Resources.cash_register_purchase; break;
|
||||
case SoundNotification.SystemSound.Reload: um_stream = bts.Properties.Resources.reload_1911; break;
|
||||
default:
|
||||
stream = null;
|
||||
return false;
|
||||
}
|
||||
stream = new MemoryStream();
|
||||
um_stream.CopyTo(stream);
|
||||
um_stream.Dispose();
|
||||
stream.Position = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace mbts.spark {
|
||||
class AlgoParamVM<T> : IAlgoParamVM<T>, INotifyPropertyChanged, ITemplateSelectorKey {
|
||||
readonly IEqualityComparer<T> _comparer;
|
||||
readonly string _template_key;
|
||||
readonly Func<T, T> _to_display_value, _from_display_value;
|
||||
AlgoParamPermissions _permissions;
|
||||
bool _is_enabled, _has_value;
|
||||
T _committed_value, _value;
|
||||
@@ -20,13 +21,15 @@ namespace mbts.spark {
|
||||
public AlgoParamVM(string id, string display_name, T value, string template_key = null)
|
||||
: this(id, display_name, value, EqualityComparer<T>.Default, template_key) { }
|
||||
|
||||
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer, string template_key = null) {
|
||||
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer, string template_key = null, Func<T, T> to_display_value = null, Func<T, T> from_display_value = null) {
|
||||
this.id = id;
|
||||
this.display_name = display_name;
|
||||
_value = value;
|
||||
_committed_value = value;
|
||||
_comparer = comparer;
|
||||
_template_key = template_key;
|
||||
_to_display_value = to_display_value;
|
||||
_from_display_value = from_display_value;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
@@ -43,6 +46,10 @@ namespace mbts.spark {
|
||||
_toggle_is_dirty();
|
||||
}
|
||||
}
|
||||
public T display_value {
|
||||
get => _to_display_value == null ? value : _to_display_value(value);
|
||||
set => this.value = _from_display_value == null ? value : _from_display_value(value);
|
||||
}
|
||||
public AlgoParamPermissions permissions {
|
||||
get => _permissions;
|
||||
set {
|
||||
@@ -54,6 +61,7 @@ namespace mbts.spark {
|
||||
public bool has_value { get => _has_value; set => _set(ref _has_value, value); }
|
||||
public bool is_dirty { get; private set; }
|
||||
object IAlgoParamVM.value { get => value; set => this.value = (T)value; }
|
||||
object IAlgoParamVM.display_value { get => display_value; set => display_value = (T)value; }
|
||||
|
||||
string ITemplateSelectorKey.key => _template_key;
|
||||
|
||||
@@ -64,6 +72,9 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public void refresh_display_value() =>
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(display_value)));
|
||||
|
||||
public IObservable<T> subscribe_value() {
|
||||
return Observable.Create<T>(o => {
|
||||
o.OnNext(_value);
|
||||
@@ -113,6 +124,7 @@ namespace mbts.spark {
|
||||
value_changed(_value, _value = new_value);
|
||||
}
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(value)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(display_value)));
|
||||
}
|
||||
|
||||
void _toggle_is_dirty() {
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace mbts.spark {
|
||||
readonly Property<TimeInForce> _current_time_in_force;
|
||||
readonly Property<CustomOrderDefinition.CustomOneofCase> _current_order_type;
|
||||
readonly Property<bool> _price_dirty, _size_dirty, _account_dirty, _time_in_force_dirty, _order_type_dirty;
|
||||
uint _display_quantity_divisor = 1;
|
||||
|
||||
public ExistingOrderTicketVM(OrdersModel model, IObservable<(SecurityMD? security_md, OrderRecordImmutable order)> order_source, IObservable<int[]> calculator_buttons) {
|
||||
uint? key = null;
|
||||
@@ -40,7 +41,7 @@ namespace mbts.spark {
|
||||
(val, curr) => !double.IsNaN(val) && val != curr));
|
||||
_size_dirty = backing(
|
||||
nameof(size_dirty),
|
||||
size_calculator.observe_current(true)
|
||||
size_calculator.observe_actual_value()
|
||||
.CombineLatest(_current_size.observe_current(true),
|
||||
(val, curr) => val.HasValue && val.Value != curr));
|
||||
_account_dirty = backing(
|
||||
@@ -83,14 +84,17 @@ namespace mbts.spark {
|
||||
_dispose = Disposable.Create(
|
||||
size_calculator,
|
||||
properties_vm,
|
||||
security_md_source.OnDispatcher().Subscribe(smd => bbo.security_md = smd),
|
||||
security_md_source.OnDispatcher().Subscribe(smd => {
|
||||
bbo.security_md = smd;
|
||||
display_quantity_divisor = model.security_auxiliary_model.get_display_quantity_divisor(smd.security);
|
||||
}),
|
||||
bbo,
|
||||
order_source.OnDispatcher().Subscribe(ort => {
|
||||
key = ort.order.Key;
|
||||
if (DoubleHelper.IsNaNOrInfinity(price))
|
||||
price = ort.order.State.Price;
|
||||
if (!size_calculator.value.HasValue || !size_calculator.value_setter_called)
|
||||
size_calculator.bypass_value_setter(ort.order.State.Quantity);
|
||||
size_calculator.bypass_value_setter(ort.order.State.Quantity, true);
|
||||
}),
|
||||
Observable.Merge(_price_dirty, _size_dirty, _account_dirty, _time_in_force_dirty, _order_type_dirty).OnDispatcher().Subscribe(_ => RaisePropertyChanged(nameof(dirty_items_text))),
|
||||
_price_dirty.observe_current(true).CombineLatest(_size_dirty.observe_current(true), (pd, sd) => pd || sd).DistinctUntilChanged().OnDispatcher().Subscribe(modify_order));
|
||||
@@ -104,6 +108,13 @@ namespace mbts.spark {
|
||||
public OrderPropertiesEditorVM properties_vm { get; }
|
||||
public double price { get => _price.Value; set => _price.Value = value; }
|
||||
public CalculatorLogic size_calculator { get; }
|
||||
public uint display_quantity_divisor {
|
||||
get => _display_quantity_divisor;
|
||||
private set {
|
||||
Set(ref _display_quantity_divisor, value);
|
||||
size_calculator.display_divisor = value;
|
||||
}
|
||||
}
|
||||
public double current_price => _current_price.Value;
|
||||
public uint current_size => _current_size.Value;
|
||||
public AccountTuple current_account => _current_account.Value;
|
||||
@@ -137,4 +148,4 @@ namespace mbts.spark {
|
||||
if (_order_type_dirty.Value) yield return "type";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ namespace mbts.spark {
|
||||
|
||||
class NewOrderTicketVM : DisposableViewModelBase, IRequestViewClose {
|
||||
readonly Property<SecurityImmutable> _security;
|
||||
uint _display_quantity_divisor = 1;
|
||||
|
||||
public NewOrderTicketVM(OrdersModel model, SecurityMD security_md, OrderDefinitionImmutable def, double price, uint size, IObservable<int[]> calculator_buttons)
|
||||
: this(model, Observable.Return(security_md), Observable.Return(new InlineOrderTicketProperties(model, def, price, size)), calculator_buttons) { }
|
||||
@@ -61,7 +62,10 @@ namespace mbts.spark {
|
||||
set_price_command = new DelegateCommand<double>(set_price);
|
||||
_dispose = Disposable.Create(
|
||||
bbo,
|
||||
security_md_source.OnDispatcher().Subscribe(smd => bbo.security_md = smd),
|
||||
security_md_source.OnDispatcher().Subscribe(smd => {
|
||||
bbo.security_md = smd;
|
||||
display_quantity_divisor = model.security_auxiliary_model.get_display_quantity_divisor(smd.security);
|
||||
}),
|
||||
size_calculator = new CalculatorLogic(0, calculator_buttons),
|
||||
props_source.OnDispatcher().Subscribe(p => on_props(p.price, p.size))
|
||||
);
|
||||
@@ -83,6 +87,13 @@ namespace mbts.spark {
|
||||
public SecurityImmutable security => _security.Value;
|
||||
public double price { get; set; }
|
||||
public CalculatorLogic size_calculator { get; }
|
||||
public uint display_quantity_divisor {
|
||||
get => _display_quantity_divisor;
|
||||
private set {
|
||||
Set(ref _display_quantity_divisor, value);
|
||||
size_calculator.display_divisor = value;
|
||||
}
|
||||
}
|
||||
public OrderPropertiesEditorVM properties_vm { get; }
|
||||
public BBOVM bbo { get; }
|
||||
public bool close_on_send { get; set; }
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
usp:OrderModificationControl.IsDirty="{Binding size_dirty, FallbackValue={x:Null}}"
|
||||
CommitOnEnter="True"
|
||||
Focusable="False"
|
||||
Value="{Binding size_calculator.value}"
|
||||
Value="{Binding size_calculator.display_value}"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid Grid.Column="2">
|
||||
@@ -114,15 +114,26 @@
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Content="{Binding bbo.bid_size}" Command="{Binding size_calculator.set_value_command}"
|
||||
CommandParameter="{Binding bbo.bid_size}" Foreground="{u:BTSColor MarketData.bid}"
|
||||
/>
|
||||
Command="{Binding size_calculator.set_actual_value_command}"
|
||||
CommandParameter="{Binding bbo.bid_size}"
|
||||
Foreground="{u:BTSColor MarketData.bid}"
|
||||
>
|
||||
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}">
|
||||
<Binding Path="bbo.bid_size"/>
|
||||
<Binding Path="display_quantity_divisor"/>
|
||||
</MultiBinding>
|
||||
</Button>
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Content="{Binding bbo.ask_size}" Command="{Binding size_calculator.set_value_command}"
|
||||
Command="{Binding size_calculator.set_actual_value_command}"
|
||||
CommandParameter="{Binding bbo.ask_size}"
|
||||
Foreground="{u:BTSColor MarketData.ask}"
|
||||
/>
|
||||
>
|
||||
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}">
|
||||
<Binding Path="bbo.ask_size"/>
|
||||
<Binding Path="display_quantity_divisor"/>
|
||||
</MultiBinding>
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid Grid.Row="4" Grid.RowSpan="3">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -139,9 +150,18 @@
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Button
|
||||
Margin="2,4,2,0" Content="{Binding}" Focusable="False" CommandParameter="{Binding}"
|
||||
Margin="2,4,2,0"
|
||||
Content="{Binding}"
|
||||
Focusable="False"
|
||||
Command="{Binding Path=DataContext.click, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
|
||||
/>
|
||||
>
|
||||
<Button.CommandParameter>
|
||||
<MultiBinding Converter="{x:Static cv:DynamicDoubleMultiplierConverter.Instance}">
|
||||
<Binding/>
|
||||
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:OrderModificationControl}"/>
|
||||
</MultiBinding>
|
||||
</Button.CommandParameter>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<Window x:Class="mbts.ui.spark.OrderTicket"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:msys="clr-namespace:bts.api;assembly=spark_api"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:usp="clr-namespace:mbts.ui.spark"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
u:WindowStyle.SizeToContent="WidthAndHeight"
|
||||
u:WindowStyle.TitleContent="{Binding bbo, Mode=OneWay}"
|
||||
Style="{StaticResource BTSWindowTitleTemplate}">
|
||||
<Window
|
||||
x:Class="mbts.ui.spark.OrderTicket"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:msys="clr-namespace:bts.api;assembly=spark_api"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
xmlns:usp="clr-namespace:mbts.ui.spark"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
u:WindowStyle.SizeToContent="WidthAndHeight"
|
||||
u:WindowStyle.TitleContent="{Binding bbo, Mode=OneWay}"
|
||||
Style="{StaticResource BTSWindowTitleTemplate}"
|
||||
>
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
@@ -123,7 +126,12 @@
|
||||
<TextBlock>
|
||||
<TextBlock.Inlines>
|
||||
<Run Text="{Binding security, Mode=OneWay}" FontWeight="Bold"/>
|
||||
<Run Foreground="{u:BTSColor MarketData.bid}" FontSize="10" Text="{Binding bid_size, Mode=OneWay, StringFormat='{}({0})'}"/>
|
||||
<Run Foreground="{u:BTSColor MarketData.bid}" FontSize="10">
|
||||
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}" Mode="OneWay" StringFormat="({0})">
|
||||
<Binding Path="bid_size" Mode="OneWay"/>
|
||||
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:OrderTicket}"/>
|
||||
</MultiBinding>
|
||||
</Run>
|
||||
<Run Foreground="{u:BTSColor MarketData.bid}" FontWeight="Bold">
|
||||
<Run.Text>
|
||||
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
|
||||
@@ -140,7 +148,12 @@
|
||||
</MultiBinding>
|
||||
</Run.Text>
|
||||
</Run>
|
||||
<Run Foreground="{u:BTSColor MarketData.ask}" FontSize="10" Text="{Binding ask_size, Mode=OneWay, StringFormat='{}({0})'}"/>
|
||||
<Run Foreground="{u:BTSColor MarketData.ask}" FontSize="10">
|
||||
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}" Mode="OneWay" StringFormat="({0})">
|
||||
<Binding Path="ask_size" Mode="OneWay"/>
|
||||
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:OrderTicket}"/>
|
||||
</MultiBinding>
|
||||
</Run>
|
||||
</TextBlock.Inlines>
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -23,6 +23,32 @@ namespace mbts.spark {
|
||||
|
||||
public IEnumerable<string> dynamic_types => _dynamic_types.DynamicOrderDesc.Select(d => d.DynamicOrderType);
|
||||
|
||||
public static string fmt_param(DynamicOrderParam dyn_param) {
|
||||
if (dyn_param == null)
|
||||
return null;
|
||||
switch (dyn_param.ValueCase) {
|
||||
case DynamicOrderParam.ValueOneofCase.AsArray:
|
||||
return string.Join(", ", dyn_param.AsArray.Element.Select(fmt_param));
|
||||
case DynamicOrderParam.ValueOneofCase.AsBool:
|
||||
return dyn_param.AsBool.ToString();
|
||||
case DynamicOrderParam.ValueOneofCase.AsDouble:
|
||||
return dyn_param.AsDouble.ToString();
|
||||
case DynamicOrderParam.ValueOneofCase.AsEnumerated:
|
||||
return dyn_param.AsEnumerated.EnumValue;
|
||||
case DynamicOrderParam.ValueOneofCase.AsInt32:
|
||||
return dyn_param.AsInt32.ToString();
|
||||
case DynamicOrderParam.ValueOneofCase.AsQuantity:
|
||||
return dyn_param.AsQuantity.ToString();
|
||||
case DynamicOrderParam.ValueOneofCase.AsString:
|
||||
return dyn_param.AsString;
|
||||
default:
|
||||
// implement additional cases as needed
|
||||
// (as of 4/17/20, this method is only used to format
|
||||
// dynamic orders' CustomState params in the orderbook)
|
||||
return $"({dyn_param.ValueCase})";
|
||||
}
|
||||
}
|
||||
|
||||
public void add_dynamic_order_defs(List<CustomOrderDefinitionImmutable> defs, Func<CustomOrderDefinition.CustomOneofCase, CustomOrderDefinition> factory) {
|
||||
foreach (var d in _dynamic_types.DynamicOrderDesc) {
|
||||
var dorder = factory(CustomOrderDefinition.CustomOneofCase.Dynamic);
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace mbts.spark
|
||||
{
|
||||
return;
|
||||
}
|
||||
var ev = fd.EnumType.Values.First(x => x.Name == value)?.Number;
|
||||
var ev = fd.EnumType.Values.FirstOrDefault(x => x.Name == value)?.Number;
|
||||
if(ev.HasValue)
|
||||
fd.Accessor.SetValue(message, ev.Value);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.spark;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.Reflection;
|
||||
using mbts.collections;
|
||||
@@ -15,21 +14,19 @@ using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reflection;
|
||||
using AlgoType = bts.api.orderman.CustomOrderDefinition.CustomOneofCase;
|
||||
|
||||
namespace mbts.spark {
|
||||
static partial class OrderTypeVMFactory
|
||||
{
|
||||
internal class GenericOrderFactory
|
||||
{
|
||||
static public IAlgoVM create_vm(AlgoKey key, AlgoType algo_type, string display_name, AlgoEditingPermissions editing_permissions)
|
||||
{
|
||||
static partial class OrderTypeVMFactory {
|
||||
internal class GenericOrderFactory {
|
||||
static public IAlgoVM create_vm(AlgoKey key, AlgoType algo_type, string display_name, AlgoEditingPermissions editing_permissions) {
|
||||
var prop = typeof(CustomOrderDefinition).GetProperty(algo_type.ToString(),
|
||||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
|
||||
BindingFlags.Instance | BindingFlags.Public);
|
||||
var rt = prop.PropertyType;
|
||||
|
||||
var propi = typeof(CustomOrderDefinitionImmutable).GetProperty(algo_type.ToString(),
|
||||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
|
||||
BindingFlags.Instance | BindingFlags.Public);
|
||||
var rti = propi.PropertyType;
|
||||
|
||||
var vmt = typeof(GenericOrderVM<,>);
|
||||
@@ -38,38 +35,33 @@ namespace mbts.spark {
|
||||
object vm = Activator.CreateInstance(makevm, key, algo_type, display_name, editing_permissions);
|
||||
return (IAlgoVM)vm;
|
||||
}
|
||||
static public void setup_generic_custom_order(CustomOrderDefinition custom, CustomOrderDefinition.CustomOneofCase type)
|
||||
{
|
||||
static public void setup_generic_custom_order(CustomOrderDefinition custom, CustomOrderDefinition.CustomOneofCase type) {
|
||||
var prop = typeof(CustomOrderDefinition).GetProperty(type.ToString(),
|
||||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
|
||||
BindingFlags.Instance | BindingFlags.Public);
|
||||
var rt = prop.PropertyType;
|
||||
var rtt = Activator.CreateInstance(rt);
|
||||
|
||||
|
||||
var ci = rt.GetProperties().Where(x => x.PropertyType == typeof(CustomOrderDefinition));
|
||||
foreach(var cid in ci)
|
||||
foreach (var cid in ci)
|
||||
cid.SetValue(rtt, new CustomOrderDefinition { Limit = new LimitOrderDefinition() });
|
||||
prop.SetValue(custom, rtt);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class GenericOrderVM<TMutable, TImmutable> : OrderTypeVMBase<TMutable, TImmutable>
|
||||
where TMutable : class, IMessage, new() where TImmutable : struct, IImmutableMessage
|
||||
{
|
||||
where TMutable : class, IMessage, new() where TImmutable : struct, IImmutableMessage {
|
||||
readonly ObservableKeyedCollection<GenericOrderParamVM<TMutable>, string> _parameters =
|
||||
new ObservableKeyedCollection<GenericOrderParamVM<TMutable>, string>(vm => vm.descriptor.Name);
|
||||
readonly AlgoType _algo_type;
|
||||
|
||||
public GenericOrderVM(AlgoKey key, AlgoType algo_type, string display_name, AlgoEditingPermissions editing_permissions)
|
||||
: base(key, display_name, editing_permissions)
|
||||
{
|
||||
: base(key, display_name, editing_permissions) {
|
||||
_algo_type = algo_type;
|
||||
_parameters = new ObservableKeyedCollection<GenericOrderParamVM<TMutable>, string>(vm => vm.descriptor.Name);
|
||||
_resources.AddRange(
|
||||
ObservableAutoWrapper.connect_item_hooks(_parameters, p =>
|
||||
{
|
||||
ObservableAutoWrapper.connect_item_hooks(_parameters, p => {
|
||||
p.PropertyChanged += on_param_prop_changed;
|
||||
return Disposable.Create(() =>
|
||||
{
|
||||
return Disposable.Create(() => {
|
||||
p.PropertyChanged -= on_param_prop_changed;
|
||||
remove_property(p.descriptor.Name);
|
||||
p.Dispose();
|
||||
@@ -80,84 +72,74 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public string algo_type => _algo_type.ToString();
|
||||
public override string abbreviation =>
|
||||
public override string abbreviation =>
|
||||
model == null ? display_name : model.factory.abbreviate_algo(new OneOf<CustomOrderDefinition.CustomOneofCase, string>(_algo_type));
|
||||
public override AlgoDisplayGroup group => AlgoDisplayGroup.API;
|
||||
public ObservableCollection<GenericOrderParamVM<TMutable>> parameters => _parameters;
|
||||
|
||||
protected override void on_model_internal()
|
||||
{
|
||||
protected override void on_model_internal() =>
|
||||
_sync_generic_params();
|
||||
}
|
||||
|
||||
void _sync_generic_params()
|
||||
{
|
||||
if (model != null)
|
||||
{
|
||||
Google.Protobuf.Reflection.MessageDescriptor desc = new TMutable().Descriptor;
|
||||
void _sync_generic_params() {
|
||||
if (model != null) {
|
||||
MessageDescriptor desc = new TMutable().Descriptor;
|
||||
_parameters.SyncWithDictionary(
|
||||
desc.Fields.InDeclarationOrder().Where(_is_value_param_desc).ToDictionary(p => p.Name), _mk_param_vm);
|
||||
}
|
||||
}
|
||||
static bool _is_value_param_desc(FieldDescriptor desc)
|
||||
{
|
||||
return (desc.FieldType != Google.Protobuf.Reflection.FieldType.Message ||
|
||||
|
||||
static bool _is_value_param_desc(FieldDescriptor desc) =>
|
||||
(desc.FieldType != FieldType.Message ||
|
||||
_is_quantity_measure_desc(desc));
|
||||
}
|
||||
|
||||
static bool _is_quantity_measure_desc(FieldDescriptor desc)
|
||||
{
|
||||
return desc.FieldType == Google.Protobuf.Reflection.FieldType.Message &&
|
||||
static bool _is_quantity_measure_desc(FieldDescriptor desc) =>
|
||||
desc.FieldType == FieldType.Message &&
|
||||
desc.MessageType.FullName == typeof(bts.spark.QuantityMeasure).FullName;
|
||||
}
|
||||
|
||||
protected override IEnumerable<ChildAlgoData> get_selector_data()
|
||||
{
|
||||
var v = OrderDataHelper.get_child_algo_data(_algo_type);
|
||||
return v;
|
||||
}
|
||||
protected override IEnumerable<ChildAlgoData> get_selector_data() =>
|
||||
OrderDataHelper.get_child_algo_data(_algo_type);
|
||||
|
||||
GenericOrderParamVM<TMutable> _mk_param_vm(FieldDescriptor descriptor)
|
||||
{
|
||||
GenericOrderParamVM<TMutable> _mk_param_vm(FieldDescriptor descriptor) {
|
||||
ExceptionAssert.Argument.NotNull(descriptor, nameof(descriptor));
|
||||
|
||||
if (descriptor.FieldType == FieldType.Message &&
|
||||
descriptor.MessageType.FullName == typeof(bts.spark.QuantityMeasure).FullName)
|
||||
{
|
||||
if (descriptor.FieldType == FieldType.Message &&
|
||||
descriptor.MessageType.FullName == typeof(bts.spark.QuantityMeasure).FullName) {
|
||||
var qm = new GenericOrderQuantityVM<TMutable>(descriptor);
|
||||
return attached(qm, GenericOrderHelper.get_quantity_value, GenericOrderHelper.set_quantity_value);
|
||||
}
|
||||
else if(descriptor.FieldType == FieldType.Enum)
|
||||
{
|
||||
var pvm = new GenericOrderEnumeratedVM<TMutable>(descriptor);
|
||||
attached(pvm, GenericOrderHelper.get_enumerated_value, GenericOrderHelper.set_enumerated_value);
|
||||
return pvm;
|
||||
}
|
||||
else if (descriptor.FieldType == FieldType.Bool)
|
||||
} else if (descriptor.FieldType == FieldType.Enum) {
|
||||
return EnumParamFactory.create(this, descriptor);
|
||||
} else if (descriptor.FieldType == FieldType.Bool) {
|
||||
return mk<bool?, bool>(descriptor, v => v, v => v.GetValueOrDefault());
|
||||
else if (descriptor.FieldType == FieldType.Double || descriptor.FieldType == FieldType.Float)
|
||||
} else if (descriptor.FieldType == FieldType.Double || descriptor.FieldType == FieldType.Float) {
|
||||
return mk<double, double>(descriptor, v => v, v => v, double.NaN);
|
||||
else if (descriptor.FieldType == FieldType.Int32)
|
||||
} else if (descriptor.FieldType == FieldType.Int32) {
|
||||
return mk<int?, int>(descriptor, v => v, v => v.GetValueOrDefault());
|
||||
else if (descriptor.FieldType == FieldType.UInt32)
|
||||
} else if (descriptor.FieldType == FieldType.UInt32) {
|
||||
return mk<uint?, uint>(descriptor, v => v, v => v.GetValueOrDefault());
|
||||
else if (descriptor.FieldType == FieldType.String)
|
||||
} else if (descriptor.FieldType == FieldType.String) {
|
||||
return mk<string, string>(descriptor, v => v, v => v);
|
||||
else
|
||||
return null;
|
||||
} else return null;
|
||||
}
|
||||
GenericOrderParamVM<TMutable, TView, TModel> mk<TView, TModel>(FieldDescriptor fdesc, Func<TModel, TView> convert, Func<TView, TModel> convert_back, TView default_value = default)
|
||||
{
|
||||
var pvm = new GenericOrderParamVM<TMutable, TView, TModel>(fdesc, convert, convert_back);
|
||||
return attached<TView, GenericOrderParamVM<TMutable, TView, TModel>>(pvm, get_from_param, assign_to_param, default_value);
|
||||
|
||||
GenericOrderParamVM<TMutable, TView, TModel> mk<TView, TModel>(
|
||||
FieldDescriptor fdesc,
|
||||
Func<TModel, TView> convert,
|
||||
Func<TView, TModel> convert_back,
|
||||
TView default_value = default) {
|
||||
//
|
||||
var pvm = new GenericOrderParamVM<TMutable, TView, TModel>(fdesc, convert, convert_back);
|
||||
return attached(pvm, get_from_param, assign_to_param, default_value);
|
||||
TView get_from_param(IMessage param, FieldDescriptor fd) => convert(ConvertUtils.try_convert<TModel>(fdesc.Accessor.GetValue(param)));
|
||||
void assign_to_param(IMessage param, FieldDescriptor fd, TView value) => fdesc.Accessor.SetValue(param, convert_back(value));
|
||||
}
|
||||
|
||||
TParam attached<T, TParam>(TParam pvm, Func<IMessage, FieldDescriptor, T> selector, Action<IMessage, FieldDescriptor, T> assign_to_param, T default_value = default) where TParam : GenericOrderParamVM<TMutable>, IGenericOrderParamVM<T>
|
||||
{
|
||||
add_property<T>(
|
||||
TParam attached<T, TParam>(
|
||||
TParam pvm,
|
||||
Func<IMessage, FieldDescriptor, T>
|
||||
selector, Action<IMessage, FieldDescriptor, T> assign_to_param,
|
||||
T default_value = default)
|
||||
where TParam : GenericOrderParamVM<TMutable>, IGenericOrderParamVM<T> {
|
||||
//
|
||||
add_property(
|
||||
pvm.descriptor.Name,
|
||||
mdef => mdef != null ? selector(mdef, pvm.descriptor) : default_value,
|
||||
(mdef, v) => assign_to_param(mdef, pvm.descriptor, v),
|
||||
@@ -166,7 +148,37 @@ namespace mbts.spark {
|
||||
pvm.subscribe_value(),
|
||||
default_value);
|
||||
return pvm;
|
||||
}
|
||||
}
|
||||
|
||||
static class EnumParamFactory {
|
||||
[ThreadStatic]
|
||||
static Dictionary<Type, IFactory> _factories_backing;
|
||||
static Dictionary<Type, IFactory> _factories =>
|
||||
_factories_backing ?? (_factories_backing = new Dictionary<Type, IFactory>());
|
||||
|
||||
public static GenericOrderParamVM<TMutable> create(GenericOrderVM<TMutable, TImmutable> owner, FieldDescriptor descriptor) {
|
||||
if (descriptor.FieldType != FieldType.Enum)
|
||||
throw ExceptionBuilder.Argument.unexpected_case(descriptor.FieldType, $"{nameof(descriptor)}.{nameof(FieldDescriptor.FieldType)}");
|
||||
if (!_factories.TryGetValue(descriptor.EnumType.ClrType, out var f)) {
|
||||
// Impl<T> is a nested type, so parent class's generic type args must be included.
|
||||
_factories[descriptor.EnumType.ClrType] = f = (IFactory)Activator.CreateInstance(typeof(Impl<>)
|
||||
.MakeGenericType(typeof(TMutable), typeof(TImmutable), descriptor.EnumType.ClrType));
|
||||
}
|
||||
return f.create(owner, descriptor);
|
||||
}
|
||||
|
||||
interface IFactory {
|
||||
GenericOrderParamVM<TMutable> create(GenericOrderVM<TMutable, TImmutable> owner, FieldDescriptor descriptor);
|
||||
}
|
||||
|
||||
class Impl<T> : IFactory where T : struct {
|
||||
static readonly Func<T, T?> _convert = v => v;
|
||||
static readonly Func<T?, T> _convert_back = v => v ?? default;
|
||||
|
||||
public GenericOrderParamVM<TMutable> create(GenericOrderVM<TMutable, TImmutable> owner, FieldDescriptor descriptor) =>
|
||||
owner.mk(descriptor, _convert, _convert_back, default);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ namespace mbts.spark {
|
||||
string id { get; }
|
||||
string display_name { get; }
|
||||
object value { get; set; }
|
||||
object display_value { get; set; }
|
||||
bool is_enabled { get; set; }
|
||||
bool is_dirty { get; }
|
||||
bool has_value { get; }
|
||||
@@ -81,6 +82,7 @@ namespace mbts.spark {
|
||||
interface IAlgoParamVM<T> : IAlgoParamVM {
|
||||
event PropertyChange<T> value_changed;
|
||||
new T value { get; set; }
|
||||
new T display_value { get; set; }
|
||||
}
|
||||
|
||||
interface IAlgoSelector : IAlgoParamVM<IAlgoVM>, IObserver<(IReadOnlyDictionary<AlgoKey, AlgoData> algo_data, AlgoKey? default_algo)> {
|
||||
|
||||
@@ -985,8 +985,10 @@ namespace mbts.spark {
|
||||
|
||||
#region IAlgoParamVM
|
||||
IAlgoVM IAlgoParamVM<IAlgoVM>.value { get => selected_algo; set => selected_algo = value; }
|
||||
IAlgoVM IAlgoParamVM<IAlgoVM>.display_value { get => selected_algo; set => selected_algo = value; }
|
||||
event PropertyChange<IAlgoVM> IAlgoParamVM<IAlgoVM>.value_changed { add => selected_algo_changed += value; remove => selected_algo_changed -= value; }
|
||||
object IAlgoParamVM.value { get => selected_algo; set => selected_algo = (IAlgoVM)value; }
|
||||
object IAlgoParamVM.display_value { get => selected_algo; set => selected_algo = (IAlgoVM)value; }
|
||||
bool IAlgoParamVM.has_value => _algo != null;
|
||||
// changing child algos is not allowed, so an algo selector is never dirty
|
||||
bool IAlgoParamVM.is_dirty => false;
|
||||
|
||||
@@ -63,6 +63,11 @@ namespace mbts.spark {
|
||||
public static string last_msg(this OrderRecordImmutable order) =>
|
||||
order.HasState ? order.State.LastMsg : string.Empty;
|
||||
|
||||
public static IReadOnlyDictionary<string, DynamicOrderParam> dynamic_custom_state(this OrderRecordImmutable order) =>
|
||||
order.HasState && order.State.HasDynamic ?
|
||||
(order.State.Dynamic.CustomState ?? DictionaryHelper.empty<string, DynamicOrderParam>()) :
|
||||
DictionaryHelper.empty<string, DynamicOrderParam>();
|
||||
|
||||
public static DateTime created(this OrderRecordImmutable order) =>
|
||||
order.HasState && order.State.HasCreated ? order.State.Created.ToDateTime().ToLocalTime() : DateTime.MinValue;
|
||||
|
||||
@@ -235,8 +240,8 @@ namespace mbts.spark {
|
||||
public static string price_quantity_description(double price, uint quantity, ContractSpecificationImmutable spec) =>
|
||||
new StringBuilderEx(quantity.ToString(), " @ ", spec.format(price) ?? price.ToString("#0.00##")).ToString();
|
||||
|
||||
public static string filled_status_description(this OrderRecordImmutable order, ContractSpecificationImmutable spec) {
|
||||
var sb = new StringBuilderEx("filled ", order.filled().ToString());
|
||||
public static string filled_status_description(this OrderRecordImmutable order, ContractSpecificationImmutable spec, uint display_qty_divisor = 1) {
|
||||
var sb = new StringBuilderEx("filled ", order.filled().safe_divide(display_qty_divisor).ToString());
|
||||
if (order.filled() > 0)
|
||||
sb.Add(" @ ", spec.format(order.avg_price()));
|
||||
sb.Add(", ", camel_to_title_case(order.status().ToString(), TextCapitalizationFormat.NoCaps));
|
||||
|
||||
@@ -10,6 +10,7 @@ using bts.spark;
|
||||
using bts.utils;
|
||||
using mbts.config;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -26,8 +27,9 @@ using OrderUpdatePiq = bts.api.core.TableUpdate<bts.api.orderman.OrderSecurity,
|
||||
namespace mbts.spark {
|
||||
[Export]
|
||||
class OrdersModel : IDisposable {
|
||||
public readonly MarketDataAuxiliaryModel md_auxiliary_model;
|
||||
public static readonly bool piq_enabled = Configuration.get<bts.config.toggles.PlacementInQueue>().enabled;
|
||||
public readonly MarketDataAuxiliaryModel md_auxiliary_model;
|
||||
public readonly ISecurityAuxiliaryDataModel security_auxiliary_model;
|
||||
|
||||
readonly IBus _bus;
|
||||
readonly OrderFactory _factory;
|
||||
@@ -44,8 +46,20 @@ namespace mbts.spark {
|
||||
IDisposable _dispose;
|
||||
|
||||
[ImportingConstructor]
|
||||
OrdersModel(IBus bus, OrderFactory factory, OrderService service, PositionService position_service, MBOService mbo_service,
|
||||
SpreaderModel spreader, DynamicOrderHelper dynamic_helper, AdminModel admin_model, IAlgoStudioModel algo_model, MarketDataAuxiliaryModel md_auxiliary_model, SecurityStore sstore, ErrorReporter error) {
|
||||
OrdersModel(IBus bus,
|
||||
OrderFactory factory,
|
||||
OrderService service,
|
||||
PositionService position_service,
|
||||
MBOService mbo_service,
|
||||
SpreaderModel spreader,
|
||||
DynamicOrderHelper dynamic_helper,
|
||||
AdminModel admin_model,
|
||||
IAlgoStudioModel algo_model,
|
||||
ISecurityAuxiliaryDataModel security_auxiliary_model,
|
||||
MarketDataAuxiliaryModel md_auxiliary_model,
|
||||
SecurityStore sstore,
|
||||
ErrorReporter error) {
|
||||
//
|
||||
_bus = bus;
|
||||
_factory = factory;
|
||||
_service = service;
|
||||
@@ -57,6 +71,7 @@ namespace mbts.spark {
|
||||
this.admin_model = admin_model;
|
||||
this.algo_model = algo_model;
|
||||
this.md_auxiliary_model = md_auxiliary_model;
|
||||
this.security_auxiliary_model = security_auxiliary_model;
|
||||
var scheduler = ThreadPoolScheduler.Instance;
|
||||
var interval = TimeSpan.FromMilliseconds(100);
|
||||
on_orders = admin_model
|
||||
@@ -80,7 +95,10 @@ namespace mbts.spark {
|
||||
_position_service.subscribei_pnl_data().Naggle(interval, scheduler),
|
||||
p => p.Pos.AccountId)
|
||||
.join(sstore, p => p.Pos.SecurityId)
|
||||
.TableCheckedAutoCorrect(EqualityComparer<(SecurityImmutable? sec, PNLUnitImmutable data)>.Default)
|
||||
.Select(
|
||||
tup => (tup.sec, tup.val, new QuantityConversionData(security_auxiliary_model.get_display_quantity_divisor(tup.sec), QuantityConversionData.OpType.DivideActual)),
|
||||
tup => tup.val.RecordId)
|
||||
.TableCheckedAutoCorrect(EqualityComparer<(SecurityImmutable?, PNLUnitImmutable, QuantityConversionData)>.Default)
|
||||
.Where(t => t.has_changes())
|
||||
.Cache();
|
||||
var working_orders = on_orders.Filter(tup => tup.order.is_active());
|
||||
@@ -167,8 +185,8 @@ namespace mbts.spark {
|
||||
working_account_order_groups,
|
||||
PNLRowData.key_fn,
|
||||
acc_orders => acc_orders.group_key,
|
||||
(pnl_row_data, acc_orders, pnl_key) => pnl_row_data.with(working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order))),
|
||||
(pnl_row_data, pnl_id) => pnl_row_data,
|
||||
(row, acc_orders, pnl_key) => row.with(working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order), row.qty_conversion)),
|
||||
(row, pnl_id) => row,
|
||||
(acc_orders, pnl_id) => {
|
||||
string account;
|
||||
SecurityImmutable? security;
|
||||
@@ -182,7 +200,9 @@ namespace mbts.spark {
|
||||
return new PNLRowData(pnl_id,
|
||||
security: security,
|
||||
account_name: account,
|
||||
working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order)));
|
||||
working: WorkingOrderData.Create(
|
||||
acc_orders.table.state.Values.Select(t => t.order),
|
||||
new QuantityConversionData(security_auxiliary_model.get_display_quantity_divisor(security), QuantityConversionData.OpType.DivideActual)));
|
||||
},
|
||||
PNLRowData.key_fn)
|
||||
.Where(t => t.has_changes())
|
||||
@@ -215,8 +235,11 @@ namespace mbts.spark {
|
||||
FillTuple.key_fn);
|
||||
on_fill_tuples_with_root = fill_order_tuples_with_root_join.Cache();
|
||||
// manual fills
|
||||
on_manual_fills_securities = _position_service.subscribei_manual_fills()
|
||||
.Naggle(interval, scheduler)
|
||||
on_manual_fills_securities = admin_model
|
||||
.permission_filter(
|
||||
_position_service.subscribei_manual_fills()
|
||||
.Naggle(interval, scheduler),
|
||||
f => f.AccountId)
|
||||
.Filter(f => f.FillType == ManualFill.Types.FillType.Local)
|
||||
.join(sstore, mf => mf.SecurityKey)
|
||||
.Table().Cache();
|
||||
@@ -279,7 +302,7 @@ namespace mbts.spark {
|
||||
public IObservable<TableUpdate<FillTuple, string>> on_fill_tuples { get; }
|
||||
public IObservable<TableUpdate<FillTuple, string>> on_fill_tuples_with_root { get; }
|
||||
public IObservable<IReadOnlyDictionary<uint, OrderSecurity>> working_orders { get; }
|
||||
public IObservable<TableUpdate<(SecurityImmutable?, PNLUnitImmutable), int>> on_pnl { get; }
|
||||
public IObservable<TableUpdate<(SecurityImmutable?, PNLUnitImmutable, QuantityConversionData), int>> on_pnl { get; }
|
||||
public IObservable<TableUpdate<PNLRowData, PNLKey>> security_account_pnl { get; }
|
||||
public IObservable<TableUpdate<(SecurityImmutable?, PositionUnitImmutable), int>> on_positions { get; }
|
||||
public IObservable<TableUpdate<(AlgoData algo, AlgoDefinitionMetadata? meta), AlgoKey>> studio_algos_with_meta { get; }
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using bts.api;
|
||||
using bts.utils;
|
||||
using mbts.config;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
|
||||
namespace mbts.spark {
|
||||
interface ISecurityAuxiliaryDataModel {
|
||||
SecurityAuxiliaryData? get_auxiliary_data(SecurityImmutable security);
|
||||
}
|
||||
|
||||
readonly struct SecurityAuxiliaryData {
|
||||
public SecurityAuxiliaryData(uint quantity_divisor) =>
|
||||
this.quantity_divisor = quantity_divisor;
|
||||
|
||||
public static SecurityAuxiliaryData Default => new SecurityAuxiliaryData(1);
|
||||
|
||||
public uint quantity_divisor { get; }
|
||||
}
|
||||
|
||||
[Export(typeof(ISecurityAuxiliaryDataModel))]
|
||||
class SecurityAuxiliaryDataModel : ISecurityAuxiliaryDataModel {
|
||||
readonly Dictionary<string, SecurityAuxiliaryData> _auxiliary_data = new Dictionary<string, SecurityAuxiliaryData>();
|
||||
|
||||
public SecurityAuxiliaryDataModel() {
|
||||
var sym_config = Configuration.get<bts.config.SymbolUIConfiguration>();
|
||||
var sym_config_items = Configuration.get<bts.config.SymbolUIConfiguration>().items;
|
||||
foreach (var item in sym_config_items)
|
||||
_auxiliary_data[item.symbol] = new SecurityAuxiliaryData(item.display_quantity_divisor);
|
||||
}
|
||||
|
||||
public SecurityAuxiliaryData? get_auxiliary_data(SecurityImmutable security) =>
|
||||
security.ToString().SafeContains("ICE Lots") ?
|
||||
new SecurityAuxiliaryData(1) :
|
||||
_auxiliary_data.TryGetValue(security.Symbol, out var d) ? d : (SecurityAuxiliaryData?)null;
|
||||
}
|
||||
|
||||
static class SecurityAuxiliaryDataModelExtensions {
|
||||
public static uint get_display_quantity(this ISecurityAuxiliaryDataModel model, SecurityImmutable? security, uint actual_qty) =>
|
||||
actual_qty.safe_divide(model.get_display_quantity_divisor(security));
|
||||
|
||||
public static uint get_display_quantity_divisor(this ISecurityAuxiliaryDataModel model, SecurityImmutable? security) =>
|
||||
security.HasValue ? model.get_auxiliary_data(security.Value)?.quantity_divisor ?? 1 : 1;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
<RuntimeIdentifiers>win7-x64</RuntimeIdentifiers>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<LanguageTargets>$(MSBuildExtensionsPath)\$(VisualStudioVersion)\Bin\Microsoft.CSharp.targets</LanguageTargets>
|
||||
<LanguageTargets Condition="'$(VisualStudioVersion)'=='16.0'">$(MSBuildToolsPath)\Microsoft.CSharp.targets</LanguageTargets>
|
||||
<ApplicationIcon>logo.ico</ApplicationIcon>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<StartupObject>mbts.ui.shell.App</StartupObject>
|
||||
|
||||
@@ -38,9 +38,6 @@ namespace mbts {
|
||||
public static bool IsNonZero(this double value) =>
|
||||
value != 0 && !double.IsNaN(value) && !double.IsInfinity(value);
|
||||
|
||||
public static bool IsRealNonZero(this double value) =>
|
||||
value > 0 && value != double.PositiveInfinity;
|
||||
|
||||
public static double CoerceNaN(double value) => CoerceNaN(value, 0.0);
|
||||
public static double CoerceNaN(double value, double if_nan) => double.IsNaN(value) ? if_nan : value;
|
||||
public static double CoerceNaNOrInfinity(double value) => CoerceNaNOrInfinity(value, 0.0);
|
||||
@@ -63,5 +60,20 @@ namespace mbts {
|
||||
|
||||
public static double? nan_nullable(double value) =>
|
||||
double.IsNaN(value) ? (double?)null : value;
|
||||
|
||||
public static int? to_optional_int32(double value) {
|
||||
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||
return null;
|
||||
if (value > 0) {
|
||||
value = Math.Floor(value);
|
||||
if (value <= int.MaxValue)
|
||||
return (int)value;
|
||||
} else if (value < 0) {
|
||||
value = Math.Ceiling(value);
|
||||
if (value >= int.MinValue)
|
||||
return (int)value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
|
||||
namespace bts.proto {
|
||||
[ProtoContract]
|
||||
readonly struct ToggleInt32Proto : IEquatable<ToggleInt32Proto> {
|
||||
public ToggleInt32Proto(int value, bool is_enabled) {
|
||||
this.value = value;
|
||||
this.is_enabled = is_enabled;
|
||||
}
|
||||
|
||||
[ProtoMember(1)]
|
||||
public int value { get; }
|
||||
[ProtoMember(2)]
|
||||
public bool is_enabled { get; }
|
||||
|
||||
public bool Equals(ToggleInt32Proto other) =>
|
||||
value == other.value && is_enabled == other.is_enabled;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is ToggleInt32Proto other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
(value % (1 << 31)) | (is_enabled ? 1 << 31 : 0);
|
||||
|
||||
public static bool operator ==(ToggleInt32Proto a, ToggleInt32Proto b) => a.Equals(b);
|
||||
public static bool operator !=(ToggleInt32Proto a, ToggleInt32Proto b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
static class ToggleInt32ProtoExtensions {
|
||||
public static int? as_nullable(this ToggleInt32Proto toggle) =>
|
||||
toggle.is_enabled ? toggle.value : (int?)null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace mbts.ui.controls {
|
||||
class DIYGridColumn : DIYGrid.ColumnBase<string> {
|
||||
public DIYGridColumn()
|
||||
: base() {
|
||||
is_visible = true;
|
||||
Width = 60;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-5
@@ -38,11 +38,7 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
|
||||
public static ICommand PreviewSoundCommand =>
|
||||
new DelegateCommand<SoundNotification.SystemSound>(s => {
|
||||
try {
|
||||
NAudioPlayer.create(s).play();
|
||||
} catch (NAudio.MmException) { }
|
||||
});
|
||||
new DelegateCommand<SoundNotification.SystemSound>(NAudioPlayer.play);
|
||||
|
||||
#region SoundTypes
|
||||
public static readonly DependencyProperty SoundTypesProperty =
|
||||
|
||||
@@ -1,39 +1,43 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace mbts.spark.filters {
|
||||
static class RegexFilterUtils {
|
||||
public static Regex build_regex(string str) {
|
||||
if (str == null)
|
||||
return null;
|
||||
str = str.Trim();
|
||||
if (str == string.Empty)
|
||||
return null;
|
||||
return new Regex(str, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
|
||||
}
|
||||
}
|
||||
class RegexFilterVM<TRow> : FilterVMBase<TRow, string, string> {
|
||||
string _text = string.Empty;
|
||||
Regex _regex;
|
||||
|
||||
public RegexFilterVM(Func<TRow, string> get_row_val, string prop_name = null) : base(get_row_val, prop_name) { }
|
||||
|
||||
public string text {
|
||||
get => _text;
|
||||
set => _regex = RegexFilterUtils.build_regex(_text = (value ?? string.Empty));
|
||||
}
|
||||
|
||||
public override string get_state() => _text ?? string.Empty;
|
||||
|
||||
protected override bool _get_active() => _regex != null;
|
||||
protected override bool _filter(string value) =>
|
||||
_regex != null && (
|
||||
_regex.IsMatch(value) ||
|
||||
(!string.IsNullOrEmpty(value) && _regex.IsMatch(value.Replace(" ", ""))));
|
||||
|
||||
namespace mbts.spark.filters {
|
||||
static class RegexFilterUtils {
|
||||
public static Regex build_regex(string str) {
|
||||
if (str == null)
|
||||
return null;
|
||||
str = str.Trim();
|
||||
if (str == string.Empty)
|
||||
return null;
|
||||
return new Regex(str, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
|
||||
}
|
||||
}
|
||||
class RegexFilterVM<TRow> : FilterVMBase<TRow, string, string> {
|
||||
string _text = string.Empty;
|
||||
Regex _regex;
|
||||
|
||||
public RegexFilterVM(Func<TRow, string> get_row_val, string prop_name = null) : base(get_row_val, prop_name) { }
|
||||
|
||||
public string text {
|
||||
get => _text;
|
||||
set => _regex = RegexFilterUtils.build_regex(_text = (value ?? string.Empty));
|
||||
}
|
||||
|
||||
public override string get_state() => _text ?? string.Empty;
|
||||
|
||||
protected override bool _get_active() => _regex != null;
|
||||
protected override bool _filter(string value) => _regex != null && _regex.IsMatch(value);
|
||||
protected override void _load_state_internal(string state) => text = state;
|
||||
protected override void _clear() {
|
||||
_close();
|
||||
_text = string.Empty;
|
||||
_regex = null;
|
||||
RaisePropertyChanged(nameof(text));
|
||||
_raise_filter_changed();
|
||||
}
|
||||
}
|
||||
protected override void _load_state_internal(string state) => text = state;
|
||||
protected override void _clear() {
|
||||
_close();
|
||||
_text = string.Empty;
|
||||
_regex = null;
|
||||
RaisePropertyChanged(nameof(text));
|
||||
_raise_filter_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1005,6 +1005,8 @@
|
||||
ContractPickerModel="{Binding contract_picker_model}"
|
||||
HedgePricing="{Binding hedge_pricing}"
|
||||
HedgePricingItemsSource="{Binding hedge_pricing_options}"
|
||||
UseFractionalTicks="{Binding use_fractional_ticks}"
|
||||
IntegerTickSize="{Binding integer_tick_size}"
|
||||
/>
|
||||
<c:DialogSkirt Grid.Row="1" Margin="-4" VerticalAlignment="Top" RightItems="{Binding skirt_items}"/>
|
||||
</Grid>
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace mbts.ui.converters
|
||||
public Visibility VisibilityForFalse { get { return _visfalse; } set { _visfalse = value; } }
|
||||
|
||||
public static BooleanToVisibilityConverter Instance => new BooleanToVisibilityConverter();
|
||||
public static BooleanToVisibilityConverter HiddenInstance => new BooleanToVisibilityConverter {
|
||||
VisibilityForFalse = Visibility.Hidden
|
||||
};
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
|
||||
bool flag = false;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using bts.utils;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace mbts.ui.converters {
|
||||
[ValueConversion(typeof(double), typeof(string))]
|
||||
class DoubleDivisorConverter : IValueConverter {
|
||||
public double Divisor { get; set; }
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
value is double d ? d.safe_divide(Divisor) :
|
||||
value is int i ? i.safe_divide(Divisor) :
|
||||
value is uint u ? u.safe_divide(Divisor) :
|
||||
double.NaN;
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
value is double d ? d * Divisor :
|
||||
value is int i ? (double)i * Divisor :
|
||||
value is uint u ? (double)u * Divisor :
|
||||
double.NaN;
|
||||
}
|
||||
|
||||
class DynamicDoubleDivisorConverter : IMultiValueConverter {
|
||||
public static DynamicDoubleDivisorConverter Instance { get; } = new DynamicDoubleDivisorConverter();
|
||||
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
|
||||
if (ConvertUtils.try_convert_args(values, out double value, out double div)) {
|
||||
return value.safe_divide(div);
|
||||
}
|
||||
return new ValidationResult(false, "unable to convert values to doubles.");
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace mbts.ui.converters {
|
||||
@@ -19,4 +20,18 @@ namespace mbts.ui.converters {
|
||||
value is uint u ? (double)u / Multiplier :
|
||||
double.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicDoubleMultiplierConverter : IMultiValueConverter {
|
||||
public static DynamicDoubleMultiplierConverter Instance { get; } = new DynamicDoubleMultiplierConverter();
|
||||
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
|
||||
if (ConvertUtils.try_convert_args(values, out double value, out double mult)) {
|
||||
return value * mult;
|
||||
}
|
||||
return new ValidationResult(false, "unable to convert values to doubles.");
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -183,7 +183,7 @@ namespace mbts.ui.dpi {
|
||||
}
|
||||
|
||||
public static void DrawHorizontalLine(this DrawingContext drawing_context, System.Windows.Media.Pen pen, Point start, double length, bool snap_to_device_pixels) {
|
||||
if (pen == null || !length.IsRealNonZero()) {
|
||||
if (pen == null || !length.is_real_positive()) {
|
||||
// no need to flip out and throw an exception
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using bts.utils;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Markup;
|
||||
|
||||
@@ -19,7 +20,7 @@ namespace mbts.ui {
|
||||
public double RoundingUnit { get; set; }
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider) =>
|
||||
RoundingUnit.IsRealNonZero() ? Math.Round(Value * RoundingUnit) / RoundingUnit : Value;
|
||||
RoundingUnit.is_real_non_zero() ? Math.Round(Value * RoundingUnit) / RoundingUnit : Value;
|
||||
}
|
||||
|
||||
internal class DoubleStringTypeConverter : TypeConverter
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui {
|
||||
@@ -51,15 +52,19 @@ namespace mbts.ui {
|
||||
public void thread_init() { }
|
||||
}
|
||||
|
||||
public static IShellWindow create(IShellModule owner, string title, string window_type, Lazy<UIElement> content, Action<UIElement, Stream> get_state = null, ImageSource icon = null, string thread_id=null, Action<ShellWindowVM> on_shell_window_vm = null) {
|
||||
public static IShellWindow create(IShellModule owner, string title, string window_type, Lazy<UIElement> content, Action<UIElement, Stream> get_state = null, ImageSource icon = null, string thread_id=null, Action<ShellWindowVM> on_shell_window_vm = null, Action<KeyEventArgs> on_key_down = null) {
|
||||
IShellWindow rv = null;
|
||||
if (string.IsNullOrEmpty(thread_id)) {
|
||||
return create<AnonShellWindow>(owner, title, window_type, content, get_state, icon, on_shell_window_vm);
|
||||
rv = create<AnonShellWindow>(owner, title, window_type, content, get_state, icon, on_shell_window_vm);
|
||||
if (on_key_down != null)
|
||||
rv = new KeyListenerWrapper(rv, on_key_down);
|
||||
}
|
||||
else {
|
||||
var res = create<LazyAnonShellWindow>(owner, title, window_type, content, get_state, icon, on_shell_window_vm);
|
||||
res.thread_id = thread_id;
|
||||
return res;
|
||||
rv = on_key_down == null ? (ISharedWindow)res : new SharedKeyListenerWrapper(res, on_key_down);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
static T create<T>(IShellModule owner, string title, string window_type, Lazy<UIElement> content, Action<UIElement,Stream> get_state=null, ImageSource icon=null, Action<ShellWindowVM> on_shell_window_vm = null) where T : AnonShellWindow, new() {
|
||||
var result = new T() {
|
||||
@@ -121,5 +126,56 @@ namespace mbts.ui {
|
||||
if (cleanup != null) { window.Closed += (o, e) => cleanup.Dispose(); }
|
||||
return window;
|
||||
}
|
||||
|
||||
class KeyListenerWrapper<T> : IShellWindow, IKeyListener where T : class, IShellWindow {
|
||||
readonly T _model;
|
||||
readonly Action<KeyEventArgs> _key_down;
|
||||
|
||||
public KeyListenerWrapper(T model, Action<KeyEventArgs> key_down) {
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
_key_down = key_down ?? throw new ArgumentNullException(nameof(key_down));
|
||||
}
|
||||
|
||||
public event Action TitleChanged {
|
||||
add => _model.TitleChanged += value;
|
||||
remove => _model.TitleChanged -= value;
|
||||
}
|
||||
|
||||
public event Action IconChanged {
|
||||
add => _model.IconChanged += value;
|
||||
remove => _model.IconChanged -= value;
|
||||
}
|
||||
|
||||
public string Title { get => _model.Title; set => _model.Title = value; }
|
||||
public string window_type => _model.window_type;
|
||||
public ImageSource Icon => _model.Icon;
|
||||
public IShellModule Owner => _model.Owner;
|
||||
|
||||
public void Dispose() => _model.Dispose();
|
||||
public ShellWindowUI GetContent() => _model.GetContent();
|
||||
public void GetState(Stream save_here) => _model.GetState(save_here);
|
||||
public void on_shell_window_vm(ShellWindowVM shell_window_vm) => _model.on_shell_window_vm(shell_window_vm);
|
||||
public void key_down(KeyEventArgs e) =>
|
||||
_key_down(e);
|
||||
}
|
||||
|
||||
class KeyListenerWrapper : KeyListenerWrapper<IShellWindow> {
|
||||
public KeyListenerWrapper(IShellWindow model, Action<KeyEventArgs> key_down)
|
||||
: base(model, key_down) { }
|
||||
}
|
||||
|
||||
class SharedKeyListenerWrapper : KeyListenerWrapper<ISharedWindow>, ISharedWindow {
|
||||
readonly ISharedWindow _model;
|
||||
readonly Action<KeyEventArgs> _key_down;
|
||||
|
||||
public SharedKeyListenerWrapper(ISharedWindow model, Action<KeyEventArgs> key_down)
|
||||
: base(model, key_down) {
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
_key_down = key_down ?? throw new ArgumentNullException(nameof(key_down));
|
||||
}
|
||||
|
||||
public string thread_id => _model.thread_id;
|
||||
public void thread_init() => _model.thread_init();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace mbts.ui.shell {
|
||||
// This Class owns the hierarchy of shell Windows
|
||||
@@ -59,6 +60,14 @@ namespace mbts.ui.shell {
|
||||
viewmodel.host_id = state.host_id ?? "";
|
||||
viewmodel.TitleOverride = state.custom_title;
|
||||
}
|
||||
if (model is IKeyListener key_listener) {
|
||||
content.KeyDown += on_key_down;
|
||||
void on_key_down(object sender, KeyEventArgs e) {
|
||||
viewmodel.Dispatcher.InvokeAsync(() => {
|
||||
key_listener.key_down(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
var window = await CreateWindow(content, state);
|
||||
|
||||
window.Show();
|
||||
|
||||
@@ -49,6 +49,28 @@ namespace mbts.spark {
|
||||
public static double parse(this ContractSpecificationImmutable? spec, string text, double display_multiplier, string nan_text) =>
|
||||
spec.spec_or_default().parse(text, display_multiplier, nan_text);
|
||||
|
||||
public static bool is_integer_tick(ContractSpecificationImmutable spec) =>
|
||||
spec.MainFraction == 0 &&
|
||||
spec.PriceDisplayFormat == 0 &&
|
||||
spec.LowTick > 0 &&
|
||||
MathUtils.is_int(spec.LowTick) &&
|
||||
spec.HighTick > 0 &&
|
||||
MathUtils.is_int(spec.HighTick);
|
||||
|
||||
public static ContractSpecification create_integer_tick_spec(int low_tick) =>
|
||||
create_integer_tick_spec(low_tick, low_tick);
|
||||
|
||||
public static ContractSpecification create_integer_tick_spec(int low_tick, int high_tick) {
|
||||
var rv = SecurityDataHelper.Defaults.ContractSpecification;
|
||||
rv.MainFraction = 0;
|
||||
rv.SubFraction = 0;
|
||||
rv.PriceDisplayFormat = 0;
|
||||
rv.PriceBreak = 0;
|
||||
rv.LowTick = low_tick;
|
||||
rv.HighTick = high_tick;
|
||||
return rv;
|
||||
}
|
||||
|
||||
public static double low_tick(NativePriceSize[] bids, NativePriceSize[] asks) {
|
||||
double low = bids[bids.Length - 1].price;
|
||||
if (double.IsNaN(low))
|
||||
@@ -326,7 +348,7 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
static bool _check_tick_size(double tick_size) =>
|
||||
(tick_size > 0 && tick_size <= 0.5) || tick_size == 1;
|
||||
tick_size > 0 && (tick_size <= 0.5 || MathUtils.is_int(tick_size));
|
||||
|
||||
struct SpecKey : IEquatable<SpecKey> {
|
||||
public readonly uint denominator, display_digits;
|
||||
|
||||
@@ -45,15 +45,16 @@ namespace mbts.spark {
|
||||
readonly SecurityImmutable _security;
|
||||
readonly MBOSecurityQueryImmutable _query;
|
||||
readonly Dictionary<(string,bool), FormattedText> _cache;
|
||||
readonly Func<int, string> _fmt_mbo_qty;
|
||||
int _max_order_size = 1;
|
||||
double _height = 0;
|
||||
double _number_width = 0;
|
||||
|
||||
|
||||
public MBOPopupModel(MBOService mbo, SecurityImmutable security, MBOSecurityQueryImmutable query) {
|
||||
public MBOPopupModel(MBOService mbo, SecurityImmutable security, MBOSecurityQueryImmutable query, Func<int, string> fmt_mbo_qty) {
|
||||
_mbo = mbo;
|
||||
_security = security;
|
||||
_query = query;
|
||||
_fmt_mbo_qty = fmt_mbo_qty ?? new Func<int, string>(v => v.ToString());
|
||||
|
||||
// prime the cache with numbers on the left hand side
|
||||
_cache =
|
||||
@@ -110,7 +111,7 @@ namespace mbts.spark {
|
||||
|
||||
pdp.row_numbers = p.orders.Select((o, i) => format_text((i+1).ToString(), ismine(o))).ToList();
|
||||
pdp.numbers_width = _number_width = Math.Max(_number_width, (int)pdp.row_numbers.Max(r => r.Width) + text_margin);
|
||||
pdp.row_labels = p.orders.Select(o => format_text(o.Size.ToString() + (ismine(o) ? "*" : ""), ismine(o))).ToList();
|
||||
pdp.row_labels = p.orders.Select(o => format_text(_fmt_mbo_qty(o.Size) + (ismine(o) ? "*" : ""), ismine(o))).ToList();
|
||||
var max_label_width = pdp.row_labels.Max(l => l.Width) + text_margin;
|
||||
|
||||
var available_width = pdp.width - pdp.numbers_width - max_label_width;
|
||||
|
||||
@@ -7,26 +7,53 @@ using System.Windows.Input;
|
||||
namespace mbts.spark {
|
||||
public class CalculatorLogic : DisposableViewModelBase {
|
||||
readonly UserProperty<uint?> _value;
|
||||
readonly OptionalDisplayQuantityMultiplier _display_translator = new OptionalDisplayQuantityMultiplier();
|
||||
IDisposable _prop_changes_echo;
|
||||
int[] _buttons;
|
||||
|
||||
public CalculatorLogic(uint? initial_size) =>
|
||||
public CalculatorLogic(uint? initial_size) {
|
||||
_value = new UserProperty<uint?>(backing(nameof(value), initial_size));
|
||||
_echo_props_changed();
|
||||
}
|
||||
|
||||
public CalculatorLogic(uint? initial_size, IObservable<int[]> buttons_source) : this(initial_size) =>
|
||||
_dispose = buttons_source.Subscribe(newval => buttons = newval);
|
||||
|
||||
public uint? value { get => _value.Value; set => _value.Value = value; }
|
||||
public uint display_divisor {
|
||||
get => _display_translator.conversion_data.value;
|
||||
set => _display_translator.conversion_data = new QuantityConversionData(value, QuantityConversionData.OpType.DivideActual);
|
||||
}
|
||||
public uint? value {
|
||||
get => _display_translator.actual_value;
|
||||
set => _display_translator.actual_value = value;
|
||||
}
|
||||
public uint? display_value {
|
||||
get => _display_translator.display_value;
|
||||
set => _display_translator.display_value = value;
|
||||
}
|
||||
public bool value_setter_called => _value.setter_called;
|
||||
public IObservable<uint?> value_observable => _value.Select(vals => vals.newval);
|
||||
public int[] buttons { get => _buttons; set => Set(ref _buttons, value); }
|
||||
public ICommand click => new DelegateCommand<uint>(handle_calculator_click);
|
||||
public ICommand set_value_command => new DelegateCommand<uint>(v => _value.Value = v);
|
||||
public ICommand clear => new DelegateCommand(() => _value.Value = null);
|
||||
public ICommand set_display_value_command => new DelegateCommand<uint>(v => _display_translator.display_value = v);
|
||||
public ICommand set_actual_value_command => new DelegateCommand<uint>(v => _display_translator.actual_value = v);
|
||||
public ICommand clear => new DelegateCommand(() => _display_translator.actual_value = null);
|
||||
|
||||
public IObservable<uint?> observe_current(bool start_immediately = false) =>
|
||||
_value.observe_current(start_immediately);
|
||||
public IObservable<uint?> observe_actual_value() =>
|
||||
_display_translator.subscribe_actual_value();
|
||||
|
||||
public void bypass_value_setter(uint value) => _value.bypass_setter(value);
|
||||
// Don't use a default value for 'is_actual_value.'
|
||||
// Quietly multiplying (or not multiplying) the size input is dangerous -
|
||||
// force the caller to be specific.
|
||||
public void bypass_value_setter(uint value, bool is_actual_value) {
|
||||
Disposable.dispose(ref _prop_changes_echo);
|
||||
if (is_actual_value) {
|
||||
this.value = value;
|
||||
} else {
|
||||
display_value = value;
|
||||
}
|
||||
if (_prop_changes_echo != null)
|
||||
_echo_props_changed();
|
||||
}
|
||||
|
||||
public bool validate() {
|
||||
if (!value.HasValue || value.Value == 0) {
|
||||
@@ -38,25 +65,34 @@ namespace mbts.spark {
|
||||
public void flash_error() =>
|
||||
flash_error(nameof(value)).ignore();
|
||||
|
||||
protected override void Dispose(bool disposing) {
|
||||
Disposable.dispose(ref _prop_changes_echo);
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
void _echo_props_changed() {
|
||||
// dispose the echo to avoid raising prop changed events more than once
|
||||
_prop_changes_echo?.Dispose();
|
||||
_prop_changes_echo = _display_translator.connect_owner(this, nameof(display_value), nameof(value));
|
||||
}
|
||||
|
||||
void handle_calculator_click(uint value) {
|
||||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift) || value == 0) {
|
||||
_value.Value = value;
|
||||
_display_translator.actual_value = value;
|
||||
} else if (KeyboardExtensions.IsCtrlDown()) {
|
||||
if (!_value.Value.HasValue)
|
||||
_value.Value = 0;
|
||||
else {
|
||||
if (value > _value.Value)
|
||||
_value.Value = 0;
|
||||
else
|
||||
_value.Value -= value;
|
||||
if (!_display_translator.actual_value.HasValue) {
|
||||
_display_translator.actual_value = 0;
|
||||
} else if (value > _display_translator.actual_value) {
|
||||
_display_translator.actual_value = 0;
|
||||
} else {
|
||||
_display_translator.actual_value -= value;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!_value.Value.HasValue)
|
||||
_value.Value = value;
|
||||
} else {
|
||||
if (!_display_translator.actual_value.HasValue)
|
||||
_display_translator.actual_value = value;
|
||||
else
|
||||
_value.Value += value;
|
||||
_display_translator.actual_value += value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using mbts.ui.converters;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
@@ -12,6 +14,31 @@ namespace mbts.ui.utilities {
|
||||
public static string fmt(string title, KeyGesture key_gesture) =>
|
||||
key_gesture == null ? title : $"{title} ({key_gesture.GetDisplayStringForCulture(CultureInfo.CurrentCulture)})";
|
||||
|
||||
public static IDisposable bind_param_source<T>(ICommand command, IObservable<T> param_source, out ICommand bound_cmd, bool dispose_command_on_dispose = false) {
|
||||
bool initialized = false;
|
||||
bool can_execute = false;
|
||||
T param = default;
|
||||
var bcmd = new DelegateCommand(() => command.Execute(param), () => can_execute);
|
||||
bound_cmd = bcmd;
|
||||
var sub = param_source.Subscribe(p => {
|
||||
param = p;
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
can_execute = command.CanExecute(p);
|
||||
bcmd.RaiseCanExecuteChanged();
|
||||
} else if (can_execute != command.CanExecute(p)) {
|
||||
can_execute = !can_execute;
|
||||
bcmd.RaiseCanExecuteChanged();
|
||||
}
|
||||
});
|
||||
return dispose_command_on_dispose ?
|
||||
Disposable.Create(() => {
|
||||
Disposable.dispose(ref sub);
|
||||
(command as IDisposable)?.Dispose();
|
||||
command = null;
|
||||
}) : sub;
|
||||
}
|
||||
|
||||
#region Paraphrased from https://referencesource.microsoft.com/#PresentationFramework/src/Framework/MS/Internal/Commands/CommandHelpers.cs
|
||||
|
||||
public static bool CanExecuteCommandSource(ICommandSource commandSource) {
|
||||
|
||||
@@ -30,6 +30,21 @@ namespace mbts.ui {
|
||||
public string name { get; }
|
||||
}
|
||||
|
||||
class NamedValueVM : ViewModelBase {
|
||||
string _name;
|
||||
object _value;
|
||||
|
||||
public NamedValueVM() { }
|
||||
|
||||
public NamedValueVM(string name, object value) {
|
||||
_name = name;
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public string name { get => _name; set => Set(ref _name, value); }
|
||||
public object value { get => _value; set => Set(ref _value, value); }
|
||||
}
|
||||
|
||||
static class EnabledValueVMExtensions {
|
||||
public static T? get_nullable<T>(this EnabledValueVM<T> vm) where T : struct =>
|
||||
vm.enabled ? vm.value : (T?)null;
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
using bts.utils;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Linq;
|
||||
|
||||
namespace mbts.ui {
|
||||
interface IDisplayTranslator<T> {
|
||||
T display_value { get; set; }
|
||||
T actual_value { get; set; }
|
||||
event Action value_changed;
|
||||
}
|
||||
|
||||
readonly struct QuantityConversionData : IEquatable<QuantityConversionData> {
|
||||
public QuantityConversionData(uint value, OpType op) {
|
||||
this.value = value;
|
||||
this.op = op;
|
||||
}
|
||||
|
||||
public enum OpType {
|
||||
None,
|
||||
MultiplyActual,
|
||||
DivideActual
|
||||
}
|
||||
|
||||
public uint value { get; }
|
||||
public OpType op { get; }
|
||||
|
||||
public bool Equals(QuantityConversionData other) =>
|
||||
value == other.value &&
|
||||
op == other.op;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is QuantityConversionData other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.auto_phase(value.GetHashCode(), op.GetHashCode());
|
||||
|
||||
public static bool operator ==(QuantityConversionData x, QuantityConversionData y) => x.Equals(y);
|
||||
public static bool operator !=(QuantityConversionData x, QuantityConversionData y) => !x.Equals(y);
|
||||
}
|
||||
|
||||
class DisplayQuantityMultiplier : IDisplayTranslator<uint> {
|
||||
uint _actual_value;
|
||||
QuantityConversionData _conversion_data;
|
||||
|
||||
public event Action value_changed;
|
||||
|
||||
public uint actual_value {
|
||||
get => _actual_value;
|
||||
set {
|
||||
if (_actual_value != value) {
|
||||
_actual_value = value;
|
||||
value_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public uint display_value {
|
||||
get => _conversion_data.to_display(_actual_value);
|
||||
set => actual_value = _conversion_data.from_display(value);
|
||||
}
|
||||
|
||||
public QuantityConversionData conversion_data {
|
||||
get => _conversion_data;
|
||||
set {
|
||||
if (_conversion_data != value) {
|
||||
_conversion_data = value;
|
||||
value_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OptionalDisplayQuantityMultiplier : IDisplayTranslator<uint?> {
|
||||
uint? _actual_value;
|
||||
QuantityConversionData _conversion_data;
|
||||
|
||||
public event Action value_changed;
|
||||
|
||||
public uint? actual_value {
|
||||
get => _actual_value;
|
||||
set {
|
||||
if (_actual_value != value) {
|
||||
_actual_value = value;
|
||||
value_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public uint? display_value {
|
||||
get => _actual_value.HasValue ? _conversion_data.to_display(_actual_value.Value) : _actual_value;
|
||||
set => actual_value = value.HasValue ? _conversion_data.from_display(value.Value) : value;
|
||||
}
|
||||
|
||||
public QuantityConversionData conversion_data {
|
||||
get => _conversion_data;
|
||||
set {
|
||||
if (_conversion_data != value) {
|
||||
_conversion_data = value;
|
||||
value_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class DisplayTranslatorExtensions {
|
||||
public static IDisposable connect_owner<T>(this IDisplayTranslator<T> display_translator, IRaiseNotifyPropertyChanged owner, params string[] properties) {
|
||||
if (display_translator is null)
|
||||
throw new ArgumentNullException(nameof(display_translator));
|
||||
if (owner is null)
|
||||
throw new ArgumentNullException(nameof(owner));
|
||||
display_translator.value_changed += on_value_changed;
|
||||
return Disposable.Create(() => display_translator.value_changed -= on_value_changed);
|
||||
void on_value_changed() {
|
||||
for (int i = 0; i < properties.Length; i++)
|
||||
owner.raise_notify_property_changed(properties[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static IObservable<T> subscribe_actual_value<T>(this IDisplayTranslator<T> display_translator) =>
|
||||
subscribe_actual_value(display_translator, EqualityComparer<T>.Default);
|
||||
|
||||
public static IObservable<T> subscribe_actual_value<T>(this IDisplayTranslator<T> display_translator, IEqualityComparer<T> comparer) {
|
||||
if (comparer is null)
|
||||
throw new ArgumentNullException(nameof(comparer));
|
||||
return Observable.Create<T>(o => {
|
||||
var curr = display_translator.actual_value;
|
||||
o.OnNext(curr);
|
||||
display_translator.value_changed += on_value_changed;
|
||||
return Disposable.Create(() => display_translator.value_changed -= on_value_changed);
|
||||
void on_value_changed() {
|
||||
var next = display_translator.actual_value;
|
||||
if (!comparer.Equals(curr, next))
|
||||
o.OnNext(curr = next);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static uint to_display(this QuantityConversionData quantity_conversion_data, uint actual_value) {
|
||||
switch (quantity_conversion_data.op) {
|
||||
case QuantityConversionData.OpType.DivideActual: return actual_value.safe_divide(quantity_conversion_data.value);
|
||||
case QuantityConversionData.OpType.MultiplyActual: return actual_value * quantity_conversion_data.value;
|
||||
default: return actual_value;
|
||||
}
|
||||
}
|
||||
|
||||
public static int to_display(this QuantityConversionData quantity_conversion_data, int actual_value) {
|
||||
switch (quantity_conversion_data.op) {
|
||||
case QuantityConversionData.OpType.DivideActual: return (int)(actual_value.safe_divide(quantity_conversion_data.value));
|
||||
case QuantityConversionData.OpType.MultiplyActual: return (int)(actual_value * quantity_conversion_data.value);
|
||||
default: return actual_value;
|
||||
}
|
||||
}
|
||||
|
||||
public static double to_display(this QuantityConversionData quantity_conversion_data, double actual_value) {
|
||||
switch (quantity_conversion_data.op) {
|
||||
case QuantityConversionData.OpType.DivideActual: return actual_value.safe_divide(quantity_conversion_data.value);
|
||||
case QuantityConversionData.OpType.MultiplyActual: return actual_value * quantity_conversion_data.value;
|
||||
default: return actual_value;
|
||||
}
|
||||
}
|
||||
|
||||
public static uint from_display(this QuantityConversionData quantity_conversion_data, uint display_value) {
|
||||
switch (quantity_conversion_data.op) {
|
||||
case QuantityConversionData.OpType.DivideActual: return display_value * quantity_conversion_data.value;
|
||||
case QuantityConversionData.OpType.MultiplyActual: return display_value.safe_divide(quantity_conversion_data.value);
|
||||
default: return display_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,9 @@ namespace mbts.ui.spark {
|
||||
/// These are private members, so any code-readability cost is confined to a limited scope.
|
||||
delegate void _SizeFormatter(ref uint merged, ref uint implied, ref StringBuilder sb);
|
||||
delegate void _SizeFormatterNPS(ref NativePriceSize nps, ref StringBuilder sb);
|
||||
|
||||
public static Func<uint, string> get_value_formatter(uint divisor) =>
|
||||
value => (value / divisor).ToString();
|
||||
|
||||
public static Func<NativePriceSize, string> get_formatter_nps(MarketSizeDisplay size_display) {
|
||||
_SizeFormatterNPS implied_absent = _get_formatter_nps(size_display.normal);
|
||||
@@ -120,6 +123,11 @@ namespace mbts.ui.spark {
|
||||
};
|
||||
}
|
||||
|
||||
public static Func<NativePriceSize, string> get_formatter_nps(MarketSizeDisplay size_display, uint display_divisor) {
|
||||
var raw_fmt_size = get_formatter(size_display);
|
||||
return nps => raw_fmt_size((nps.implied_size + nps.direct_size) / display_divisor, nps.implied_size / display_divisor);
|
||||
}
|
||||
|
||||
public static SizeFormatter get_formatter(MarketSizeDisplay size_display) {
|
||||
_SizeFormatter implied_absent = _get_formatter(size_display.normal);
|
||||
_SizeFormatter implied_present = _get_formatter(size_display.implied_present ?? size_display.normal);
|
||||
@@ -281,4 +289,4 @@ namespace mbts.ui.spark {
|
||||
public void refresh() => _display.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,13 +39,18 @@ namespace bts.api {
|
||||
|
||||
static string _format(SecurityImmutable? security) {
|
||||
switch (security?.ContractCase) {
|
||||
case ContractOneofCase.Future: return format_future(security?.Symbol, security?.security_desc(), security.Value.Future);
|
||||
case ContractOneofCase.Future:
|
||||
if(security?.HasIce == true)
|
||||
return _format_ICE(security);
|
||||
return format_future(security?.Symbol, security?.security_desc(), security.Value.Future);
|
||||
case ContractOneofCase.Spread:
|
||||
if (security?.HasGlbx == true) {
|
||||
// In case we ever bring it back, pretty printed spread names can be found here:
|
||||
// GlbxSecurityFormatter.FormatSpread(security, out string result)
|
||||
return security?.Glbx.SecurityDesc;
|
||||
}
|
||||
else if(security?.HasIce == true)
|
||||
return _format_ICE(security);
|
||||
return string.Join("", _format_future_legs(SecurityHelper.get_flat_future_legs(security?.Spread.Legs)));
|
||||
case ContractOneofCase.Synthetic: return security?.Synthetic.Name;
|
||||
default: return "Unknown-security";
|
||||
@@ -79,5 +84,28 @@ namespace bts.api {
|
||||
default: return $"{fl.quantity:+0;-0}{sym_exp_name}";
|
||||
}
|
||||
}
|
||||
|
||||
static string _format_ICE (SecurityImmutable? security){
|
||||
string symbol = security?.Symbol,
|
||||
strip = security?.Ice.StripName,
|
||||
md_desc = security?.Ice.MarketDescription,
|
||||
type_append = "";
|
||||
|
||||
if(md_desc.Contains("25k ICE Lots")){
|
||||
if(md_desc.Contains("TAS"))
|
||||
type_append = "TAS";
|
||||
}
|
||||
else if(md_desc.Contains("ICE Lots")){
|
||||
if(md_desc.Contains("TAS"))
|
||||
type_append = "ICE Lots TAS";
|
||||
else
|
||||
type_append = "ICE Lots";
|
||||
}
|
||||
else if(md_desc.Contains("TAS"))
|
||||
type_append = "TAS";
|
||||
type_append = string.IsNullOrEmpty(type_append) ?
|
||||
" " : $" {type_append} ";
|
||||
return $"{symbol}{type_append}{strip}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,22 +83,24 @@ namespace bts.api {
|
||||
}
|
||||
|
||||
public static int Compare(SecurityImmutable x, SecurityImmutable y) {
|
||||
if (x.ContractCase == ContractOneofCase.Future) return y.ContractCase == ContractOneofCase.Future ? Compare(x.Symbol, x.Future, y.Symbol, y.Future) : -1;
|
||||
else if (y.ContractCase == ContractOneofCase.Future) return 1;
|
||||
if (x.ContractCase == ContractOneofCase.Future) {
|
||||
return y.ContractCase == ContractOneofCase.Future ? Compare(x.Symbol, x.Future, y.Symbol, y.Future) : -1;
|
||||
} else if (y.ContractCase == ContractOneofCase.Future) {
|
||||
return 1;
|
||||
}
|
||||
var xlegs = get_flat_future_legs(x).ToList();
|
||||
var ylegs = get_flat_future_legs(y).ToList();
|
||||
int max = Math.Min(xlegs.Count, ylegs.Count);
|
||||
for (int i = 0; i < max; i++) {
|
||||
int rv = Compare(xlegs[i].symbol, xlegs[i].future, ylegs[i].symbol, ylegs[i].future);
|
||||
if (rv != 0) return rv;
|
||||
}
|
||||
for (int i = 0; i < max; i++) {
|
||||
int rv = xlegs[i].quantity.CompareTo(ylegs[i].quantity);
|
||||
if (rv != 0) return rv;
|
||||
}
|
||||
for (int i = 0; i < max; i++) {
|
||||
int rv = xlegs[i].exchange.CompareTo(ylegs[i].exchange);
|
||||
if (rv != 0) return rv;
|
||||
if (rv != 0)
|
||||
return rv;
|
||||
rv = xlegs[i].quantity.CompareTo(ylegs[i].quantity);
|
||||
if (rv != 0)
|
||||
return rv;
|
||||
rv = xlegs[i].exchange.CompareTo(ylegs[i].exchange);
|
||||
if (rv != 0)
|
||||
return rv;
|
||||
}
|
||||
return xlegs.Count.CompareTo(ylegs.Count);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,28 @@ namespace bts.utils {
|
||||
return epsilon_equals(with_int_value, d);
|
||||
}
|
||||
|
||||
public static bool is_real(this double value) {
|
||||
switch (value) {
|
||||
case double.PositiveInfinity:
|
||||
case double.NegativeInfinity:
|
||||
case double.NaN:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool is_real_positive(this double value) =>
|
||||
value > 0 && value != double.PositiveInfinity;
|
||||
|
||||
public static bool is_real_negative(this double value) =>
|
||||
value < 0 && value != double.NegativeInfinity;
|
||||
|
||||
public static bool is_real_non_zero(this double value) =>
|
||||
value > 0 ?
|
||||
!double.IsPositiveInfinity(value) :
|
||||
value < 0 && !double.IsNegativeInfinity(value);
|
||||
|
||||
public static double factor_out(double value, params double[] factors) {
|
||||
foreach (double f in factors)
|
||||
value = factor_out(value, f, out int _);
|
||||
@@ -129,6 +151,27 @@ namespace bts.utils {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static int safe_divide(this int value, int divisor) =>
|
||||
divisor == 0 ? value : value / divisor;
|
||||
|
||||
public static int safe_divide(this int value, uint divisor) =>
|
||||
divisor == 0 ? value : (int)(value / divisor);
|
||||
|
||||
public static double safe_divide(this int value, double divisor) =>
|
||||
divisor.is_real_non_zero() ? value / divisor : value;
|
||||
|
||||
public static uint safe_divide(this uint value, uint divisor) =>
|
||||
divisor == 0 ? value : value / divisor;
|
||||
|
||||
public static double safe_divide(this uint value, double divisor) =>
|
||||
divisor.is_real_non_zero() ? value / divisor : value;
|
||||
|
||||
public static ulong safe_divide(this ulong value, uint divisor) =>
|
||||
divisor == 0 ? value : value / divisor;
|
||||
|
||||
public static double safe_divide(this double value, double divisor) =>
|
||||
divisor.is_real_non_zero() && value.is_real_non_zero() ? value / divisor : value;
|
||||
|
||||
/// <summary>
|
||||
/// Adds <see cref="mod"/> to the modulos result if negative.
|
||||
/// </summary>
|
||||
|
||||
@@ -390,6 +390,15 @@ namespace System.Collections.Generic {
|
||||
public static ReadOnlyDictionary<TKey, TElement> ToReadOnlyDictionary<T, TKey, TElement>(this IEnumerable<T> enumerable, Func<T, TKey> key_selector, Func<T, TElement> element_selector, IEqualityComparer<TKey> comparer) =>
|
||||
new ReadOnlyDictionary<TKey, TElement>(enumerable.ToDictionary(key_selector, element_selector, comparer));
|
||||
|
||||
public static ReadOnlyDictionary<TKey, TElement> ToReadOnlyDictionaryWhere<TKey, TElement>(this IEnumerable<TKey> keys, TryGetValue<TKey, TElement> value_selector) {
|
||||
var mdict = new Dictionary<TKey, TElement>();
|
||||
foreach (var k in keys) {
|
||||
if (value_selector(k, out var val))
|
||||
mdict[k] = val;
|
||||
}
|
||||
return new ReadOnlyDictionary<TKey, TElement>(mdict);
|
||||
}
|
||||
|
||||
public static ILookup<TKey, TElement> ToFlatLookup<T, TKey, TElement>(this IEnumerable<T> source, Func<T, TKey> key_selector, Func<T, IEnumerable<TElement>> selector) =>
|
||||
source.SelectMany(v => {
|
||||
var k = key_selector(v);
|
||||
|
||||
@@ -103,6 +103,15 @@ namespace bts.collections {
|
||||
return array;
|
||||
}
|
||||
|
||||
public static bool SafeTryGetAt<T>(this IList<T> list, int index, out T value) {
|
||||
if (list != null && index >= 0 && index < list.Count) {
|
||||
value = list[index];
|
||||
return true;
|
||||
}
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetAt<T>(this IList<T> list, int index, out T value) {
|
||||
if (index >= 0 && index < list.Count) {
|
||||
value = list[index];
|
||||
|
||||
+9
-6
@@ -1,11 +1,11 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace mbts.utils {
|
||||
delegate bool TryGetValue<T>(out T value);
|
||||
delegate bool TryGetValue<TKey, TValue>(TKey key, out TValue value);
|
||||
namespace bts.utils {
|
||||
public delegate bool TryGetValue<T>(out T value);
|
||||
public delegate bool TryGetValue<TKey, TValue>(TKey key, out TValue value);
|
||||
|
||||
static class LogicalDelegates {
|
||||
public static class LogicalDelegates {
|
||||
public static readonly Func<bool, bool, bool>
|
||||
And = (a, b) => a && b,
|
||||
Or = (a, b) => a || b,
|
||||
@@ -13,7 +13,10 @@ namespace mbts.utils {
|
||||
None = (a, b) => !a && !b;
|
||||
}
|
||||
|
||||
static class FormattingDelegates {
|
||||
public static class FormattingDelegates {
|
||||
public static Func<T, string> default_string<T>() =>
|
||||
v => v.ToString();
|
||||
|
||||
public static class Price {
|
||||
public static readonly Func<double, string> Value =
|
||||
p => double.IsNaN(p) ? "" : p.ToString("#,0.00");
|
||||
@@ -26,4 +29,4 @@ namespace mbts.utils {
|
||||
p => p.ToString("#,0.00", CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace bts.utils {
|
||||
public static class FilePathHelper {
|
||||
public static string GetUniquePathWithDate(string path_root, string ext) =>
|
||||
GetUniquePath($"{path_root}_{DateTime.Now:MM - dd - yyyy_hh - mm}", ext);
|
||||
|
||||
public static string GetUniquePath(string path_root, string ext) {
|
||||
var path = $"{path_root}.{ext}";
|
||||
const int max_copies = 2 ^ 14;
|
||||
int i = 1;
|
||||
while (File.Exists(path)) {
|
||||
path = $"{path_root}({i++}).{ext}";
|
||||
if (i == max_copies)
|
||||
throw new InvalidOperationException("Unable to make path unique by concating a number.");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -209,7 +209,7 @@ namespace System.Reactive.Linq {
|
||||
/// <returns>An IDisposable object used to unsubscribe from the source sequence, and dispose the latest value subscription.</returns>
|
||||
public static IDisposable SubscribeEach<T>(this IObservable<T> source, Func<T, IDisposable> subscribe_value) {
|
||||
IDisposable value_sub = null;
|
||||
IDisposable source_sub = source.Select(v => v).Subscribe(on_source);
|
||||
IDisposable source_sub = source.Subscribe(on_source);
|
||||
return Disposable.Create(dispose);
|
||||
// local methods
|
||||
void on_source(T value) {
|
||||
|
||||
Loaded 100 of 235 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user