Skip to content

Commit

Permalink
feat: directional premiums for referrals (#837)
Browse files Browse the repository at this point in the history
* feat: directional premiums for referrals

* fixup! feat: directional premiums for referrals

* fixup! feat: directional premiums for referrals

* fixup! feat: directional premiums for referrals
  • Loading branch information
maybeast authored Mar 6, 2025
1 parent 098f6c8 commit 036923e
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 41 deletions.
56 changes: 46 additions & 10 deletions lib/db/models/Referral.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { DataTypes, Model, Sequelize } from 'sequelize';
import { SwapType } from '../../consts/Enums';
import { OrderSide, SwapType } from '../../consts/Enums';

type Limits = {
minimal?: number;
maximal?: number;
};

// TODO: direction of chain swaps
type Premiums = Partial<Record<SwapType, number>>;
type DirectionalPremium = Partial<{
[OrderSide.BUY]: number;
[OrderSide.SELL]: number;
}>;

type Premiums = Partial<{
[SwapType.Submarine]: number;
[SwapType.ReverseSubmarine]: number;
[SwapType.Chain]: DirectionalPremium;
}>;

type LimitsPerType = Partial<Record<SwapType, Limits>>;

Expand Down Expand Up @@ -131,27 +139,55 @@ class Referral extends Model implements ReferralType {
return this.config?.limits?.[type];
};

public premium = (pair: string, type: SwapType): number | undefined => {
return (
public premium = (
pair: string,
type: SwapType,
orderSide: OrderSide,
): number | undefined => {
const premium =
this.config?.pairs?.[pair]?.premiums?.[type] ||
this.config?.premiums?.[type]
);
this.config?.premiums?.[type];

if (type === SwapType.Chain) {
return premium !== undefined ? premium[orderSide] : undefined;
}

return premium as number | undefined;
};

public premiumForPairs = (
pairs: string[],
type: SwapType,
orderSide?: OrderSide,
): number | undefined => {
if (type === SwapType.Chain && orderSide === undefined) {
throw new Error('chain swap premiums require an order side');
}

for (const pair of pairs) {
const premium = this.config?.pairs?.[pair]?.premiums?.[type];
if (premium !== undefined) {
return premium;
if (type === SwapType.Chain) {
return premium[orderSide!];
}
return premium as number | undefined;
}
}

return this.config?.premiums?.[type];
const premium = this.config?.premiums?.[type];
if (type === SwapType.Chain && premium !== undefined) {
return premium[orderSide!];
}

return premium as number | undefined;
};
}

export default Referral;
export { ReferralType, ReferralConfig, ReferralPairConfig };
export {
ReferralType,
ReferralConfig,
ReferralPairConfig,
DirectionalPremium,
Premiums,
};
76 changes: 65 additions & 11 deletions lib/db/repositories/ReferralRepository.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { QueryTypes } from 'sequelize';
import { SuccessSwapUpdateEvents } from '../../consts/Enums';
import {
OrderSide,
SuccessSwapUpdateEvents,
SwapType,
} from '../../consts/Enums';
import Database from '../Database';
import Referral, {
DirectionalPremium,
ReferralConfig,
ReferralPairConfig,
ReferralType,
Expand Down Expand Up @@ -38,6 +43,13 @@ class ReferralRepository {
ORDER BY year, month;
`;

private static readonly maxPremiumPercentage = 100;
private static readonly minPremiumPercentage = -100;
private static readonly minExpiration = 120;
private static readonly maxExpiration = 60 * 60 * 24;
private static readonly minRoutingFee = 0;
private static readonly maxRoutingFee = 0.005;

public static addReferral = async (
referral: ReferralType,
): Promise<Referral> => {
Expand Down Expand Up @@ -112,23 +124,67 @@ class ReferralRepository {
private static sanityCheckConfig = (
config: ReferralConfig | null | undefined,
) => {
const sanityCheckPairConfig = (cfg: ReferralPairConfig) => {
if (cfg.maxRoutingFee) {
if (cfg.maxRoutingFee < 0 || cfg.maxRoutingFee > 0.005) {
throw 'maxRoutingFee out of range';
const validateDirectionalPremium = (
premium: unknown,
): DirectionalPremium => {
if (typeof premium === 'object') {
const premiumObj = premium as Record<string, unknown>;
if (
(OrderSide.BUY in premiumObj &&
typeof premiumObj[OrderSide.BUY] !== 'number') ||
(OrderSide.SELL in premiumObj &&
typeof premiumObj[OrderSide.SELL] !== 'number')
) {
throw 'premium values must be numbers';
}
}

return premium as DirectionalPremium;
};

const sanityCheckPairConfig = (cfg: ReferralPairConfig) => {
if (
cfg.maxRoutingFee !== undefined &&
(cfg.maxRoutingFee < ReferralRepository.minRoutingFee ||
cfg.maxRoutingFee > ReferralRepository.maxRoutingFee)
) {
throw 'maxRoutingFee out of range';
}

if (cfg.premiums) {
if (Object.values(cfg.premiums).some((p) => p < -100 || p > 100)) {
throw 'premium out of range';
for (const [typeStr, premium] of Object.entries(cfg.premiums)) {
const type = Number(typeStr) as SwapType;

if (type === SwapType.Chain) {
const directionalPremium = validateDirectionalPremium(premium);
Object.values(directionalPremium).forEach((p) => {
if (
p < ReferralRepository.minPremiumPercentage ||
p > ReferralRepository.maxPremiumPercentage
) {
throw 'premium out of range';
}
});
} else {
if (typeof premium !== 'number') {
throw 'premium must be a number';
}
if (
premium < ReferralRepository.minPremiumPercentage ||
premium > ReferralRepository.maxPremiumPercentage
) {
throw 'premium out of range';
}
}
}
}

if (cfg.expirations) {
if (
Object.values(cfg.expirations).some(
(e) => e < 120 || e > 60 * 60 * 24,
(e) =>
e < ReferralRepository.minExpiration ||
e > ReferralRepository.maxExpiration,
)
) {
throw 'expiration out of range';
Expand All @@ -140,9 +196,7 @@ class ReferralRepository {
sanityCheckPairConfig(config);

if (config.pairs) {
for (const pair of Object.values(config.pairs)) {
sanityCheckPairConfig(pair);
}
Object.values(config.pairs).forEach(sanityCheckPairConfig);
}
}
};
Expand Down
2 changes: 1 addition & 1 deletion lib/rates/FeeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ class FeeProvider {
typeof percentageType === 'number'
? percentageType
: percentageType[orderSide],
referral?.premium(pair, type),
referral?.premium(pair, type, orderSide),
);

return feeType === PercentageFeeType.Calculation
Expand Down
17 changes: 16 additions & 1 deletion lib/rates/providers/RateProviderTaproot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,22 @@ class RateProviderTaproot extends RateProviderBase<SwapTypes> {
[to, from],
].map(([base, quote]) => getPairId({ base, quote }));

const premium = referral?.premiumForPairs(pairIds, type);
const getConfigPairId = (pairIds: string[]) => {
for (const pairId of pairIds) {
if (this.pairConfigs.has(pairId)) {
return pairId;
}
}
throw new Error('could not find pair id');
};

const { quote } = splitPairId(getConfigPairId(pairIds));

const premium = referral?.premiumForPairs(
pairIds,
type,
to === quote ? OrderSide.SELL : OrderSide.BUY,
);
const limits = referral?.limitsForPairs(pairIds, type);

const result = {
Expand Down
42 changes: 25 additions & 17 deletions test/integration/db/models/Referral.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Logger from '../../../../lib/Logger';
import { createApiCredential } from '../../../../lib/Utils';
import { SwapType } from '../../../../lib/consts/Enums';
import { OrderSide, SwapType } from '../../../../lib/consts/Enums';
import Database from '../../../../lib/db/Database';
import ReferralRepository, {
ReferralType,
Expand All @@ -19,6 +19,7 @@ describe('Referral', () => {
premiums: {
[SwapType.Submarine]: -15,
[SwapType.ReverseSubmarine]: 21,
[SwapType.Chain]: { [OrderSide.BUY]: -15, [OrderSide.SELL]: -10 },
},
limits: {
[SwapType.ReverseSubmarine]: {
Expand All @@ -30,7 +31,7 @@ describe('Referral', () => {
'RBTC/BTC': {
maxRoutingFee: 0.0025,
premiums: {
[SwapType.Chain]: -25,
[SwapType.Chain]: { [OrderSide.BUY]: -25, [OrderSide.SELL]: -20 },
},
limits: {
[SwapType.Submarine]: {
Expand Down Expand Up @@ -108,29 +109,36 @@ describe('Referral', () => {
});

test.each`
pair | type | expected
${'BTC/BTC'} | ${SwapType.Submarine} | ${referralValues.config!.premiums![SwapType.Submarine]}
${'BTC/BTC'} | ${SwapType.ReverseSubmarine} | ${referralValues.config!.premiums![SwapType.ReverseSubmarine]}
${'BTC/BTC'} | ${SwapType.Chain} | ${referralValues.config!.premiums![SwapType.Chain]}
${'RBTC/BTC'} | ${SwapType.Chain} | ${referralValues.config!.pairs!['RBTC/BTC']!.premiums![SwapType.Chain]}
pair | type | orderSide | expected
${'BTC/BTC'} | ${SwapType.Submarine} | ${undefined} | ${referralValues.config!.premiums![SwapType.Submarine]}
${'BTC/BTC'} | ${SwapType.ReverseSubmarine} | ${undefined} | ${referralValues.config!.premiums![SwapType.ReverseSubmarine]}
${'BTC/BTC'} | ${SwapType.Chain} | ${OrderSide.BUY} | ${referralValues.config!.premiums![SwapType.Chain]![OrderSide.BUY]}
${'BTC/BTC'} | ${SwapType.Chain} | ${OrderSide.SELL} | ${referralValues.config!.premiums![SwapType.Chain]![OrderSide.SELL]}
${'RBTC/BTC'} | ${SwapType.Chain} | ${OrderSide.BUY} | ${referralValues.config!.pairs!['RBTC/BTC']!.premiums![SwapType.Chain]![OrderSide.BUY]}
${'RBTC/BTC'} | ${SwapType.Chain} | ${OrderSide.SELL} | ${referralValues.config!.pairs!['RBTC/BTC']!.premiums![SwapType.Chain]![OrderSide.SELL]}
`(
'should get premium for pair $pair and type $type',
async ({ pair, type, expected }) => {
async ({ pair, type, orderSide, expected }) => {
const ref = await ReferralRepository.getReferralById(referralValues.id);

expect(ref).not.toBeNull();
expect(ref!.premium(pair, type)).toEqual(expected);
expect(ref!.premium(pair, type, orderSide)).toEqual(expected);
},
);

test.each`
pairs | type | expected
${['BTC/BTC']} | ${SwapType.Chain} | ${referralValues.config!.premiums![SwapType.Chain]}
${['BTC/BTC', 'RBTC/BTC']} | ${SwapType.Chain} | ${referralValues.config!.pairs!['RBTC/BTC']!.premiums![SwapType.Chain]}
`('should get premium for pairs', async ({ pairs, type, expected }) => {
const ref = await ReferralRepository.getReferralById(referralValues.id);
pairs | type | orderSide | expected
${['BTC/BTC']} | ${SwapType.Chain} | ${OrderSide.BUY} | ${referralValues.config!.premiums![SwapType.Chain]![OrderSide.BUY]}
${['BTC/BTC']} | ${SwapType.Chain} | ${OrderSide.SELL} | ${referralValues.config!.premiums![SwapType.Chain]![OrderSide.SELL]}
${['BTC/BTC', 'RBTC/BTC']} | ${SwapType.Chain} | ${OrderSide.BUY} | ${referralValues.config!.pairs!['RBTC/BTC']!.premiums![SwapType.Chain]![OrderSide.BUY]}
${['BTC/BTC', 'RBTC/BTC']} | ${SwapType.Chain} | ${OrderSide.SELL} | ${referralValues.config!.pairs!['RBTC/BTC']!.premiums![SwapType.Chain]![OrderSide.SELL]}
`(
'should get premium for pairs',
async ({ pairs, type, orderSide, expected }) => {
const ref = await ReferralRepository.getReferralById(referralValues.id);

expect(ref).not.toBeNull();
expect(ref!.premiumForPairs(pairs, type)).toEqual(expected);
});
expect(ref).not.toBeNull();
expect(ref!.premiumForPairs(pairs, type, orderSide)).toEqual(expected);
},
);
});
76 changes: 75 additions & 1 deletion test/integration/db/repositories/ReferralRepository.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Logger from '../../../../lib/Logger';
import { SwapType } from '../../../../lib/consts/Enums';
import { OrderSide, SwapType } from '../../../../lib/consts/Enums';
import Database from '../../../../lib/db/Database';
import Referral from '../../../../lib/db/models/Referral';
import ReferralRepository from '../../../../lib/db/repositories/ReferralRepository';
Expand Down Expand Up @@ -148,6 +148,80 @@ describe('ReferralRepository', () => {
).rejects.toEqual(`${field} out of range`);
},
);

test.each`
premium | description
${{ [OrderSide.BUY]: 'not-a-number' }} | ${'invalid BUY value type'}
${{ [OrderSide.SELL]: 'not-a-number' }} | ${'invalid SELL value type'}
${{ [OrderSide.BUY]: true }} | ${'invalid BUY value type'}
${{ [OrderSide.SELL]: {} }} | ${'invalid SELL value type'}
${{ [OrderSide.BUY]: () => {} }} | ${'invalid BUY value type'}
${{ [OrderSide.SELL]: [1, 2] }} | ${'invalid SELL value type'}
`(
'should throw if chain swap premium has $description',
async ({ premium }) => {
await expect(
ReferralRepository.addReferral({
...fixture,
config: {
premiums: {
[SwapType.Chain]: premium,
},
},
}),
).rejects.toEqual('premium values must be numbers');
},
);

test.each`
premium | description
${{}} | ${'empty object'}
${{ [OrderSide.BUY]: 10 }} | ${'only BUY'}
${{ [OrderSide.SELL]: 10 }} | ${'only SELL'}
${{ [OrderSide.BUY]: 10, [OrderSide.SELL]: 10 }} | ${'both BUY and SELL'}
`(
'should accept valid chain swap premium ($description)',
async ({ premium }) => {
const ref = await ReferralRepository.addReferral({
...fixture,
config: {
premiums: {
[SwapType.Chain]: premium,
},
},
});

expect(ref.config?.premiums?.[SwapType.Chain]).toEqual(premium);
},
);

test.each`
buy | sell
${10} | ${20}
${-100} | ${-100}
${0} | ${0}
${100} | ${100}
`(
'should accept valid chain swap premiums (buy: $buy, sell: $sell)',
async ({ buy, sell }) => {
const ref = await ReferralRepository.addReferral({
...fixture,
config: {
premiums: {
[SwapType.Chain]: {
[OrderSide.BUY]: buy,
[OrderSide.SELL]: sell,
},
},
},
});

expect(ref.config?.premiums?.[SwapType.Chain]).toEqual({
[OrderSide.BUY]: buy,
[OrderSide.SELL]: sell,
});
},
);
});
});
});
Loading

0 comments on commit 036923e

Please sign in to comment.