diff --git a/core/integration/docs/AMM.md b/core/integration/docs/AMM.md index 04d0612e35a..750cee8e766 100644 --- a/core/integration/docs/AMM.md +++ b/core/integration/docs/AMM.md @@ -26,11 +26,11 @@ Lastly, cancelling an existing AMM can be done through: ``` And the parties cancel the following AMM: - | party | market id | method | error | - | party id | market ID | cancellation method | OPTIONAL: error expected | + | party | market id | method | error | + | party id | market ID | CancelAMM_Method | OPTIONAL: error expected | ``` -The possible values for `method` are `METHOD_IMMEDIATE` or `METHOD_REDUCE_ONLY`. Technically `METHOD_UNSPECIFIED` is also a valid value for `method`, but doesn't apply for integration tests. +Details on the [`CancelAMM_Method` type](types.md#Cancel-AMM-Method) ### Checking AMM pools @@ -38,44 +38,29 @@ To see what's going on with an existing AMM, we can check the AMM pool events wi ``` Then the AMM pool status should be: - | party | market id | amount | status | reason | base | lower bound | upper bound | lower leverage | upper leverage | - | party ID | market ID | commitment amout | AMM pool status | OPTIONAL: AMM status reason | uint | uint | uint | float | float | + | party | market id | amount | status | reason | base | lower bound | upper bound | lower leverage | upper leverage | + | party ID | market ID | commitment amout | AMM_Status | AMM_StatusReason | uint | uint | uint | float | float | ``` -Required fields are `party`, `market id`, `amount`, and `status`. All others are optional. possible values for AMM pool status are: +Required fields are `party`, `market id`, `amount`, and `status`. All others are optional. -``` -STATUS_UNSPECIFIED (not applicable) -STATUS_ACTIVE -STATUS_REJECTED -STATUS_CANCELLED -STATUS_STOPPED -STATUS_REDUCE_ONLY -``` - -The possible `AMM status reason` values are: - -``` -STATUS_REASON_UNSPECIFIED -STATUS_REASON_CANCELLED_BY_PARTY -STATUS_REASON_CANNOT_FILL_COMMITMENT -STATUS_REASON_PARTY_ALREADY_OWNS_A_POOL -STATUS_REASON_PARTY_CLOSED_OUT -STATUS_REASON_MARKET_CLOSED -STATUS_REASON_COMMITMENT_TOO_LOW -STATUS_REASON_CANNOT_REBASE -``` +Details on the [`AMM_Status` type](types.md#AMM-Status) +Details on the [`AMM_StatusReason` type](types.md#AMM-Status-Reason) Checking the status for a given AMM only checks the most recent AMMPool event that was emitted. If we need to check all statuses a given AMM passed through during a scenario, use the following step: ``` And the following AMM pool events should be emitted: - | party | market id | amount | status | reason | base | lower bound | upper bound | lower leverage | upper leverage | - | party ID | market ID | commitment amout | AMM pool status | OPTIONAL: AMM status reason | uint | uint | uint | float | float | + | party | market id | amount | status | reason | base | lower bound | upper bound | lower leverage | upper leverage | + | party ID | market ID | commitment amout | AMM_Status | AMM_StatusReason | uint | uint | uint | float | float | ``` The table data is identical to that used in the previous step, with the same optional/required fields. The difference here is that we can check whether the correct events were emitted in a scenario like this: +Details on the [`AMM_Status` type](types.md#AMM-Status) +Details on the [`AMM_StatusReason` type](types.md#AMM-Status-Reason) + + ``` When When the parties submit the following AMM: @@ -154,6 +139,8 @@ And the following transfers should happen: | vamm1-id | ACCOUNT_TYPE_GENERAL | vamm1-id | ACCOUNT_TYPE_MARGIN | ETH/MAR22 | 274 | USD | true | TRANSFER_TYPE_MARGIN_LOW | ``` +For more details on how to check transfer data [see here](transfers.md). + ### Checking AMM trades Because the parties who created the vAMM don't actually trade directly, the derived party ID will appear as the buyer or seller. The account owner alias created above should therefore be used to check the buyer/seller of trades involving the vAMM pools: diff --git a/core/integration/docs/delegation_validator.md b/core/integration/docs/delegation_validator.md new file mode 100644 index 00000000000..e64fe2c68b2 --- /dev/null +++ b/core/integration/docs/delegation_validator.md @@ -0,0 +1,97 @@ +## Integration test framework for delegation and validators. + +### Registering/setting up validators. + +To create/register a validator, the following step should be used: + +```cucumber +Given the validators: + | id | staking account balance | pub_key | + | node1 | 10000 | n1pk | + | node 2 | 20000 | n2pk | +``` + +Where `id` and `staking account balance` are required, the `pub_key` is optional. The types are as follows: + +``` +| id | string | +| staking account balance | uint | +| pub_key | string | +``` + +The step _must_ match the pattern `^the validators:$`. + +### Verifying the delegation balances for a given epoch. + +To validate what the delegation balance is given a party and epoch sequence number, use the following step: + +```cucumber +Then the parties should have the following delegation balances for epoch 3: + | party | node id | amount | + | node1 | node1 | 1000 | + | party1 | node1 | 123 | + | node2 | node2 | 2000 | + | party2 | node2 | 100 | + | party2 | node1 | 100 | +``` +All fields in the table are required and are of the following types: + +``` +| party | string | +| node id | string | +| amount | uint | +``` +The step _must_ match the pattern `^the parties should have the following delegation balances for epoch (\d+):$`. + +### Verifying the validator scores per epoch. + +To check whether or not the validator scores are what we'd expect, use the following step: + +```cucumber +Then the validators should have the following val scores for epoch 1: + | node id | validator score | normalised score | + | node1 | 0.35 | 0.45 | + | node2 | 0.65 | 0.55 | +``` +All fields are required, and have the following types: + +``` +| node id | string | +| validator score | decimal [up to 16 decimals] | +| normalised score | decimal [up to 16 decimals] | +``` +The step _must_ match the pattern `^the validators should have the following val scores for epoch (\d+):$` + +### Verify the rewards received per epoch. + +To validate whether the parties receive the expected rewards for a given epoch, use: + +```cucumber +Then the parties receive the following reward for epoch 5: + | party | asset | amount | + | party1 | TOKEN | 12 | + | party2 | TOKEN | 20 | + | node1 | TOKEN | 100 | + | node2 | TOKEN | 200 | +``` + +All fields are required and of the following types: + +``` +| party | string | +| asset | string | +| amount | uint | +``` +The step _must_ match the pattern `^the parties receive the following reward for epoch (\d+):$` + +### Ensure we are in the expected epoch. + +To make sure the scenario is indeed in the epoch we expect to be in: + +```cucumber +When the current epoch is "2" +``` + +The step _must_ match the pattern `^the current epoch is "([^"]+)"$`. +**NOTE**: the matched, quoted value should be a `uint`, otherwise the scenario will trigger a panic. + diff --git a/core/integration/docs/governance.md b/core/integration/docs/governance.md new file mode 100644 index 00000000000..07eca58e49b --- /dev/null +++ b/core/integration/docs/governance.md @@ -0,0 +1,177 @@ +## Governance placeholders + +The integration test framework does not bootstrap the governance engine, but rather replaces it. It submits proposals directly to the execution engine as though it were a proposal that has gone through governance. This document will cover these quasi governance steps to: + +- [Update a market](#Updating-markets) +- [De-/Re-activate markets](#Governance-auctions) + - [Suspend a market](#Suspending-markets) + - [Resume a market](#Resuming-markets) +- [Terminate a market](#Terminating-markets) +- [Set or change network parameters](#Network-parameters) +- [Update assets](#Updating-assets) + +### Updating markets + +Markets can be updated throughout, some of the paramters that can be changed (things like price monitoring) require setting up new price monitoring paramters (or using a default). How this can be done is outlined in the documentation detailing [setting up markets](markets.md). + +```cucumber +When the markets are updated: + | id | price monitoring | linear slippage factor | sla params | liquidity fee settings | risk model | liquidation strategy | + | ETH/MAR22 | new-pm | 1e-3 | new-sla | new-fee-conf | new-risk-mdl | new-liquidation-strat | +``` + +All fields, bar the ID are treated as optional, and are defined as follows: + +``` +| name | type | NOTE | +| id | string (market ID) | | +| linear slippage factor | float64 | | +| quadratic slippage factor | float64 | deprecated | +| data source config | string (oracle name) | not possible to update the product in all cases | +| price monitoring | string (price monitoring name) | | +| risk model | string (risk model name) | | +| liquidity monitoring | string (liquidity monitoring name) | deprecated | +| sla params | string (sla params name) | | +| liquidity fee settings | string (fee config name) | | +| liquidation strategy | string (liquidation strategy name) | | +| price type | Price_Type | | +| decay weight | decimal | | +| decay power | decimal | | +| cash amount | Uint | | +| source weights | Source_Weights | | +| source staleness tolerance | Staleness_Tolerance | | +| oracle1 | string (composite price oracle name) | | +| oracle2 | string (composite price oracle name) | | +| oracle3 | string (composite price oracle name) | | +| oracle4 | string (composite price oracle name) | | +| oracle5 | string (composite price oracle name) | | +| tick size | uint | | +``` + +Details on the [`Price_Type` type](types.md#Price-type). +Details on the [`Source_Weights` type](types.md#Source-weights) +Details on the [`Staleness_Tolerance` type](types.md#Staleness-tolerance) + +Any field that is not set means that aspect of the market configuration is not to be updated. + +### Governance auctions + +Markets can be put into governance auctions, which can be ended through governance, too. + +#### Suspending markets + +To start a governance auction, the following step is used: + +```cucumber +When the market states are updated through governance: + | market id | state | + | ETH/DEC20 | MARKET_STATE_UPDATE_TYPE_SUSPEND | +``` + +Where the relevant fields are: + +``` +| market id | string (market ID) | required | +| state | MarketStateUpdate | required | +| error | expected error | optional | +``` + +Details on the [`MarketStateUpdate` type](types.md#Market-state-update) + +#### Resuming markets + +To end a goverance auction, the same step is used like so: + +```cucumber +When the market states are updated through governance: + | market id | state | + | ETH/DEC20 | MARKET_STATE_UPDATE_TYPE_RESUME | +``` + +Where the relevant fields are: + +``` +| market id | string (market ID) | required | +| state | MarketStateUpdate | required | +| error | expected error | optional | +``` + +Details on the [`MarketStateUpdate` type](types.md#Market-state-update) + +### Terminating markets + +A market can be terminated through governace, too. This can be done with or without a settlement price: + +```cucumber +When the market states are updated through governance: + | market id | state | settlement price | + | ETH/DEC19 | MARKET_STATE_UPDATE_TYPE_TERMINATE | 976 | +``` + +Where the relevant fields are: + +``` +| market id | string (market ID) | required | +| state | MarketStateUpdate | required | +| settlement price | Uint | optional | +| error | expected error | optional | +``` + +Details on the [`MarketStateUpdate` type](types.md#Market-state-update) + +### Network parameters + +Setting network parameters is typically done as part of the `Background` part of a feature file, or at the start of a scenario. However, changing some network paramters may have an effect on active markets. In that case, a transaction that failed or succeeded previously is expected to behave differently after the network parameters have been updated. This can be useful to test whether or not network paramter changes are correctly propagated. Setting or updating network paramters is done using this step: + +```cucumber +Background: + # setting network parameter to an initial value + Given the following network parameters are set: + | name | value | + | limits.markets.maxPeggedOrders | 2 | + +Scenario: + When the parties place the following pegged orders: + | party | market id | side | volume | pegged reference | offset | error | + | party1 | ETH/DEC24 | buy | 100 | BEST_BID | 5 | | + | party1 | ETH/DEC24 | buy | 200 | BEST_BID | 10 | | + | party1 | ETH/DEC24 | buy | 250 | BEST_BID | 15 | error: too many pegged orders | + + Then the following network parameters are set: + | name | value | + | limits.markets.maxPeggedOrders | 2 | + + When the parties place the following pegged orders: + | party | market id | side | volume | pegged reference | offset | error | + | party1 | ETH/DEC24 | buy | 250 | BEST_BID | 15 | | +``` + +_Note: the error is not necessarily the correct value._ + + +The fields are both required and defined as follows: + +``` +| name | string (the network paramter key name) | +| value | string (must be compatible with the parameter type) | +``` + +### Updating assets + +Similarly to registering assets, it is possible to re-define an existing asset, though this is a rather niche thing to do, using this step: + +```cucumber +When the following assets are updated: + | id | decimal places | quantum | + | BTC | 5 | 20 | +``` + +Where the fields are defined as follows: + +``` +| id | string | required | +| decimal places | uint64 | required | +| quantum | decimal | optional | +``` + +Should this cause an error, the test will fail. diff --git a/core/integration/docs/markets.md b/core/integration/docs/markets.md new file mode 100644 index 00000000000..8548a6fa5de --- /dev/null +++ b/core/integration/docs/markets.md @@ -0,0 +1,559 @@ +## Integration test framework setting up markets. + +Markets are the cornerstone of integration tests, and are rather complex entities to set up. As such, there are a number of parameters that in turn need to be configured through distinct steps. +These sub-components are: + +* [Risk model](#Risk-models) +* [Fee configuration](#Fees-configuration) +* [Oracles for settlement](#Settlement-data) + * [Settlement data oracles with specific decimal places.](#Oracle-decimal-places) +* [Oracles for termination.](#Trading-termination-oracle) +* [Oracles for perpetual markets](#Perpetual-oracles) +* [Oracles for composite price.](#Composite-price-oracles) +* [Price monitoring parameters](#Price-monitoring) +* [Liquidity SLA parameters](#Liquidity-SLA-parameters) +* [Liquidity monitoring parameters](#Liquidity-monitoring-parameters) [No longer used - DEPRECATED] +* [Margin calculators.](#Margin-calculator) +* [Liquidation strategies.](#Liquidation-strategies) + +Arguably not a sub-component, but something that needs to be mentioned in this context: + +* [Asset configuration](#Assets) + +Before covering the actual configuration of a market, this document will first outline how these parameters can be configured. It's important to note that defaults are available for the following: + +* [Fee configuration](#Fees-configuration) +* [Liquidation strats](#Liquidation-strategies) +* Liquidity monitoring [DEPRECATED] +* [Margin calculators](#Margin-calculator) +* [Oracles](#Data-source-configuration) (settlement data, perpetual markets, and market termination). +* [Price monintoring](#Price-monitoring) +* [Risk models](#Risk-models) +* [Liquidity SLA parameters](#Liquidity-SLA-parameters) + +The available defaults will be mentioned under their respective sections, along with details on where the provided defaults can be found. + +Once a market has been set up, the current market state should also be checked. This can be done through: + +* [Market data](#Market-data) +* [Market state](#Market-state) +* [Last market state](#Last-market-state) may sometimes be needed to check markets that have reached a final state. +* [Mark price](#Mark-price) + +The market lifecycle is largely determined through oracles. How to use oracles in integration tests is [covered here](oracles.md). Markets can, however be updated or closed through governance. The integration test framework essentially takes the chain and the governance engine out of the equation, but to test market changes through governance, some steps have been provided. These steps are [covered here](governance.md). + +### Risk models + +There are a number of pre-defined risk models, but if a custom risk model is required, there are steps provided to create one. + +#### Pre-configured risk models + +The pre-configured risk models can be found under `core/integration/steps/market/defaults/risk-model`. The models themselves are split into _simple_ and _log-normal_ models. +The simple risk models only exist to simplify testing, in practice real markets will only use the _log-normal_ risk models. +Risk models (both pre-configured or manually registered) can then be used in a market definition by name. Manually registered risk models are scoped to the feature file (if defined in the `Background` section), or the `Scenario` if defined there. + +The _log-normal_ risk models currently defined are: + +* closeout-st-risk-model +* default-log-normal-risk-model +* default-st-risk-model + +The _simple_ risk models proveded are: + +* system-test-risk-model +* default-simple-risk-model +* default-simple-risk-model-2 +* default-simple-risk-model-3 +* default-simple-risk-model-4 + +#### Creating a risk model. + +If the pre-configured risk models are not sufficient, or you're testing the impact of changes to a risk model, one or more risk models can be configured using one of the following step: + +```cucumber +Given the simple risk model named "my-custom-model": + | long | short | max move up | min move down | probability of trading | + | 0.2 | 0.1 | 100 | -100 | 0.1 | +``` + +Where the fields are all required and have the following types: + +``` +| long | decimal | +| short | decimal | +| max move up | uint | +| min move down | int (must be < 0) | +| probability of trading | decimal | +``` + +This will register a new simple risk model alongside the pre-existing ones with the name `my-custom-model`. To add a _log-normal_ risk model, the following step should be used: + +```cucumber +And the log normal risk model named "custom-logn-model": + | risk aversion | tau | mu | r | sigma | + | 0.0002 | 0.01 | 0 | 0.0 | 1.2 | +``` + +Where the fields are all required and have the following types: + +``` +| risk aversion | decimal | +| tau | decimal | +| mu | decimal | +| r | decimal | +| sigma | decimal | +``` + +### Fees configuration + +Analogous to risk models, fees configuration are pre-defined, but can be configured for specific tests if needed. + +#### Pre-configured fees configuration + + he pre-defined fees configurations can be found in `core/integration/steps/market/defaults/fees-config/`. +Pre-defined fees configurations available are: + +* default-none + +#### Creating custom fees configuration + +To create a fees configuration specific to the feature or scenario, use the following step: + +```cucumber +Given the fees configuration named "custom-fee-config": + | maker fee | infrastructure fee | liquidity fee method | liquidity fee constant | buy back fee | treasury fee | + | 0.0004 | 0.001 | METHOD_CONSTANT | 0 | 0.0001 | 0.00002 | +``` + +Where the fields are defined as: + +``` +| maker fee | required | decimal/string | +| infrastructure fee | required | decimal/string | +| buy back fee | optional | decimal/string (default "0") | +| treasury fee | optional | decimal/string (default "0") | +| liquidity fee constant | optional | decimal/string | +| liquidity fee method | optional | LiquidityFeeMethod (default METHOD_UNSPECIFIED) | +``` + +Details on the [`LiquidityFeeMethod` type](types.md#LiquidityFeeMethod). + +### Price monitoring + +Again, like risk models and fees config, some price monitoring settings are pre-defined, but custom configuration can be used. + +#### Pre-configured price monitoring + +The pre-configured price monitoring parameters can be found in `core/integration/steps/market/defaults/price-monitoring` +Available price monitoring settings are: + +* default-none +* default-basic + +#### Creating custom price monitoring + +To create custom price monitoring config, use the following step: + +```cucumber +Given the price monitoring named "custom-price-monitoring": + | horizon | probability | auction extension | + | 3600 | 0.99 | 3 | + | 7200 | 0.95 | 30 | +``` + +Where fields are required, and the types are: + +``` +| horizon | integer (timestamp) | +| probability | decimal | +| auction extension | integer (seconds) | +``` + +### Liquidity SLA parameters + +Again, pre-defined SLA parameters can be used, or custom parameters can be defined. + +#### Pre-configured liquidity SLA parameters + +The pre-configured liquidity SLA parameters can be found in `core/integration/steps/market/defaults/liquidity-sla-params` +Existing SLA parameters are: + +* default-basic +* default-futures +* default-st + +#### Creating custom lquidity SLA parameters + +To create custom liquidity SLA parameters, use the following step: + +```cucumber +Given the liquidity sla params named "custom-sla-params": + | price range | commitment min time fraction | performance hysteresis epochs | sla competition factor | + | 0.5 | 0.6 | 1 | 1.0 | +``` + +Where the fields are all required and defined as: + +``` +| price range | decimal/string | +| commitment min time fraction | decimal/string | +| performance hysteresis epochs | int64 | +| sla competition factor | decimal | +``` + +### Liquidity monitoring parameters + +Liquidity auctions have been deprecated. The defaults and steps have not yet been removed as they are referenced by some tests, but shouldn't be used in new tests. + +### Margin calculator + +Margin calculators are provided as pre-configured settings, but can be customised like risk, SLA, price monitoring, etc... + +#### Pre-configured margin calculators + +The pre-configured margin calculaters can be found in `core/integration/steps/market/defaults/margin-calculator` +Existing margin calculators are: + +* default-capped-margin-calculator +* default-margin-calculator +* default-overkill-margin-calculator + +#### Creating custom margn calculator + +To create a custom margin calculator, use the following step: + +```cucumber +Given the margin calculator named "custom-margin-calculator": + | search factor | initial factor | release factor | + | 1.2 | 1.5 | 1.7 | +``` + +All fields are required, and defined as `decimal` + +### Liquidation strategies + +As any market sub-configuration type, there are pre-defined liquidation strategies, and custom strategies can be created: + +#### Pre-configured liquidtaion strategies + +The pre-configured liquidation strategies can be found in `core/integration/steps/market/defaults/liquidation-config` +Existing liquidation strategies are: + +* AC-013-strat +* default-liquidation-strat +* legacy-liquidation-strategy +* slow-liquidation-strat + +#### Creating custom liquidation strategies + +Several liquidation strategies can be created using the following step: + +```cucumber +Given the liquidation strategies: + | name | disposal step | disposal fraction | full disposal size | max fraction consumed | disposal slippage range | + | cumstom-liquidation-1 | 10 | 0.1 | 20 | 0.05 | 0.5 | + | cumstom-liquidation-2 | 20 | 0.2 | 50 | 0.02 | 0.4 | +``` + +All fields are required and defined as follows: + +``` +| name | string | +| disposal step | int64 (duration in seconds) | +| disposal fraction | decimal | +| full disposal size | uint64 | +| max fraction consumed | decimal | +| disposal slippage range | decimal | +``` + +### Data source configuration + +Markets rely on oracles for things like settlement price data, or trading temrination (future markets specifically). To create a market, an oracle needs to be configured. quite often a default oracle can be used, but if the test needs to control the oracle, a custom oracle _must_ be configured. + +#### Pre-configured oracles + +Pre-configured oracles can be found in `core/integration/steps/market/defaults/oracle-config` +Existing oracles are: + +* default-eth-for-future +* default-eth-for-perps +* default-dai-for-future +* default-dai-for-perps +* default-usd-for-future +* default-usd-for-perps + +#### Creating custom oracles + +Creating a custom oracle requires a bit more work: + +##### Settlement data + +To create an oracle for settlement data, use the following step: + +```cucumber +Given the oracle spec for settlement data filtering data from "0xCAFECAFE1" named "myOracle": + | property | type | binding | decimals | condition | value | + | prices.ETH.value | TYPE_INTEGER | settlement data | 0 | OPERATOR_GREATER_THAN | 0 | +``` + +Where the fields are defined as: + +``` +| property | required | string | +| type | required | PropertyKey_Type | +| binding | required | string | +| decimals | optional | uint64 | +| condition | optional | Condition_Operator | +| value | optional (required if condition is set) | string (must match type) | +``` + +Details on the [`PropertyKey_Type` type](types.md#PropertyKey_Type). +Details on the [`Condition_Operator` type](types.md#Condition_Operator). + +##### Trading termination oracle + +The same inputs are used for trading termination bindings, but the step looks like this: + +```cucmber +And the oracle spec for trading termination filtering data from "0xCAFECAFE1" named "myOracle": + | property | type | binding | + | trading.terminated | TYPE_BOOLEAN | trading termination | +``` + +Note that the `from` and `named` section of the step matches. This is required. + +##### Oracle decimal places + +Oracles feed data to the system from an external source. The asset used might have 18 decimal places (e.g. ETH), the market could be limited to 10 decimals, and the oracle in turn could supply price data with 12 decimal places. To mimic this, the number of decimal for the oracle can be set using the following step: + +```cucumber +And the settlement data decimals for the oracle named "myOracle" is given in "1" decimal places +``` + +## Creating a basic futures market + +With these components covered/set up, a basic market can now be set up using the step: + +```cucumber +Given the markets: + | id | quote name | asset | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | liquidation-strategy | + | ETH/MAR24 | ETH | ETH | custom-logn-model | custom-margin-calculator | 1 | custom-fee-config | custom-price-monitoring | myOracle | 1e0 | 0 | custom-sla-params | custom-liquidation-1 | +``` + +Note that, when using one of the pre-configured data sources that has a perps counterpart, the test is expected to pass both as a future or a perpetual market. Should this not be the case for whatever reason, the test should be tagged with the `@NoPerp` tag. + +Market configuration is extensive, and can be configured with a myriad of additional (optional) settings. The full list of fields is as follows: + +``` +| field name | required | type | deprecated | +|----------------------------|----------|-----------------------------------------------|------------| +| id | yes | string | | +| quote name | yes | string | | +| asset | yes | string | | +| risk model | yes | string (risk model name) | | +| fees | yes | string (fees-config name) | | +| data source config | yes | string (oracle name) | | +| price monitoring | yes | string (price monitoring name) | | +| margin calculator | yes | string (margin calculator name) | | +| auction duration | yes | int64 (opening auction duration in ticks) | | +| linear slippage factor | yes | float64 | | +| quadratic slippage factor | yes | float64 | yes | +| sla params | yes | string (sla params name) | | +| decimal places | no | integer (default 0) | | +| position decimal places | no | integer (default 0) | | +| liquidity monitoring | no | string (name of liquidity monitoring) | yes | +| parent market id | no | string (ID of other market) | | +| insurance pool fraction | no | decimal (default 0) | | +| successor auction | no | int64 (duration in seconds) | | +| is passed | no | boolean | not used | +| market type | no | string (perp for perpetual market) | | +| liquidation strategy | no | string (liquidation strategy name) | | +| price type | no | Price_Type | | +| decay weight | no | decimal (default 0) | | +| decay power | no | decimal (default 0) | | +| cash amount | no | uint (default 0) | | +| source weights | no | Source_Weights (default 0,0,0,0) | | +| source staleness tolerance | no | Staleness_Tolerance (default 1us,1us,1us,1us) | | +| oracle1 | no | string (composite price oracle name) | | +| oracle2 | no | string (composite price oracle name) | | +| oracle3 | no | string (composite price oracle name) | | +| oracle4 | no | string (composite price oracle name) | | +| oracle5 | no | string (composite price oracle name) | | +| tick size | no | uint (default 1) | | +| max price cap | no | uint | | +| binary | no | boolean (if true, max price cap is required) | | +| fully collateralised | no | boolean (if true, max price cap is required) | | +|----------------------------|----------|-----------------------------------------------|------------| +``` + +Details on the [`Price_Type` type](types.md#Price-type). +Details on the [`Source_Weights` type](types.md#Source-weights) +Details on the [`Staleness_Tolerance` type](types.md#Staleness-tolerance) + +## Optional market config components + +As seen in the table above, there are certain optional parameters that haven't been covered yet. Most notably composite price oracles, oracles for perpetual markets, and assets. + +### Composite price oracles + +There are no default composite price oracles provided, the only way to create one is to define one or more oracles using the following step: + +```cucumber +Given the composite price oracles from "0xCAFECAFE1": + | name | price property | price type | price decimals | + | composite-1 | price.USD.value | TYPE_INTEGER | 0 | + | composite-2 | price.USD.value.2 | TYPE_INTEGER | 0 | +``` + +Where the fields are defined as follows: + +``` +| name | required | string | +| price property | required | string | +| type | required | PropertyKey_Type | +| price decimals | optional | int | +``` + +Details on [`PropertyKey_Type` type](types.md#PropertyKey_Type). + +### Perpetual oracles + +The pre-existing oracles have been covered as part of the data source config section. To create a custom perpetual oracle, a custom oracle can be created using the following step: + +```cucumber +Given the perpetual oracles from "0xCAFECAFE1": + | name | asset | settlement property | settlement type | schedule property | schedule type | margin funding factor | interest rate | clamp lower bound | clamp upper bound | quote name | settlement decimals | source weights | source staleness tolerance | price type | + | perp-oracle | ETH | perp.ETH.value | TYPE_INTEGER | perp.funding.cue | TYPE_TIMESTAMP | 0 | 0 | 0 | 0 | ETH | 18 | 1,0,0,0 | 100s,0s,0s,0s | weight | +``` + +Where the fields are defined as follows: + +``` +| field | required | type | +|-----------------------------|----------|-------------------------------------------------------| +| name | yes | string | +| asset | yes | string (asset ID) | +| settlement property | yes | string | +| settlement type | yes | PropertyKey_Type | +| schedule property | yes | string | +| schedule type | yes | PropertyKey_Type | +| settlement decimals | no | uint64 | +| margin fundgin factor | no | decimal (default 0) | +| interest rate | no | decimal (default 0) | +| clamp lower bound | no | decimal (default 0) | +| clamp upper bound | no | decimal (default 0) | +| quote name | no | string (asset ID) | +| funding rate scaling factor | no | decimal | +| funding rate lower bound | no | decimal | +| funding rate upper bound | no | decimal | +| decay weight | no | decimal (default 0) | +| decay power | no | decimal (default 0) | +| cash amount | no | decimal | +| source weights | no | Source_Weights | +| source staleness tolerance | no | Staleness_Tolerance (default 1000s,1000s,1000s,1000s) | +| price type | no | Price_Type | +``` + +Details on the [`Price_Type` type](types.md#Price-type). +Details on the [`Source_Weights` type](types.md#Source-weights) +Details on the [`Staleness_Tolerance` type](types.md#Staleness-tolerance) + +### Assets + +It is not required to define an asset prior to using it in a market. If you create a market with an non-existing asset, the asset will be created ad-hoc, with the same number of decimal places as the market. As mentioned earlier, though, actual markets may have less decimal places than the asset they use. To make it possible to test whether or not the system handles these scenario's as expected, it's possible to configure an asset with a specific number of decimal places using the following step: + +```cucumber +Given the following assets are registered: + | id | decimal places | quantum | + | ETH | 18 | 10 | + | DAI | 18 | 1 | + | USDT | 10 | 2.3 | +``` + +Where the fields are defined as follows: + +``` +| id | string | required | +| decimal places | uint64 | required | +| quantum | decimal | optional | +``` + +## Checking the market state + +### Market data + +The most comprehensive way to check the market state is by checking the last `MarketData` event. This is done using the following step + +```cucumber +Then the market data for the market "ETH/MAR24" should be: + | mark price | trading mode | horizon | min bound | max bound | target stake | supplied stake | open interest | + | 1000 | TRADING_MODE_CONTINUOUS | 3600 | 973 | 1027 | 3556 | 100000 | 1 | +``` + +All fields are treated as optional, and are defined as follows: + +``` +| market | string | +| best bid price | Uint | +| best bid volume | uint64 | +| best offer price | Uint | +| best offer volume | uint64 | +| best static bid price | Uint | +| best static bid volume | uint64 | +| best static offer price | Uint | +| best static offer volume | uint64 | +| mid price | Uint | +| static mid price | Uint | +| mark price | Uint | +| last traded price | Uint | +| timestamp | int64 (timestamp) | +| open interest | uint64 | +| indicative price | Uint | +| indicative volume | uint64 | +| auction start | int64 (timestamp) | +| auction end | int64 (timestamp) | +| trading mode | Market_TradingMode | +| auction trigger | AuctionTrigger | +| extension trigger | AuctionTrigger | +| target stake | Uint | +| supplied stake | Uint | +| horizon | int64 (duration in seconds) | +| ref price | Uint (reference price) | +| min bound | Uint | +| max bound | Uint | +| market value proxy | Uint | +| average entry valuation | Uint | +| party | string | +| equity share | decimal | +``` + +Details on the [`Market_TradingMode` type](types.md#Trading-mode) +Details on the [`AuctionTrigger` type](types.md#Auction-trigger) + +### Market state + +To check what the current `Market_state` of a given market is, the following step should be used: + +```cucumber +Then the market state should be "STATE_ACTIVE" for the market "ETH/DEC22" +``` + +Details on the [`Market_State` type](types.md#Market-state) + +### Last market state + +Similarly to checking the market state for an active market, should a market have settled or succeeded, we can check the last market state event pertaining to that market using: + +```cucumber +Then the last market state should be "STATE_CANCELLED" for the market "ETH/JAN23" +``` + +Details on the [`Market_State` type](types.md#Market-state) + +### Mark price + +To quickly check what the current mark price is: + +```cucumber +Then the mark price should be "1000" for the market "ETH/FEB23" +``` diff --git a/core/integration/docs/oracles.md b/core/integration/docs/oracles.md new file mode 100644 index 00000000000..6aad907e19d --- /dev/null +++ b/core/integration/docs/oracles.md @@ -0,0 +1,56 @@ +## Using oracles + +Sending oracle data for any oracle is done using the property name, and the public key as set up previously in the test. How oracles are set up is covered [in the markets documentation](markets.md#Data-source-configuration). + +The public key, in all setup steps is the value specified as _[...] from "0xCAFECAFE1"_. The public key, then, is `0xCAFECAFE1`. + +### Sending simple signal + +In its simplest form, an oracle can be triggered to send a data signal using the following step: + +```cucumber +When the oracles broadcast data signed with "0xCAFECAFE1": + | name | value | eth-block-time | + | prices.ETH.value | 23456789 | 1725643814 | +``` + +Where the fields are defined as follows: + +``` +| name | string | +| value | string (compatible with PropertyKey_Type) | +| eth-block-time | timestamp - optional | +``` + +Details on the [`PropertyKey_Type` type](types.md#PropertyKey_Type). + +For settlement data, the eth-block-time doesn't matter too much. This value, however, is useful for perpetual markets where the time-weighted average values matter a lot. + +It is possible to broadcast the same data using multiple keys (e.g. where 2 markets are configured using the same property key, but with different signers). In the keys can simply be comma-separated in the step: + +```cucumber +When the oracles broadcast data signed with "0xCAFECAFE1,0xCAFECAFE2,0xCAFECAFE3": + | name | value | eth-block-time | + | prices.ETH.value | 23456789 | 1725643814 | +``` + +### Sending a signal relative to the current block time + +For perpetual markets, specifically funding payments, we want to be able to control when (relative to the current block time in the test), certain data-points were received like so: + +```cucumber +When the oracles broadcast data with block time signed with "0xCAFECAFE1": + | name | value | time offset | + | perp.funding.cue | 1511924180 | -100s | + | perp.ETH.value | 975 | -2s | + | perp.ETH.value | 977 | -1s | +``` + +Other than that, the step works similarly to the previous one discussed fields are defined as follows: + +``` +| name | string | +| value | string (compatible with PropertyKey_Type) | +| time offset | duration | +| eth-block-time | timestamp - optional | +``` diff --git a/core/integration/docs/positions.md b/core/integration/docs/positions.md new file mode 100644 index 00000000000..24da92168a8 --- /dev/null +++ b/core/integration/docs/positions.md @@ -0,0 +1,38 @@ +## Verifying positions and PnL + +At its heart, we are testing a trading platform. Therefore, we need to have the ability to verify the positions held by traders, and their realised or unrealised profit and loss (PnL). The data-node component implements a positions API which collates data events sent out when parties trade, positions are marked to market or settled, when perpetual markets process a funding payment, etc... +The integration test framework implements a number of steps to verify whether or not a trade took place, verifying [individual transfer data](transfers.md). In addition to this, the integration test framework implements a step that allows us to verify the position data analogous to the data-node API. + +```cucumber +Then the parties should have the following profit and loss: + | market id | party | volume | unrealised pnl | realised pnl | status | taker fees | taker fees since | maker fees | maker fees since | other fees | other fees since | funding payments | funding payments since | + | ETH/DEC19 | trader2 | 0 | 0 | 0 | POSITION_STATUS_ORDERS_CLOSED | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + | ETH/DEC19 | trader3 | 0 | 0 | -162 | POSITION_STATUS_CLOSED_OUT | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + | ETH/DEC19 | auxiliary1 | -10 | -900 | 0 | | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + | ETH/DEC19 | auxiliary2 | 5 | 475 | 586 | | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | + ``` + + Where the fields are defined as follows: + + ``` + | name | type | required | + |------------------------|----------------|----------| + | market id | string | no | + | party | string | yes | + | volume | int64 | yes | + | unrealised pnl | Int | yes | + | realised pnl | Int | yes | + | status | PositionStatus | no | + | taker fees | Uint | no | + | maker fees | Uint | no | + | other fees | Uint | no | + | taker fees since | Uint | no | + | maker fees since | Uint | no | + | other fees since | Uint | no | + | funding payments | Int | no | + | funding payments since | Int | no | + | is amm | bool | no | + ``` + +Details for the [`PositionStatus` type](types.md#Position-status) + diff --git a/core/integration/docs/transfers.md b/core/integration/docs/transfers.md new file mode 100644 index 00000000000..6e1fd72f891 --- /dev/null +++ b/core/integration/docs/transfers.md @@ -0,0 +1,44 @@ +## Validating transfers + +To ensure the correct amounts are transferred to and from the correct accounts, we use the following step: + +```cucumber +Then the following transfers should happen: + | type | from | to | from account | to account | market id | amount | asset | + | TRANSFER_TYPE_PERPETUALS_FUNDING_LOSS | trader1 | market | ACCOUNT_TYPE_MARGIN | ACCOUNT_TYPE_SETTLEMENT | ETH/DEC19 | 700000000 | ETH | + | TRANSFER_TYPE_PERPETUALS_FUNDING_WIN | market | trader2 | ACCOUNT_TYPE_SETTLEMENT | ACCOUNT_TYPE_MARGIN | ETH/DEC19 | 700000000 | ETH | + | TRANSFER_TYPE_PERPETUALS_FUNDING_LOSS | trader3 | market | ACCOUNT_TYPE_MARGIN | ACCOUNT_TYPE_SETTLEMENT | ETH/DEC19 | 1400000000 | ETH | + | TRANSFER_TYPE_PERPETUALS_FUNDING_WIN | market | trader4 | ACCOUNT_TYPE_SETTLEMENT | ACCOUNT_TYPE_MARGIN | ETH/DEC19 | 1400000000 | ETH | + | TRANSFER_TYPE_INFRASTRUCTURE_FEE_DISTRIBUTE | party3 | | ACCOUNT_TYPE_GENERAL | ACCOUNT_TYPE_FEES_INFRASTRUCTURE | | 50 | ETH | +``` + +With the fields being defined as follows: + +``` +| field | required | type | +| from | yes | string (party or market ID, blank for system) | +| to | yes | string(party or market ID, blank for system) | +| from account | yes | ACCOUNT_TYPE | +| to account | yes | ACCOUNT_TYPE | +| market id | yes | string (blank for general/system account) | +| asset | yes | string (asset ID) | +| type | no | TRANSFER_TYPE | +| is amm | no | boolean (true if either from or to is an AMM subkey) | +``` + +Details for the [`ACCOUNT_TYPE` type](types.md#Account-type) +Details for the [`TRANSFER_TYPE` type](types.md#Transfer-type) + +## Debugging transfers + +To diagnose a problem, it can be useful to dump all the transfers that happened up to that particular point in a given test. To do this, simply add the following: + +```cucumber +Then debug transfers +``` + +This will simply print out all transfer events using the following format: + +```go +fmt.Sprintf("\t|%38s |%85s |%85s |%19s |\n", v.Type, v.FromAccount, v.ToAccount, v.Amount) +``` diff --git a/core/integration/docs/types.md b/core/integration/docs/types.md new file mode 100644 index 00000000000..ca867b1c8f1 --- /dev/null +++ b/core/integration/docs/types.md @@ -0,0 +1,342 @@ +# List of special types + +Below is a list of types used in the docs and the possible values + +## Position status + +Possible values for `PositionStatus` are: + +* POSITION_STATUS_UNSPECIFIED +* POSITION_STATUS_ORDERS_CLOSED +* POSITION_STATUS_CLOSED_OUT +* POSITION_STATUS_DISTRESSED + +## Auction trigger + +Possible values for `AuctionTrigger` are: + +* AUCTION_TRIGGER_UNSPECIFIED +* AUCTION_TRIGGER_BATCH +* AUCTION_TRIGGER_OPENING +* AUCTION_TRIGGER_PRICE +* AUCTION_TRIGGER_LIQUIDITY +* AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET +* AUCTION_TRIGGER_UNABLE_TO_DEPLOY_LP_ORDERS +* AUCTION_TRIGGER_GOVERNANCE_SUSPENSION +* AUCTION_TRIGGER_LONG_BLOCK + +## Trading mode + +Possible values for `Market_TradingMode` are: + +* TRADING_MODE_UNSPECIFIED +* TRADING_MODE_CONTINUOUS +* TRADING_MODE_BATCH_AUCTION +* TRADING_MODE_OPENING_AUCTION +* TRADING_MODE_MONITORING_AUCTION +* TRADING_MODE_NO_TRADING +* TRADING_MODE_SUSPENDED_VIA_GOVERNANCE +* TRADING_MODE_LONG_BLOCK_AUCTION + +## Market state update + +Possible values for `MarketStateUpdate` are: + +* MARKET_STATE_UPDATE_TYPE_UNSPECIFIED +* MARKET_STATE_UPDATE_TYPE_TERMINATE +* MARKET_STATE_UPDATE_TYPE_SUSPEND +* MARKET_STATE_UPDATE_TYPE_RESUME + +## Market state + +Possible values for `Market_State` are: + +* STATE_UNSPECIFIED +* STATE_PROPOSED +* STATE_REJECTED +* STATE_PENDING +* STATE_CANCELLED +* STATE_ACTIVE +* STATE_SUSPENDED +* STATE_CLOSED +* STATE_TRADING_TERMINATED +* STATE_SETTLED +* STATE_SUSPENDED_VIA_GOVERNANCE + +## Order error + +Possible values for `ORDER_ERROR` are: + +* ORDER_ERROR_UNSPECIFIED +* ORDER_ERROR_INVALID_MARKET_ID +* ORDER_ERROR_INVALID_ORDER_ID +* ORDER_ERROR_OUT_OF_SEQUENCE +* ORDER_ERROR_INVALID_REMAINING_SIZE +* ORDER_ERROR_TIME_FAILURE +* ORDER_ERROR_REMOVAL_FAILURE +* ORDER_ERROR_INVALID_EXPIRATION_DATETIME +* ORDER_ERROR_INVALID_ORDER_REFERENCE +* ORDER_ERROR_EDIT_NOT_ALLOWED +* ORDER_ERROR_AMEND_FAILURE +* ORDER_ERROR_NOT_FOUND +* ORDER_ERROR_INVALID_PARTY_ID +* ORDER_ERROR_MARKET_CLOSED +* ORDER_ERROR_MARGIN_CHECK_FAILED +* ORDER_ERROR_MISSING_GENERAL_ACCOUNT +* ORDER_ERROR_INTERNAL_ERROR +* ORDER_ERROR_INVALID_SIZE +* ORDER_ERROR_INVALID_PERSISTENCE +* ORDER_ERROR_INVALID_TYPE +* ORDER_ERROR_SELF_TRADING +* ORDER_ERROR_INSUFFICIENT_FUNDS_TO_PAY_FEES +* ORDER_ERROR_INCORRECT_MARKET_TYPE +* ORDER_ERROR_INVALID_TIME_IN_FORCE +* ORDER_ERROR_CANNOT_SEND_GFN_ORDER_DURING_AN_AUCTION +* ORDER_ERROR_CANNOT_SEND_GFA_ORDER_DURING_CONTINUOUS_TRADING +* ORDER_ERROR_CANNOT_AMEND_TO_GTT_WITHOUT_EXPIRYAT +* ORDER_ERROR_EXPIRYAT_BEFORE_CREATEDAT +* ORDER_ERROR_CANNOT_HAVE_GTC_AND_EXPIRYAT +* ORDER_ERROR_CANNOT_AMEND_TO_FOK_OR_IOC +* ORDER_ERROR_CANNOT_AMEND_TO_GFA_OR_GFN +* ORDER_ERROR_CANNOT_AMEND_FROM_GFA_OR_GFN +* ORDER_ERROR_CANNOT_SEND_IOC_ORDER_DURING_AUCTION +* ORDER_ERROR_CANNOT_SEND_FOK_ORDER_DURING_AUCTION +* ORDER_ERROR_MUST_BE_LIMIT_ORDER +* ORDER_ERROR_MUST_BE_GTT_OR_GTC +* ORDER_ERROR_WITHOUT_REFERENCE_PRICE +* ORDER_ERROR_BUY_CANNOT_REFERENCE_BEST_ASK_PRICE +* ORDER_ERROR_OFFSET_MUST_BE_GREATER_OR_EQUAL_TO_ZERO +* ORDER_ERROR_SELL_CANNOT_REFERENCE_BEST_BID_PRICE +* ORDER_ERROR_OFFSET_MUST_BE_GREATER_THAN_ZERO +* ORDER_ERROR_INSUFFICIENT_ASSET_BALANCE +* ORDER_ERROR_CANNOT_AMEND_PEGGED_ORDER_DETAILS_ON_NON_PEGGED_ORDER +* ORDER_ERROR_UNABLE_TO_REPRICE_PEGGED_ORDER +* ORDER_ERROR_UNABLE_TO_AMEND_PRICE_ON_PEGGED_ORDER +* ORDER_ERROR_NON_PERSISTENT_ORDER_OUT_OF_PRICE_BOUNDS +* ORDER_ERROR_TOO_MANY_PEGGED_ORDERS +* ORDER_ERROR_POST_ONLY_ORDER_WOULD_TRADE +* ORDER_ERROR_REDUCE_ONLY_ORDER_WOULD_NOT_REDUCE_POSITION +* ORDER_ERROR_ISOLATED_MARGIN_CHECK_FAILED +* ORDER_ERROR_PEGGED_ORDERS_NOT_ALLOWED_IN_ISOLATED_MARGIN_MODE +* ORDER_ERROR_PRICE_NOT_IN_TICK_SIZE +* ORDER_ERROR_PRICE_MUST_BE_LESS_THAN_OR_EQUAL_TO_MAX_PRICE + + +## Account type + +Possible values for `ACCOUNT_TYPE` are: + +* ACCOUNT_TYPE_UNSPECIFIED +* ACCOUNT_TYPE_INSURANCE +* ACCOUNT_TYPE_SETTLEMENT +* ACCOUNT_TYPE_MARGIN +* ACCOUNT_TYPE_GENERAL +* ACCOUNT_TYPE_FEES_INFRASTRUCTURE +* ACCOUNT_TYPE_FEES_LIQUIDITY +* ACCOUNT_TYPE_FEES_MAKER +* ACCOUNT_TYPE_BOND +* ACCOUNT_TYPE_EXTERNAL +* ACCOUNT_TYPE_GLOBAL_INSURANCE +* ACCOUNT_TYPE_GLOBAL_REWARD +* ACCOUNT_TYPE_PENDING_TRANSFERS +* ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES +* ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES +* ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES +* ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS +* ACCOUNT_TYPE_HOLDING +* ACCOUNT_TYPE_LP_LIQUIDITY_FEES +* ACCOUNT_TYPE_LIQUIDITY_FEES_BONUS_DISTRIBUTION +* ACCOUNT_TYPE_NETWORK_TREASURY +* ACCOUNT_TYPE_VESTING_REWARDS +* ACCOUNT_TYPE_VESTED_REWARDS +* ACCOUNT_TYPE_REWARD_RELATIVE_RETURN +* ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY +* ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING +* ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD +* ACCOUNT_TYPE_ORDER_MARGIN +* ACCOUNT_TYPE_REWARD_REALISED_RETURN +* ACCOUNT_TYPE_BUY_BACK_FEES +* ACCOUNT_TYPE_REWARD_AVERAGE_NOTIONAL +* ACCOUNT_TYPE_REWARD_ELIGIBLE_ENTITIES + +For in some debug output, particularly when the accounts types are used as part of an account ID, these constants will be shown as a numeric value, the mapping is as follows: + +* 0: ACCOUNT_TYPE_UNSPECIFIED +* 1: ACCOUNT_TYPE_INSURANCE +* 2: ACCOUNT_TYPE_SETTLEMENT +* 3: ACCOUNT_TYPE_MARGIN +* 4: ACCOUNT_TYPE_GENERAL +* 5: ACCOUNT_TYPE_FEES_INFRASTRUCTURE +* 6: ACCOUNT_TYPE_FEES_LIQUIDITY +* 7: ACCOUNT_TYPE_FEES_MAKER +* 9: ACCOUNT_TYPE_BOND +* 10: ACCOUNT_TYPE_EXTERNAL +* 11: ACCOUNT_TYPE_GLOBAL_INSURANCE +* 12: ACCOUNT_TYPE_GLOBAL_REWARD +* 13: ACCOUNT_TYPE_PENDING_TRANSFERS +* 14: ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES +* 15: ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES +* 16: ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES +* 17: ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS +* 18: ACCOUNT_TYPE_HOLDING +* 19: ACCOUNT_TYPE_LP_LIQUIDITY_FEES +* 20: ACCOUNT_TYPE_LIQUIDITY_FEES_BONUS_DISTRIBUTION +* 21: ACCOUNT_TYPE_NETWORK_TREASURY +* 22: ACCOUNT_TYPE_VESTING_REWARDS +* 23: ACCOUNT_TYPE_VESTED_REWARDS +* 25: ACCOUNT_TYPE_REWARD_RELATIVE_RETURN +* 26: ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY +* 27: ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING +* 28: ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD +* 29: ACCOUNT_TYPE_ORDER_MARGIN +* 30: ACCOUNT_TYPE_REWARD_REALISED_RETURN +* 31: ACCOUNT_TYPE_BUY_BACK_FEES +* 32: ACCOUNT_TYPE_REWARD_AVERAGE_NOTIONAL +* 33: ACCOUNT_TYPE_REWARD_ELIGIBLE_ENTITIES + +## Transfer type + +Possible values for `TRANSFER_TYPE` are: + +* TRANSFER_TYPE_UNSPECIFIED +* TRANSFER_TYPE_LOSS +* TRANSFER_TYPE_WIN +* TRANSFER_TYPE_MTM_LOSS +* TRANSFER_TYPE_MTM_WIN +* TRANSFER_TYPE_MARGIN_LOW +* TRANSFER_TYPE_MARGIN_HIGH +* TRANSFER_TYPE_MARGIN_CONFISCATED +* TRANSFER_TYPE_MAKER_FEE_PAY +* TRANSFER_TYPE_MAKER_FEE_RECEIVE +* TRANSFER_TYPE_INFRASTRUCTURE_FEE_PAY +* TRANSFER_TYPE_INFRASTRUCTURE_FEE_DISTRIBUTE +* TRANSFER_TYPE_LIQUIDITY_FEE_PAY +* TRANSFER_TYPE_LIQUIDITY_FEE_DISTRIBUTE +* TRANSFER_TYPE_BOND_LOW +* TRANSFER_TYPE_BOND_HIGH +* TRANSFER_TYPE_WITHDRAW +* TRANSFER_TYPE_DEPOSIT +* TRANSFER_TYPE_BOND_SLASHING +* TRANSFER_TYPE_REWARD_PAYOUT +* TRANSFER_TYPE_TRANSFER_FUNDS_SEND +* TRANSFER_TYPE_TRANSFER_FUNDS_DISTRIBUTE +* TRANSFER_TYPE_CLEAR_ACCOUNT +* TRANSFER_TYPE_CHECKPOINT_BALANCE_RESTORE +* TRANSFER_TYPE_SPOT +* TRANSFER_TYPE_HOLDING_LOCK +* TRANSFER_TYPE_HOLDING_RELEASE +* TRANSFER_TYPE_SUCCESSOR_INSURANCE_FRACTION +* TRANSFER_TYPE_LIQUIDITY_FEE_ALLOCATE +* TRANSFER_TYPE_LIQUIDITY_FEE_NET_DISTRIBUTE +* TRANSFER_TYPE_SLA_PENALTY_BOND_APPLY +* TRANSFER_TYPE_SLA_PENALTY_LP_FEE_APPLY +* TRANSFER_TYPE_LIQUIDITY_FEE_UNPAID_COLLECT +* TRANSFER_TYPE_SLA_PERFORMANCE_BONUS_DISTRIBUTE +* TRANSFER_TYPE_PERPETUALS_FUNDING_LOSS +* TRANSFER_TYPE_PERPETUALS_FUNDING_WIN +* TRANSFER_TYPE_REWARDS_VESTED +* TRANSFER_TYPE_FEE_REFERRER_REWARD_PAY +* TRANSFER_TYPE_FEE_REFERRER_REWARD_DISTRIBUTE +* TRANSFER_TYPE_ORDER_MARGIN_LOW +* TRANSFER_TYPE_ORDER_MARGIN_HIGH +* TRANSFER_TYPE_ISOLATED_MARGIN_LOW +* TRANSFER_TYPE_ISOLATED_MARGIN_HIGH +* TRANSFER_TYPE_AMM_LOW +* TRANSFER_TYPE_AMM_HIGH +* TRANSFER_TYPE_AMM_RELEASE +* TRANSFER_TYPE_TREASURY_FEE_PAY +* TRANSFER_TYPE_BUY_BACK_FEE_PAY +* TRANSFER_TYPE_HIGH_MAKER_FEE_REBATE_PAY +* TRANSFER_TYPE_HIGH_MAKER_FEE_REBATE_RECEIVE + + +## Cancel AMM Method + +Possible values for `CancelAMM_Method` are: + +* METHOD_UNSPECIFIED +* METHOD_IMMEDIATE +* METHOD_REDUCE_ONLY + +## AMM Status + +Possible values for the `AMM_Status` are: + +* STATUS_UNSPECIFIED +* STATUS_ACTIVE +* STATUS_REJECTED +* STATUS_CANCELLED +* STATUS_STOPPED +* STATUS_REDUCE_ONLY + +## AMM Status Reason + +Possible values for the `AMM_StatusReason` are: + +* STATUS_REASON_UNSPECIFIED +* STATUS_REASON_CANCELLED_BY_PARTY +* STATUS_REASON_CANNOT_FILL_COMMITMENT +* STATUS_REASON_PARTY_ALREADY_OWNS_A_POOL +* STATUS_REASON_PARTY_CLOSED_OUT +* STATUS_REASON_MARKET_CLOSED +* STATUS_REASON_COMMITMENT_TOO_LOW +* STATUS_REASON_CANNOT_REBASE + +## PropertyKey_Type + +Possible values for `PropertyKey_Type` are: + +* TYPE_UNSPECIFIED +* TYPE_INTEGER +* TYPE_STRING +* TYPE_EMPTY +* TYPE_BOOLEAN +* TYPE_DECIMAL +* TYPE_TIMESTAMP + +## Condition_Operator + +Possible values for `Condition_Operator` are: + +* OPERATOR_UNSPECIFIED +* OPERATOR_EQUALS +* OPERATOR_LESS_THAN +* OPERATOR_LESS_THAN_OR_EQUAL +* OPERATOR_GREATER_THAN +* OPERATOR_GREATER_THAN_OR_EQUAL + +## LiquidityFeeMethod + +Possible values for `LiquidityFeeMethod` are: + +* METHOD_UNSPECIFIED +* METHOD_CONSTANT +* METHOD_MARGINAL_COST +* METHOD_WEIGHTED_AVERAGE + +## Price type + +Possible values for price type are: + +* last trade +* median +* weight + +## Source weights + +The source weight type takes the form: + +`decimal,decimal,decimal,decimal` + +And usually defaults to: + +`0,0,0,0` + +## Staleness tolerance + +Staleness tolerance, analogous to [source weights](#Source-weights) takes the form: + +`duration,duration,duration,duration` + +Its defaults to either four times `1us`, or `1000s` +Valid inputs look like: `10s,1ms,0s,0s` or `1000s,0s,0s,0s` diff --git a/core/integration/features/zero-position.feature b/core/integration/features/zero-position.feature index 1cc077f8f77..37b2d31bb46 100644 --- a/core/integration/features/zero-position.feature +++ b/core/integration/features/zero-position.feature @@ -142,11 +142,11 @@ Feature: Closeout scenarios | trader3 | USD | ETH/DEC19 | 0 | 0 | Then the parties should have the following profit and loss: - | party | volume | unrealised pnl | realised pnl | status | - | trader2 | 0 | 0 | 0 | POSITION_STATUS_ORDERS_CLOSED | - | trader3 | 0 | 0 | -162 | POSITION_STATUS_CLOSED_OUT | - | auxiliary1 | -10 | -900 | 0 | | - | auxiliary2 | 5 | 475 | 586 | | + | party | volume | unrealised pnl | realised pnl | status | taker fees | taker fees since | maker fees | maker fees since | other fees | other fees since | funding payments | funding payments since | + | trader2 | 0 | 0 | 0 | POSITION_STATUS_ORDERS_CLOSED | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + | trader3 | 0 | 0 | -162 | POSITION_STATUS_CLOSED_OUT | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + | auxiliary1 | -10 | -900 | 0 | | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + | auxiliary2 | 5 | 475 | 586 | | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | And the insurance pool balance should be "0" for the market "ETH/DEC19" When the parties place the following orders: | party | market id | side | price | volume | resulting trades | type | tif | reference | diff --git a/core/integration/steps/parties_should_have_the_following_profit_and_loss.go b/core/integration/steps/parties_should_have_the_following_profit_and_loss.go index 3fe5c2d066e..02db8ee82e8 100644 --- a/core/integration/steps/parties_should_have_the_following_profit_and_loss.go +++ b/core/integration/steps/parties_should_have_the_following_profit_and_loss.go @@ -48,6 +48,8 @@ func positionAPIProduceTheFollowingRow(exec Execution, positionService *plugins. var pos []*types.Position // check position status if needed ps, checkPS := row.positionState() + checkFees := row.checkFees() + checkFunding := row.checkFunding() party := row.party() readableParty := party if row.isAMM() { @@ -79,12 +81,22 @@ func positionAPIProduceTheFollowingRow(exec Execution, positionService *plugins. } if areSamePosition(pos, row) { - if !checkPS { + if !checkFees && !checkFunding && !checkPS { return nil } - // check state if required - states, _ := positionService.GetPositionStatesByParty(party) - if len(states) == 1 && states[0] == ps { + match := true + if checkFees { + match = feesMatch(pos, row) + } + if checkFunding && match { + match = fundingMatches(pos, row) + } + if checkPS && match { + // check state if required + states, _ := positionService.GetPositionStatesByParty(party) + match = len(states) == 1 && states[0] == ps + } + if match { return nil } } @@ -109,15 +121,19 @@ func errProfitAndLossValuesForParty(pos []*types.Position, row pnlRow) error { } return formatDiff( fmt.Sprintf("invalid positions values for party(%v)", row.party()), + row.diffMap(), map[string]string{ - "volume": i64ToS(row.volume()), - "unrealised PNL": row.unrealisedPNL().String(), - "realised PNL": row.realisedPNL().String(), - }, - map[string]string{ - "volume": i64ToS(pos[0].OpenVolume), - "unrealised PNL": pos[0].UnrealisedPnl.String(), - "realised PNL": pos[0].RealisedPnl.String(), + "volume": i64ToS(pos[0].OpenVolume), + "unrealised PNL": pos[0].UnrealisedPnl.String(), + "realised PNL": pos[0].RealisedPnl.String(), + "taker fees": pos[0].TakerFeesPaid.String(), + "taker fees since": pos[0].TakerFeesPaidSince.String(), + "maker fees": pos[0].MakerFeesReceived.String(), + "maker fees since": pos[0].MakerFeesReceivedSince.String(), + "other fees": pos[0].FeesPaid.String(), + "other fees since": pos[0].FeesPaidSince.String(), + "funding payments": pos[0].FundingPaymentAmount.String(), + "funding payments since": pos[0].FundingPaymentAmountSince.String(), }, ) } @@ -133,6 +149,52 @@ func areSamePosition(pos []*types.Position, row pnlRow) bool { pos[0].UnrealisedPnl.Equals(row.unrealisedPNL()) } +func feesMatch(pos []*types.Position, row pnlRow) bool { + if len(pos) == 0 { + return false + } + taker, ok := row.takerFees() + if ok && !taker.EQ(pos[0].TakerFeesPaid) { + return false + } + maker, ok := row.makerFees() + if ok && !maker.EQ(pos[0].MakerFeesReceived) { + return false + } + other, ok := row.otherFees() + if ok && !other.EQ(pos[0].FeesPaid) { + return false + } + taker, ok = row.takerFeesSince() + if ok && !taker.EQ(pos[0].TakerFeesPaidSince) { + return false + } + maker, ok = row.makerFeesSince() + if ok && !maker.EQ(pos[0].MakerFeesReceivedSince) { + return false + } + other, ok = row.otherFeesSince() + if ok && !other.EQ(pos[0].FeesPaidSince) { + return false + } + return true +} + +func fundingMatches(pos []*types.Position, row pnlRow) bool { + if len(pos) == 0 { + return false + } + fp, ok := row.fundingPayment() + if ok && !fp.EQ(pos[0].FundingPaymentAmount) { + return false + } + fp, ok = row.fundingPaymentSince() + if ok && !fp.EQ(pos[0].FundingPaymentAmountSince) { + return false + } + return true +} + func errCannotGetPositionForParty(party string, err error) error { return fmt.Errorf("error getting party position, party(%v), err(%v)", party, err) } @@ -147,6 +209,14 @@ func parseProfitAndLossTable(table *godog.Table) []RowWrapper { "status", "market id", "is amm", + "taker fees", + "taker fees since", + "maker fees", + "maker fees since", + "other fees", + "other fees since", + "funding payments", + "funding payments since", }) } @@ -191,3 +261,130 @@ func (r pnlRow) isAMM() bool { } return r.row.MustBool("is amm") } + +func (r pnlRow) takerFees() (*num.Uint, bool) { + if !r.row.HasColumn("taker fees") { + return nil, false + } + return r.row.MustUint("taker fees"), true +} + +func (r pnlRow) takerFeesSince() (*num.Uint, bool) { + if !r.row.HasColumn("taker fees since") { + return nil, false + } + return r.row.MustUint("taker fees since"), true +} + +func (r pnlRow) makerFees() (*num.Uint, bool) { + if !r.row.HasColumn("maker fees") { + return nil, false + } + return r.row.MustUint("maker fees"), true +} + +func (r pnlRow) makerFeesSince() (*num.Uint, bool) { + if !r.row.HasColumn("maker fees since") { + return nil, false + } + return r.row.MustUint("maker fees since"), true +} + +func (r pnlRow) otherFees() (*num.Uint, bool) { + if !r.row.HasColumn("other fees") { + return nil, false + } + return r.row.MustUint("other fees"), true +} + +func (r pnlRow) otherFeesSince() (*num.Uint, bool) { + if !r.row.HasColumn("other fees since") { + return nil, false + } + return r.row.MustUint("other fees since"), true +} + +func (r pnlRow) fundingPayment() (*num.Int, bool) { + if !r.row.HasColumn("funding payments") { + return nil, false + } + return r.row.MustInt("funding payments"), true +} + +func (r pnlRow) fundingPaymentSince() (*num.Int, bool) { + if !r.row.HasColumn("funding payments since") { + return nil, false + } + return r.row.MustInt("funding payments since"), true +} + +func (r pnlRow) checkFees() bool { + if _, taker := r.takerFees(); taker { + return true + } + if _, maker := r.makerFees(); maker { + return true + } + if _, other := r.otherFees(); other { + return true + } + if _, ok := r.takerFeesSince(); ok { + return true + } + if _, ok := r.makerFeesSince(); ok { + return true + } + if _, ok := r.otherFeesSince(); ok { + return true + } + return false +} + +func (r pnlRow) checkFunding() bool { + if _, ok := r.fundingPayment(); ok { + return true + } + _, ok := r.fundingPaymentSince() + return ok +} + +func (r pnlRow) diffMap() map[string]string { + m := map[string]string{ + "volume": i64ToS(r.volume()), + "unrealised PNL": r.unrealisedPNL().String(), + "realised PNL": r.realisedPNL().String(), + "taker fees": "", + "taker fees since": "", + "maker fees": "", + "maker fees since": "", + "other fees": "", + "other fees since": "", + "funding payments": "", + "funding payments since": "", + } + if v, ok := r.takerFees(); ok { + m["taker fees"] = v.String() + } + if v, ok := r.makerFees(); ok { + m["maker fees"] = v.String() + } + if v, ok := r.otherFees(); ok { + m["other fees"] = v.String() + } + if v, ok := r.takerFeesSince(); ok { + m["taker fees since"] = v.String() + } + if v, ok := r.makerFeesSince(); ok { + m["maker fees since"] = v.String() + } + if v, ok := r.otherFeesSince(); ok { + m["other fees since"] = v.String() + } + if v, ok := r.fundingPayment(); ok { + m["funding payments"] = v.String() + } + if v, ok := r.fundingPaymentSince(); ok { + m["funding payments since"] = v.String() + } + return m +} diff --git a/core/plugins/fees.go b/core/plugins/fees.go new file mode 100644 index 00000000000..b6739dfc7dc --- /dev/null +++ b/core/plugins/fees.go @@ -0,0 +1,85 @@ +// Copyright (C) 2023 Gobalsky Labs Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package plugins + +import ( + "code.vegaprotocol.io/vega/core/types" + "code.vegaprotocol.io/vega/libs/num" + "code.vegaprotocol.io/vega/protos/vega" +) + +type feeAmounts struct { + side types.Side + maker *num.Uint + taker *num.Uint + other *num.Uint +} + +func newFeeAmounts(side types.Side) *feeAmounts { + return &feeAmounts{ + side: side, + maker: num.UintZero(), + taker: num.UintZero(), + other: num.UintZero(), + } +} + +func getFeeAmounts(trade *vega.Trade) (*feeAmounts, *feeAmounts) { + buyer, seller := newFeeAmounts(types.SideBuy), newFeeAmounts(types.SideSell) + buyer.setAmounts(trade) + seller.setAmounts(trade) + // auction end trades don't really have an aggressor, maker and taker fees are split. + if trade.Aggressor == types.SideSell { + buyer.maker.AddSum(seller.taker) + } else if trade.Aggressor == types.SideBuy { + seller.maker.AddSum(buyer.taker) + } else { + buyer.maker.AddSum(seller.taker) + seller.maker.AddSum(buyer.taker) + } + return buyer, seller +} + +func (f *feeAmounts) setAmounts(trade *vega.Trade) { + fee := trade.BuyerFee + if f.side == types.SideSell { + fee = trade.SellerFee + } + if fee == nil { + return + } + maker, infra, lFee, tFee, bbFee, hvFee := num.UintZero(), num.UintZero(), num.UintZero(), num.UintZero(), num.UintZero(), num.UintZero() + if len(fee.MakerFee) > 0 { + maker, _ = num.UintFromString(fee.MakerFee, 10) + } + if len(fee.InfrastructureFee) > 0 { + infra, _ = num.UintFromString(fee.InfrastructureFee, 10) + } + if len(fee.LiquidityFee) > 0 { + lFee, _ = num.UintFromString(fee.LiquidityFee, 10) + } + if len(fee.TreasuryFee) > 0 { + tFee, _ = num.UintFromString(fee.TreasuryFee, 10) + } + if len(fee.BuyBackFee) > 0 { + bbFee, _ = num.UintFromString(fee.BuyBackFee, 10) + } + if len(fee.HighVolumeMakerFee) > 0 { + hvFee, _ = num.UintFromString(fee.HighVolumeMakerFee, 10) + } + f.other.AddSum(infra, lFee, tFee, bbFee, hvFee) + f.taker.AddSum(maker) +} diff --git a/core/plugins/positions.go b/core/plugins/positions.go index 79de097e4a9..37ef3ed08ef 100644 --- a/core/plugins/positions.go +++ b/core/plugins/positions.go @@ -70,6 +70,7 @@ type LSE interface { MarketID() string Amount() *num.Int Timestamp() int64 + IsFunding() bool } // DOC DistressedOrdersClosedEvent. @@ -148,10 +149,48 @@ func (p *Positions) Push(evts ...events.Event) { p.mu.Unlock() } +func (p *Positions) handleRegularTrade(e TE) { + trade := e.Trade() + if trade.Type == types.TradeTypeNetworkCloseOutBad { + return + } + marketID := e.MarketID() + partyPos, ok := p.data[marketID] + if !ok { + // @TODO should this be done? + return + } + buyerFee, sellerFee := getFeeAmounts(&trade) + buyer, ok := partyPos[trade.Buyer] + if !ok { + buyer = Position{ + Position: types.NewPosition(marketID, trade.Buyer), + AverageEntryPriceFP: num.DecimalZero(), + RealisedPnlFP: num.DecimalZero(), + UnrealisedPnlFP: num.DecimalZero(), + } + } + buyer.setFees(buyerFee) + seller, ok := partyPos[trade.Seller] + if !ok { + seller = Position{ + Position: types.NewPosition(marketID, trade.Seller), + AverageEntryPriceFP: num.DecimalZero(), + RealisedPnlFP: num.DecimalZero(), + UnrealisedPnlFP: num.DecimalZero(), + } + } + seller.setFees(sellerFee) + partyPos[trade.Buyer] = buyer + partyPos[trade.Seller] = seller + p.data[marketID] = partyPos +} + // handle trade event closing distressed parties. func (p *Positions) handleTradeEvent(e TE) { trade := e.Trade() if trade.Type != types.TradeTypeNetworkCloseOutBad { + p.handleRegularTrade(e) return } marketID := e.MarketID() @@ -170,18 +209,31 @@ func (p *Positions) handleTradeEvent(e TE) { pos, ok := partyPos[types.NetworkParty] if !ok { pos = Position{ - Position: types.Position{ - MarketID: marketID, - PartyID: types.NetworkParty, - }, + Position: types.NewPosition(marketID, types.NetworkParty), AverageEntryPriceFP: num.DecimalZero(), RealisedPnlFP: num.DecimalZero(), UnrealisedPnlFP: num.DecimalZero(), } } + dParty := trade.Seller + networkFee, otherFee := getFeeAmounts(&trade) if trade.Seller == types.NetworkParty { size *= -1 + dParty = trade.Buyer + networkFee, otherFee = otherFee, networkFee } + other, ok := partyPos[dParty] + if !ok { + other = Position{ + Position: types.NewPosition(marketID, dParty), + AverageEntryPriceFP: num.DecimalZero(), + RealisedPnlFP: num.DecimalZero(), + UnrealisedPnlFP: num.DecimalZero(), + } + } + other.setFees(otherFee) + other.ResetSince() + pos.setFees(networkFee) opened, closed := calculateOpenClosedVolume(pos.OpenVolume, size) realisedPnlDelta := markPriceDec.Sub(pos.AverageEntryPriceFP).Mul(num.DecimalFromInt64(closed)).Div(posFactor) pos.RealisedPnl = pos.RealisedPnl.Add(realisedPnlDelta) @@ -196,6 +248,7 @@ func (p *Positions) handleTradeEvent(e TE) { pos.OpenVolume += opened mtm(&pos, mPrice, posFactor) partyPos[types.NetworkParty] = pos + partyPos[dParty] = other p.data[marketID] = partyPos } @@ -208,12 +261,16 @@ func (p *Positions) handleFundingPayments(e FP) { payments := e.FundingPayments().Payments for _, pay := range payments { pos, ok := partyPos[pay.PartyId] - amt, _ := num.DecimalFromString(pay.Amount) if !ok { continue } + amt, _ := num.DecimalFromString(pay.Amount) + iAmt, _ := num.IntFromDecimal(amt) pos.RealisedPnl = pos.RealisedPnl.Add(amt) pos.RealisedPnlFP = pos.RealisedPnlFP.Add(amt) + // add funding totals + pos.FundingPaymentAmount.Add(iAmt) + pos.FundingPaymentAmountSince.Add(iAmt) partyPos[pay.PartyId] = pos } p.data[marketID] = partyPos @@ -256,7 +313,8 @@ func (p *Positions) applyDistressedOrders(e DOC) { } func (p *Positions) applyLossSocialization(e LSE) { - marketID, partyID, amountLoss := e.MarketID(), e.PartyID(), num.DecimalFromInt(e.Amount()) + iAmt := e.Amount() + marketID, partyID, amountLoss := e.MarketID(), e.PartyID(), num.DecimalFromInt(iAmt) pos, ok := p.data[marketID][partyID] if !ok { return @@ -266,6 +324,11 @@ func (p *Positions) applyLossSocialization(e LSE) { } else { pos.adjustment = pos.adjustment.Add(amountLoss) } + if e.IsFunding() { + // adjust funding amounts if needed. + pos.FundingPaymentAmount.Add(iAmt) + pos.FundingPaymentAmountSince.Add(iAmt) + } pos.RealisedPnlFP = pos.RealisedPnlFP.Add(amountLoss) pos.RealisedPnl = pos.RealisedPnl.Add(amountLoss) @@ -488,10 +551,16 @@ func mtm(p *Position, markPrice *num.Uint, positionFactor num.Decimal) { func updateSettlePosition(p *Position, e SPE) { for _, t := range e.Trades() { + reset := p.OpenVolume == 0 pr := t.Price() openedVolume, closedVolume := calculateOpenClosedVolume(p.OpenVolume, t.Size()) _ = closeV(p, closedVolume, pr, e.PositionFactor()) + before := p.OpenVolume openV(p, openedVolume, pr) + // was the position zero, or did the position flip sides? + if reset || (before < 0 && p.OpenVolume > 0) || (before > 0 && p.OpenVolume < 0) { + p.ResetSince() + } p.AverageEntryPrice, _ = num.UintFromDecimal(p.AverageEntryPriceFP.Round(0)) p.RealisedPnl = p.RealisedPnlFP.Round(0) @@ -515,10 +584,7 @@ type Position struct { func seToProto(e SE) Position { return Position{ - Position: types.Position{ - MarketID: e.MarketID(), - PartyID: e.PartyID(), - }, + Position: types.NewPosition(e.MarketID(), e.PartyID()), AverageEntryPriceFP: num.DecimalZero(), RealisedPnlFP: num.DecimalZero(), UnrealisedPnlFP: num.DecimalZero(), @@ -544,3 +610,12 @@ func (p *Positions) Types() []events.Type { events.TradeEvent, } } + +func (p *Position) setFees(fee *feeAmounts) { + p.TakerFeesPaid.AddSum(fee.taker) + p.TakerFeesPaidSince.AddSum(fee.taker) + p.MakerFeesReceived.AddSum(fee.maker) + p.MakerFeesReceivedSince.AddSum(fee.maker) + p.FeesPaid.AddSum(fee.other) + p.FeesPaidSince.AddSum(fee.other) +} diff --git a/core/types/plugins.go b/core/types/plugins.go index 55aafd762cf..c89a60dd2ae 100644 --- a/core/types/plugins.go +++ b/core/types/plugins.go @@ -35,18 +35,73 @@ type Position struct { // formatted price of `1.23456` assuming market configured to 5 decimal places AverageEntryPrice *num.Uint // Timestamp for the latest time the position was updated - UpdatedAt int64 + UpdatedAt int64 + TakerFeesPaid *num.Uint + TakerFeesPaidSince *num.Uint + MakerFeesReceived *num.Uint + MakerFeesReceivedSince *num.Uint + FeesPaid *num.Uint + FeesPaidSince *num.Uint + FundingPaymentAmount *num.Int + FundingPaymentAmountSince *num.Int +} + +func NewPosition(marketID, partyID string) Position { + return Position{ + MarketID: marketID, + PartyID: partyID, + AverageEntryPrice: num.UintZero(), + TakerFeesPaid: num.UintZero(), + MakerFeesReceived: num.UintZero(), + FeesPaid: num.UintZero(), + TakerFeesPaidSince: num.UintZero(), + MakerFeesReceivedSince: num.UintZero(), + FeesPaidSince: num.UintZero(), + FundingPaymentAmount: num.IntZero(), + FundingPaymentAmountSince: num.IntZero(), + } } func (p *Position) IntoProto() *proto.Position { + if p.FeesPaid == nil { + p.FeesPaid = num.UintZero() + } + if p.FeesPaidSince == nil { + p.FeesPaidSince = num.UintZero() + } + if p.TakerFeesPaid == nil { + p.TakerFeesPaid = num.UintZero() + } + if p.TakerFeesPaidSince == nil { + p.TakerFeesPaidSince = num.UintZero() + } + if p.MakerFeesReceived == nil { + p.MakerFeesReceived = num.UintZero() + } + if p.MakerFeesReceivedSince == nil { + p.MakerFeesReceivedSince = num.UintZero() + } + if p.FundingPaymentAmount == nil { + p.FundingPaymentAmount = num.IntZero() + } + if p.FundingPaymentAmountSince == nil { + p.FundingPaymentAmountSince = num.IntZero() + } return &proto.Position{ - MarketId: p.MarketID, - PartyId: p.PartyID, - OpenVolume: p.OpenVolume, - RealisedPnl: p.RealisedPnl.BigInt().String(), - UnrealisedPnl: p.UnrealisedPnl.BigInt().String(), - AverageEntryPrice: num.UintToString(p.AverageEntryPrice), - UpdatedAt: p.UpdatedAt, + MarketId: p.MarketID, + PartyId: p.PartyID, + OpenVolume: p.OpenVolume, + RealisedPnl: p.RealisedPnl.BigInt().String(), + UnrealisedPnl: p.UnrealisedPnl.BigInt().String(), + AverageEntryPrice: num.UintToString(p.AverageEntryPrice), + UpdatedAt: p.UpdatedAt, + TakerFeesPaid: p.TakerFeesPaid.String(), + MakerFeesReceived: p.MakerFeesReceived.String(), + FeesPaid: p.FeesPaid.String(), + TakerFeesPaidSince: p.TakerFeesPaidSince.String(), + MakerFeesReceivedSince: p.MakerFeesReceivedSince.String(), + FundingPaymentAmount: p.FundingPaymentAmount.String(), + FundingPaymentAmountSince: p.FundingPaymentAmountSince.String(), } } @@ -59,3 +114,10 @@ func (p Positions) IntoProto() []*proto.Position { } return out } + +func (p *Position) ResetSince() { + p.TakerFeesPaidSince = num.UintZero() + p.MakerFeesReceivedSince = num.UintZero() + p.FeesPaidSince = num.UintZero() + p.FundingPaymentAmountSince = num.IntZero() +} diff --git a/datanode/entities/position.go b/datanode/entities/position.go index d8d5fcee141..2d1ccf2d1fe 100644 --- a/datanode/entities/position.go +++ b/datanode/entities/position.go @@ -39,6 +39,7 @@ type positionSettlement interface { type lossSocialization interface { Amount() *num.Int TxHash() string + IsFunding() bool } type settleDistressed interface { @@ -262,6 +263,11 @@ func (p *Position) UpdateWithLossSocialization(e lossSocialization) { p.Adjustment = p.Adjustment.Add(amountLoss) p.LossSocialisationAmount = p.LossSocialisationAmount.Add(amountLoss) } + if e.IsFunding() { + // adjust if this is a loss socialisation resulting from a funding payment settlement. + p.FundingPaymentAmount = p.FundingPaymentAmount.Add(amountLoss) + p.FundingPaymentAmountSince = p.FundingPaymentAmountSince.Add(amountLoss) + } p.RealisedPnl = p.RealisedPnl.Add(amountLoss) p.TxHash = TxHash(e.TxHash()) diff --git a/datanode/sqlsubscribers/position.go b/datanode/sqlsubscribers/position.go index 4d3527fd1d2..0747cc6c3d1 100644 --- a/datanode/sqlsubscribers/position.go +++ b/datanode/sqlsubscribers/position.go @@ -59,6 +59,7 @@ type positionSettlement interface { type lossSocialization interface { positionEventBase Amount() *num.Int + IsFunding() bool } type settleDistressed interface {