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/lib/games/stonks/constants.d.ts +1 -5
- package/lib/games/stonks/constants.js +2 -6
- package/lib/games/stonks/constants.js.map +1 -1
- package/lib/games/stonks/stonks-rules.d.ts +6 -7
- package/lib/games/stonks/stonks-rules.js +20 -24
- package/lib/games/stonks/stonks-rules.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/games/stonks/constants.ts +1 -9
- package/src/games/stonks/stonks-rules.spec.ts +58 -64
- package/src/games/stonks/stonks-rules.ts +28 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shufflecom-calculations",
|
|
3
|
-
"version": "6.
|
|
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": "
|
|
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
|
|
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.
|
|
47
|
+
const impliedPrice = StonksRules.impliedPriceForMultiplier(bet, number('1.47'));
|
|
48
48
|
|
|
49
|
-
expect(StonksRules.multiplierAtPrice(bet, impliedPrice)).toBigNumberEqual(1.
|
|
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.
|
|
55
|
+
expect(StonksRules.impliedPriceForMultiplier(bet, number('1.47'))).toBigNumberEqual(95);
|
|
56
56
|
});
|
|
57
|
-
});
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
expect(StonksRules.
|
|
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('
|
|
67
|
-
it('
|
|
68
|
-
|
|
69
|
-
|
|
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('
|
|
139
|
-
it('
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
expect(StonksRules.
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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('
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
294
|
-
|
|
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.
|
|
322
|
-
expect(StonksRules.multiplierForReason(bet, { kind: StonksBetTerminalReason.LIQUIDATION }, price)).toBigNumberEqual(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('
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
expect(StonksRules.
|
|
331
|
-
expect(StonksRules.
|
|
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('
|
|
336
|
-
|
|
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('
|
|
343
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
128
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
177
|
-
return BigNumber.maximum(0,
|
|
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
|
|
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.
|
|
189
|
+
return StonksRules.netPayoutAtPrice(bet, price);
|
|
197
190
|
default:
|
|
198
191
|
reason satisfies never;
|
|
199
|
-
throw new Error(`StonksRules.
|
|
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
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return
|
|
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
|
}
|