shufflecom-calculations 6.12.0 → 6.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shufflecom-calculations",
3
- "version": "6.12.0",
3
+ "version": "6.13.0",
4
4
  "description": "",
5
5
  "types": "lib/index.d.ts",
6
6
  "main": "lib/index.js",
@@ -19,5 +19,5 @@
19
19
  },
20
20
  "author": "",
21
21
  "license": "ISC",
22
- "gitHead": "6656b3e0dc340fe986901432c7fdf076d838c304"
22
+ "gitHead": "02c7a0d05558345dbbd1724b7a87fc4a9379a2a8"
23
23
  }
@@ -2,9 +2,6 @@ import BigNumber from 'bignumber.js';
2
2
 
3
3
  export const STONKS_MIN_TAKE_PROFIT_MULTIPLIER = BigNumber(1.05);
4
4
 
5
- export const STONKS_TP_MIN_PROXIMITY_TO_ENTRY = BigNumber(0.001);
6
- export const STONKS_TP_MIN_PROXIMITY_TO_BUST = BigNumber(0.001);
7
-
8
5
  // Extra decimal precision carried internally, used only to detect bust price colliding with
9
6
  // entry price at high leverage / low price.
10
7
  export const STONKS_INTERNAL_PRICE_DECIMAL_PLACES = 10;
@@ -13,8 +10,6 @@ export const STONKS_INTERNAL_PRICE_DECIMAL_PLACES = 10;
13
10
  export const STONKS_MIN_LEVERAGE = BigNumber(1);
14
11
  export const STONKS_MAX_LEVERAGE = BigNumber(1000);
15
12
 
16
- export const STONKS_MAX_ACTIVE_BETS_PER_ACCOUNT = 20;
17
-
18
13
  export const STONKS_START_PRICE = BigNumber(1000);
19
14
  export const STONKS_VOLATILITY = 0.001;
20
15
 
@@ -38,7 +33,4 @@ export const STONKS_CHECKPOINT_INTERVAL_TICKS = 500;
38
33
 
39
34
  export const STONKS_PRICE_BAR_INTERVAL_TICKS = 5_000 / STONKS_TICK_DURATION_MS;
40
35
 
41
- export const STONKS_PROFIT_FEE_RATE = BigNumber(0.05);
42
-
43
- // Keep well under 2^53: contracts cross into Redis Lua's tonumber(), which loses precision above that (unlike BigNumber here)
44
- export const STONKS_CONTRACT_SCALE = BigNumber(10).pow(6);
36
+ export const STONKS_PAYOUT_FEE_RATE = BigNumber(0.02);
@@ -44,35 +44,29 @@ describe('StonksRules', () => {
44
44
  it('inverts multiplierAtPrice without depending on amount', () => {
45
45
  const bet = openBet({ amount: number(0), leverage: number(10) });
46
46
 
47
- const impliedPrice = StonksRules.impliedPriceForMultiplier(bet, number(1.5));
47
+ const impliedPrice = StonksRules.impliedPriceForMultiplier(bet, number('1.47'));
48
48
 
49
- expect(StonksRules.multiplierAtPrice(bet, impliedPrice)).toBigNumberEqual(1.5);
49
+ expect(StonksRules.multiplierAtPrice(bet, impliedPrice)).toBigNumberEqual('1.47');
50
50
  });
51
51
 
52
52
  it('moves in the opposite price direction for DOWN bets', () => {
53
53
  const bet = openBet({ direction: StonksDirection.DOWN, leverage: number(10) });
54
54
 
55
- expect(StonksRules.impliedPriceForMultiplier(bet, number(1.5))).toBigNumberEqual(95);
55
+ expect(StonksRules.impliedPriceForMultiplier(bet, number('1.47'))).toBigNumberEqual(95);
56
56
  });
57
- });
58
57
 
59
- describe('isValidTakeProfitMultiplier', () => {
60
- it('requires the multiplier to exceed the configured floor', () => {
61
- expect(StonksRules.isValidTakeProfitMultiplier(STONKS_MIN_TAKE_PROFIT_MULTIPLIER)).toBe(false);
62
- expect(StonksRules.isValidTakeProfitMultiplier(STONKS_MIN_TAKE_PROFIT_MULTIPLIER.plus('0.01'))).toBe(true);
58
+ it('treats the multiplier as net of the fee, so 1x leverage needs more than a 100% move to reach 2x', () => {
59
+ const bet = openBet({ leverage: number(1) });
60
+
61
+ expect(StonksRules.impliedPriceForMultiplier(bet, number('1.96'))).toBigNumberEqual(200);
62
+ expect(StonksRules.impliedPriceForMultiplier(bet, number(2))).toBigNumberGreaterThan(200);
63
63
  });
64
64
  });
65
65
 
66
- describe('isTakeProfitFarEnoughFromEntry / isTakeProfitFarEnoughFromBust', () => {
67
- it('rejects an implied price too close to entry or too close to bust', () => {
68
- const entryPrice = number(100);
69
- const bustPrice = number(90);
70
-
71
- expect(StonksRules.isTakeProfitFarEnoughFromEntry(entryPrice, number('100.0001'))).toBe(false);
72
- expect(StonksRules.isTakeProfitFarEnoughFromEntry(entryPrice, number(110))).toBe(true);
73
-
74
- expect(StonksRules.isTakeProfitFarEnoughFromBust(entryPrice, bustPrice, number('90.0001'))).toBe(false);
75
- expect(StonksRules.isTakeProfitFarEnoughFromBust(entryPrice, bustPrice, number(110))).toBe(true);
66
+ describe('isValidTakeProfitMultiplier', () => {
67
+ it('requires the multiplier to be at or above the configured floor', () => {
68
+ expect(StonksRules.isValidTakeProfitMultiplier(STONKS_MIN_TAKE_PROFIT_MULTIPLIER.minus('0.01'))).toBe(false);
69
+ expect(StonksRules.isValidTakeProfitMultiplier(STONKS_MIN_TAKE_PROFIT_MULTIPLIER)).toBe(true);
76
70
  });
77
71
  });
78
72
 
@@ -135,12 +129,22 @@ describe('StonksRules', () => {
135
129
  });
136
130
  });
137
131
 
138
- describe('hasReachedTargetPrice', () => {
139
- it('is inclusive of an exact match, in the direction of profit', () => {
140
- expect(StonksRules.hasReachedTargetPrice(StonksDirection.UP, number(110), number(110))).toBe(true);
141
- expect(StonksRules.hasReachedTargetPrice(StonksDirection.UP, number(109), number(110))).toBe(false);
142
- expect(StonksRules.hasReachedTargetPrice(StonksDirection.DOWN, number(90), number(90))).toBe(true);
143
- expect(StonksRules.hasReachedTargetPrice(StonksDirection.DOWN, number(91), number(90))).toBe(false);
132
+ describe('hasReachedTakeProfit', () => {
133
+ it('compares the net multiplier at the price against the take-profit, inclusive, in the direction of profit', () => {
134
+ const up = openBet({ direction: StonksDirection.UP, leverage: number(10) });
135
+ const down = openBet({ direction: StonksDirection.DOWN, leverage: number(10) });
136
+
137
+ expect(StonksRules.hasReachedTakeProfit(up, number(105), number('1.47'))).toBe(true);
138
+ expect(StonksRules.hasReachedTakeProfit(up, number('104.99'), number('1.47'))).toBe(false);
139
+ expect(StonksRules.hasReachedTakeProfit(down, number(95), number('1.47'))).toBe(true);
140
+ expect(StonksRules.hasReachedTakeProfit(down, number('95.01'), number('1.47'))).toBe(false);
141
+ });
142
+
143
+ it('needs the price to clear the fee: a 10% move at 10x reaches 1.96x, not 2x', () => {
144
+ const bet = openBet({ leverage: number(10) });
145
+
146
+ expect(StonksRules.hasReachedTakeProfit(bet, number(110), number('1.96'))).toBe(true);
147
+ expect(StonksRules.hasReachedTakeProfit(bet, number(110), number(2))).toBe(false);
144
148
  });
145
149
  });
146
150
 
@@ -154,10 +158,11 @@ describe('StonksRules', () => {
154
158
  });
155
159
 
156
160
  describe('multiplierAtPrice', () => {
157
- it('is 1 at entry and floors at zero once loss would exceed the wager', () => {
161
+ it('is net of the fee at entry and floors at zero once loss would exceed the wager', () => {
158
162
  const bet = openBet({ entryPrice: number(100), leverage: number(10), direction: StonksDirection.UP });
159
163
 
160
- expect(StonksRules.multiplierAtPrice(bet, number(100))).toBigNumberEqual(1);
164
+ expect(StonksRules.multiplierAtPrice(bet, number(100))).toBigNumberEqual('0.98');
165
+ expect(StonksRules.multiplierAtPrice(bet, number(110))).toBigNumberEqual('1.96');
161
166
  expect(StonksRules.multiplierAtPrice(bet, number(80))).toBigNumberEqual(0);
162
167
  });
163
168
  });
@@ -198,11 +203,11 @@ describe('StonksRules', () => {
198
203
  });
199
204
  });
200
205
 
201
- it('fires MAX_PAYOUT once the gross payout reaches the live cap, even overshooting it', () => {
206
+ it('fires MAX_PAYOUT once the net payout reaches the live cap, even overshooting it', () => {
202
207
  const bet = openBet({ leverage: number(100) });
203
208
  const cap = number(150);
204
209
 
205
- // At price 101 the gross payout (200) overshoots the cap (150) — MAX_PAYOUT still fires,
210
+ // At price 101 the net payout (196) overshoots the cap (150) — MAX_PAYOUT still fires,
206
211
  // and the caller settles at this real tick price regardless, not a price back-solved from the cap.
207
212
  expect(StonksRules.evaluateTriggers(bet, number(101), cap)).toEqual({
208
213
  fired: true,
@@ -225,7 +230,7 @@ describe('StonksRules', () => {
225
230
  const reason = { kind: StonksBetTerminalReason.MAX_PAYOUT, appliedCap: cap } as const;
226
231
 
227
232
  expect(StonksRules.evaluateTriggers(bet, impliedPrice, cap)).toEqual({ fired: true, reason });
228
- expect(StonksRules.grossPayoutForReason(bet, reason, impliedPrice)).toBigNumberEqual(250);
233
+ expect(StonksRules.netPayoutForReason(bet, reason, impliedPrice)).toBigNumberEqual(250);
229
234
  });
230
235
 
231
236
  // The mirror of the case above, and the reason the cap cannot simply be checked before the
@@ -235,12 +240,12 @@ describe('StonksRules', () => {
235
240
  const takeProfitMultiplier = number(2);
236
241
  const bet = openBet({ takeProfitMultiplier });
237
242
  const cap = number(250);
238
- // One tick jumps to 130, well past the TP-implied 110: the price-derived payout there is 400
239
- // and clears the cap, but the agreed take-profit payout is only 100 × 2 = 200, under it.
243
+ // One tick jumps to 130, well past the TP-implied ~110.4: the price-derived net payout there
244
+ // is 392 and clears the cap, but the agreed take-profit payout is only 100 × 2 = 200, under it.
240
245
  const reason = { kind: StonksBetTerminalReason.TAKE_PROFIT, takeProfitMultiplier } as const;
241
246
 
242
247
  expect(StonksRules.evaluateTriggers(bet, number(130), cap)).toEqual({ fired: true, reason });
243
- expect(StonksRules.grossPayoutForReason(bet, reason, number(130))).toBigNumberEqual(200);
248
+ expect(StonksRules.netPayoutForReason(bet, reason, number(130))).toBigNumberEqual(200);
244
249
  });
245
250
 
246
251
  it('fires TAKE_PROFIT, not MAX_PAYOUT, when the agreed take-profit payout exactly equals the live cap', () => {
@@ -267,38 +272,32 @@ describe('StonksRules', () => {
267
272
  });
268
273
  });
269
274
 
270
- describe('grossPayoutForReason', () => {
275
+ describe('netPayoutForReason', () => {
271
276
  const bet = openBet({ leverage: number(10) });
272
277
 
273
278
  it('TAKE_PROFIT credits wager × multiplier exactly, ignoring price', () => {
274
279
  const takeProfitMultiplier = number('1.5');
275
280
 
276
281
  expect(
277
- StonksRules.grossPayoutForReason(bet, { kind: StonksBetTerminalReason.TAKE_PROFIT, takeProfitMultiplier }, number(999)),
282
+ StonksRules.netPayoutForReason(bet, { kind: StonksBetTerminalReason.TAKE_PROFIT, takeProfitMultiplier }, number(999)),
278
283
  ).toBigNumberEqual(150);
279
284
  });
280
285
 
281
286
  it('LIQUIDATION is exact zero regardless of price', () => {
282
- expect(StonksRules.grossPayoutForReason(bet, { kind: StonksBetTerminalReason.LIQUIDATION }, number(999))).toBigNumberEqual(0);
287
+ expect(StonksRules.netPayoutForReason(bet, { kind: StonksBetTerminalReason.LIQUIDATION }, number(999))).toBigNumberEqual(0);
283
288
  });
284
289
 
285
290
  it('MAX_PAYOUT is exactly the applied cap, ignoring price', () => {
286
291
  const appliedCap = number(150);
287
292
 
288
293
  expect(
289
- StonksRules.grossPayoutForReason(bet, { kind: StonksBetTerminalReason.MAX_PAYOUT, appliedCap }, number(999)),
294
+ StonksRules.netPayoutForReason(bet, { kind: StonksBetTerminalReason.MAX_PAYOUT, appliedCap }, number(999)),
290
295
  ).toBigNumberEqual(150);
291
296
  });
292
297
 
293
- it('CASHOUT and ROUND_END fall back to the price-derived gross payout', () => {
294
- const priceDerivedPayout = StonksRules.grossPayoutAtPrice(bet, number(110));
295
-
296
- expect(StonksRules.grossPayoutForReason(bet, { kind: StonksBetTerminalReason.CASHOUT }, number(110))).toBigNumberEqual(
297
- priceDerivedPayout,
298
- );
299
- expect(StonksRules.grossPayoutForReason(bet, { kind: StonksBetTerminalReason.ROUND_END }, number(110))).toBigNumberEqual(
300
- priceDerivedPayout,
301
- );
298
+ it('CASHOUT and ROUND_END fall back to the price-derived payout, net of the fee', () => {
299
+ expect(StonksRules.netPayoutForReason(bet, { kind: StonksBetTerminalReason.CASHOUT }, number(110))).toBigNumberEqual(196);
300
+ expect(StonksRules.netPayoutForReason(bet, { kind: StonksBetTerminalReason.ROUND_END }, number(110))).toBigNumberEqual(196);
302
301
  });
303
302
  });
304
303
 
@@ -315,34 +314,29 @@ describe('StonksRules', () => {
315
314
  it.each([
316
315
  [StonksDirection.UP, number(101)],
317
316
  [StonksDirection.DOWN, number(99)],
318
- ])('every other reason falls back to the price-derived multiplier, independently of wager (%s)', (direction, price) => {
317
+ ])('every other reason falls back to the price-derived net multiplier, independently of wager (%s)', (direction, price) => {
319
318
  const bet = openBet({ direction, amount: number(0) });
320
319
 
321
- expect(StonksRules.multiplierForReason(bet, { kind: StonksBetTerminalReason.CASHOUT }, price)).toBigNumberEqual(1.1);
322
- expect(StonksRules.multiplierForReason(bet, { kind: StonksBetTerminalReason.LIQUIDATION }, price)).toBigNumberEqual(1.1);
320
+ expect(StonksRules.multiplierForReason(bet, { kind: StonksBetTerminalReason.CASHOUT }, price)).toBigNumberEqual('1.078');
321
+ expect(StonksRules.multiplierForReason(bet, { kind: StonksBetTerminalReason.LIQUIDATION }, price)).toBigNumberEqual('1.078');
323
322
  });
324
323
  });
325
324
 
326
- describe('netPayout', () => {
327
- const wager = number(100);
328
-
329
- it('passes losses and breakeven payouts through untaxed', () => {
330
- expect(StonksRules.netPayout(number(90), wager)).toBigNumberEqual(90);
331
- expect(StonksRules.netPayout(number(0), wager)).toBigNumberEqual(0);
332
- expect(StonksRules.netPayout(wager, wager)).toBigNumberEqual(wager);
325
+ describe('netOfFee / grossOfFee', () => {
326
+ it('takes a flat 2% fee on every payout, including losses and breakeven', () => {
327
+ expect(StonksRules.netOfFee(number(90))).toBigNumberEqual('88.2');
328
+ expect(StonksRules.netOfFee(number(100))).toBigNumberEqual(98);
329
+ expect(StonksRules.netOfFee(number(101))).toBigNumberEqual('98.98');
330
+ expect(StonksRules.netOfFee(number(200))).toBigNumberEqual(196);
333
331
  });
334
332
 
335
- it('taxes only the profit portion at 5%, never the wager', () => {
336
- // gross 101 = wager + 1 profit; only the 1 is taxed, so net = 100 + 0.95 = 100.95
337
- expect(StonksRules.netPayout(number(101), wager)).toBigNumberEqual('100.95');
338
- // gross 200 = wager + 100 profit; net = 100 + 95 = 195
339
- expect(StonksRules.netPayout(number(200), wager)).toBigNumberEqual(195);
333
+ it('leaves a liquidated payout at zero', () => {
334
+ expect(StonksRules.netOfFee(number(0))).toBigNumberEqual(0);
340
335
  });
341
336
 
342
- it('never drops a nominal win below the wager, however small the gross profit is', () => {
343
- const net = StonksRules.netPayout(number('100.01'), wager);
344
-
345
- expect(net).toBigNumberGreaterThan(wager);
337
+ it('grossOfFee inverts netOfFee', () => {
338
+ expect(StonksRules.grossOfFee(number(98))).toBigNumberEqual(100);
339
+ expect(StonksRules.grossOfFee(StonksRules.netOfFee(number('123.456')))).toBigNumberEqual('123.456');
346
340
  });
347
341
  });
348
342
  });
@@ -4,12 +4,10 @@ import {
4
4
  STONKS_INTERNAL_PRICE_DECIMAL_PLACES,
5
5
  STONKS_MAX_STALE_CLIENT_TICKS,
6
6
  STONKS_MIN_TAKE_PROFIT_MULTIPLIER,
7
- STONKS_PROFIT_FEE_RATE,
7
+ STONKS_PAYOUT_FEE_RATE,
8
8
  STONKS_ROUND_DURATION_MS,
9
9
  STONKS_ROUND_TICK_COUNT,
10
10
  STONKS_TICK_DURATION_MS,
11
- STONKS_TP_MIN_PROXIMITY_TO_BUST,
12
- STONKS_TP_MIN_PROXIMITY_TO_ENTRY,
13
11
  } from './constants';
14
12
 
15
13
  export enum StonksDirection {
@@ -40,7 +38,6 @@ export interface StonksTerminalBetState extends StonksOpenBetState {
40
38
  readonly terminalTick: number;
41
39
  readonly terminalPrice: BigNumber;
42
40
  readonly multiplier: BigNumber;
43
- readonly grossPayout: BigNumber;
44
41
  readonly netPayout: BigNumber;
45
42
  }
46
43
 
@@ -69,21 +66,13 @@ export class StonksRules {
69
66
  targetMultiplier: BigNumber,
70
67
  ): BigNumber {
71
68
  const directionSign = bet.direction === StonksDirection.UP ? 1 : -1;
72
- const multiplierDelta = targetMultiplier.minus(1);
69
+ const multiplierDelta = StonksRules.grossOfFee(targetMultiplier).minus(1);
73
70
  const priceChange = BigNumber(directionSign).multipliedBy(multiplierDelta).dividedBy(bet.leverage);
74
71
  return bet.entryPrice.multipliedBy(BigNumber(1).plus(priceChange));
75
72
  }
76
73
 
77
74
  static isValidTakeProfitMultiplier(takeProfitMultiplier: BigNumber): boolean {
78
- return takeProfitMultiplier.isGreaterThan(STONKS_MIN_TAKE_PROFIT_MULTIPLIER);
79
- }
80
-
81
- static isTakeProfitFarEnoughFromEntry(entryPrice: BigNumber, takeProfitImpliedPrice: BigNumber): boolean {
82
- return takeProfitImpliedPrice.minus(entryPrice).abs().dividedBy(entryPrice).isGreaterThanOrEqualTo(STONKS_TP_MIN_PROXIMITY_TO_ENTRY);
83
- }
84
-
85
- static isTakeProfitFarEnoughFromBust(entryPrice: BigNumber, bustPrice: BigNumber, takeProfitImpliedPrice: BigNumber): boolean {
86
- return takeProfitImpliedPrice.minus(bustPrice).abs().dividedBy(entryPrice).isGreaterThanOrEqualTo(STONKS_TP_MIN_PROXIMITY_TO_BUST);
75
+ return takeProfitMultiplier.isGreaterThanOrEqualTo(STONKS_MIN_TAKE_PROFIT_MULTIPLIER);
87
76
  }
88
77
 
89
78
  static isBustPriceDistinctFromEntry(entryPrice: BigNumber, bustPrice: BigNumber): boolean {
@@ -124,8 +113,12 @@ export class StonksRules {
124
113
  return new Date(startedAtMs + tick * STONKS_TICK_DURATION_MS);
125
114
  }
126
115
 
127
- static hasReachedTargetPrice(direction: StonksDirection, currentPrice: BigNumber, targetPrice: BigNumber): boolean {
128
- return direction === StonksDirection.UP ? currentPrice.isGreaterThanOrEqualTo(targetPrice) : currentPrice.isLessThanOrEqualTo(targetPrice);
116
+ static hasReachedTakeProfit(
117
+ bet: Pick<StonksOpenBetState, 'leverage' | 'entryPrice' | 'direction'>,
118
+ price: BigNumber,
119
+ takeProfitMultiplier: BigNumber,
120
+ ): boolean {
121
+ return StonksRules.multiplierAtPrice(bet, price).isGreaterThanOrEqualTo(takeProfitMultiplier);
129
122
  }
130
123
 
131
124
  static hasReachedBust(direction: StonksDirection, price: BigNumber, bustPrice: BigNumber): boolean {
@@ -137,11 +130,7 @@ export class StonksRules {
137
130
 
138
131
  if (bet.takeProfitMultiplier != null) {
139
132
  const takeProfitMultiplier = bet.takeProfitMultiplier;
140
- const hitTakeProfit = StonksRules.hasReachedTargetPrice(
141
- bet.direction,
142
- tickPrice,
143
- StonksRules.impliedPriceForMultiplier(bet, takeProfitMultiplier),
144
- );
133
+ const hitTakeProfit = StonksRules.hasReachedTakeProfit(bet, tickPrice, takeProfitMultiplier);
145
134
  if (hitTakeProfit && hitBust) {
146
135
  throw new Error('StonksRules.evaluateTriggers: take profit and liquidation both fired');
147
136
  }
@@ -151,7 +140,7 @@ export class StonksRules {
151
140
  // A take-profit is the one reason whose payout is a fixed wager × multiplier rather than
152
141
  // something the tick price bounds, and the cap is read live every batch — so it can be
153
142
  // lowered under an already-agreed multiplier. Nothing downstream re-checks it.
154
- if (StonksRules.grossPayoutForReason(bet, reason, tickPrice).isGreaterThan(liveMaxPayoutCap)) {
143
+ if (StonksRules.netPayoutForReason(bet, reason, tickPrice).isGreaterThan(liveMaxPayoutCap)) {
155
144
  return { fired: true, reason: { kind: StonksBetTerminalReason.MAX_PAYOUT, appliedCap: liveMaxPayoutCap } };
156
145
  }
157
146
  return { fired: true, reason };
@@ -162,19 +151,19 @@ export class StonksRules {
162
151
  return { fired: true, reason: { kind: StonksBetTerminalReason.LIQUIDATION } };
163
152
  }
164
153
 
165
- if (StonksRules.grossPayoutAtPrice(bet, tickPrice).isGreaterThanOrEqualTo(liveMaxPayoutCap)) {
154
+ if (StonksRules.netPayoutAtPrice(bet, tickPrice).isGreaterThanOrEqualTo(liveMaxPayoutCap)) {
166
155
  return { fired: true, reason: { kind: StonksBetTerminalReason.MAX_PAYOUT, appliedCap: liveMaxPayoutCap } };
167
156
  }
168
157
  return { fired: false };
169
158
  }
170
159
 
171
- static multiplierAtPrice(bet: StonksOpenBetState, price: BigNumber): BigNumber {
160
+ static multiplierAtPrice(bet: Pick<StonksOpenBetState, 'leverage' | 'entryPrice' | 'direction'>, price: BigNumber): BigNumber {
172
161
  const directionSign = bet.direction === StonksDirection.UP ? 1 : -1;
173
162
  const percentagePriceChange = price.minus(bet.entryPrice).dividedBy(bet.entryPrice);
174
163
  const leveragedPriceChange = BigNumber(directionSign).multipliedBy(bet.leverage).multipliedBy(percentagePriceChange);
175
164
 
176
- const multiplier = BigNumber(1).plus(leveragedPriceChange);
177
- return BigNumber.maximum(0, multiplier);
165
+ const grossMultiplier = BigNumber(1).plus(leveragedPriceChange);
166
+ return StonksRules.netOfFee(BigNumber.maximum(0, grossMultiplier));
178
167
  }
179
168
 
180
169
  static grossPayoutAtPrice(bet: StonksOpenBetState, price: BigNumber): BigNumber {
@@ -183,7 +172,11 @@ export class StonksRules {
183
172
  return BigNumber.maximum(0, bet.amount.plus(pnl));
184
173
  }
185
174
 
186
- static grossPayoutForReason(bet: StonksOpenBetState, reason: StonksSettlementReason, price: BigNumber): BigNumber {
175
+ static netPayoutAtPrice(bet: StonksOpenBetState, price: BigNumber): BigNumber {
176
+ return StonksRules.netOfFee(StonksRules.grossPayoutAtPrice(bet, price));
177
+ }
178
+
179
+ static netPayoutForReason(bet: StonksOpenBetState, reason: StonksSettlementReason, price: BigNumber): BigNumber {
187
180
  switch (reason.kind) {
188
181
  case StonksBetTerminalReason.TAKE_PROFIT:
189
182
  return calculatePayoutBN(bet.amount, reason.takeProfitMultiplier);
@@ -193,10 +186,10 @@ export class StonksRules {
193
186
  return reason.appliedCap;
194
187
  case StonksBetTerminalReason.CASHOUT:
195
188
  case StonksBetTerminalReason.ROUND_END:
196
- return StonksRules.grossPayoutAtPrice(bet, price);
189
+ return StonksRules.netPayoutAtPrice(bet, price);
197
190
  default:
198
191
  reason satisfies never;
199
- throw new Error(`StonksRules.grossPayoutForReason: unhandled reason`);
192
+ throw new Error(`StonksRules.netPayoutForReason: unhandled reason`);
200
193
  }
201
194
  }
202
195
 
@@ -207,11 +200,11 @@ export class StonksRules {
207
200
  return StonksRules.multiplierAtPrice(bet, price);
208
201
  }
209
202
 
210
- static netPayout(grossPayout: BigNumber, amount: BigNumber): BigNumber {
211
- if (grossPayout.lte(amount)) {
212
- return grossPayout;
213
- }
214
- const profit = grossPayout.minus(amount);
215
- return amount.plus(profit.multipliedBy(BigNumber(1).minus(STONKS_PROFIT_FEE_RATE)));
203
+ static netOfFee(gross: BigNumber): BigNumber {
204
+ return gross.multipliedBy(BigNumber(1).minus(STONKS_PAYOUT_FEE_RATE));
205
+ }
206
+
207
+ static grossOfFee(net: BigNumber): BigNumber {
208
+ return net.dividedBy(BigNumber(1).minus(STONKS_PAYOUT_FEE_RATE));
216
209
  }
217
210
  }