Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ stream.latestTradeDetail$.subscribe((v) => {})
* Account Interface
- [x] Get Perpetual Swap Account Asset Information
- [x] Perpetual Swap Positions
- [ ] Get Account Profit and Loss Fund Flow
- [x] Get Account Profit and Loss Fund Flow
- [ ] Export fund flow
- [ ] User fee rate
* Trade Interface
Expand Down
79 changes: 79 additions & 0 deletions src/bingx-client/services/account-income.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { AccountService } from 'bingx-api/bingx-client/services/account.service';
import { AccountInterface } from 'bingx-api/bingx/account/account.interface';
import { RequestExecutorInterface } from 'bingx-api/bingx/request-executor/request-executor.interface';
import { EndpointInterface } from 'bingx-api/bingx/endpoints/endpoint.interface';
import { BingxAccountProfitLossFundFlowEndpoint } from 'bingx-api/bingx/endpoints/bingx-account-profit-loss-fund-flow-endpoint';

describe('account income service', () => {
let account: AccountInterface;
let capturedEndpoints: EndpointInterface<unknown>[];
let requestExecutor: RequestExecutorInterface;
let executeSpy: jest.SpyInstance;
let nowSpy: jest.SpyInstance<number, []>;

beforeEach(() => {
account = {
getApiKey: jest.fn(() => 'api-key'),
sign: jest.fn(() => ({
toString: () => 'signature',
secretKey: () => 'secret-key',
})),
};

capturedEndpoints = [];
requestExecutor = {
execute<T>(endpoint: EndpointInterface<T>): Promise<T> {
capturedEndpoints.push(endpoint as EndpointInterface<unknown>);
return Promise.resolve(endpoint as unknown as T);
},
};

executeSpy = jest.spyOn(requestExecutor, 'execute');
nowSpy = jest.spyOn(Date, 'now').mockReturnValue(1770000000123);
});

afterEach(() => {
nowSpy.mockRestore();
});

it('dispatches the signed account profit and loss fund flow endpoint', async () => {
const service = new AccountService(requestExecutor);
const startTime = new Date('2026-01-02T03:04:05.006Z');
const endTime = 1770000000000;

const endpoint = (await service.getAccountProfitLossFundFlow(
{
symbol: 'BTC-USDT',
incomeType: 'FUNDING_FEE',
startTime,
endTime,
limit: 100,
recvWindow: 5000,
},
account,
)) as unknown as BingxAccountProfitLossFundFlowEndpoint;

expect(executeSpy).toHaveBeenCalledTimes(1);
expect(capturedEndpoints[0]).toBe(endpoint);
expect(endpoint).toBeInstanceOf(BingxAccountProfitLossFundFlowEndpoint);
expect(endpoint.method()).toBe('get');
expect(endpoint.path()).toBe('/openApi/swap/v2/user/income');
expect(endpoint.parameters().asRecord()).toEqual({
symbol: 'BTC-USDT',
incomeType: 'FUNDING_FEE',
startTime: startTime.getTime().toString(10),
endTime: endTime.toString(10),
limit: '100',
recvWindow: '5000',
timestamp: '1770000000123',
});
});

it('omits optional filters when no fund flow options are provided', () => {
const endpoint = new BingxAccountProfitLossFundFlowEndpoint({}, account);

expect(endpoint.parameters().asRecord()).toEqual({
timestamp: '1770000000123',
});
});
});
13 changes: 13 additions & 0 deletions src/bingx-client/services/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { RequestExecutorInterface } from 'bingx-api/bingx/request-executor/reque
import { AccountInterface } from 'bingx-api/bingx/account/account.interface';
import { BingxGetPerpetualSwapAccountAssetEndpoint } from 'bingx-api/bingx/endpoints/bingx-get-perpetual-swap-account-asset-endpoint';
import { BingxPerpetualSwapPositionsEndpoint } from 'bingx-api/bingx/endpoints/bingx-perpetual-swap-positions-endpoint';
import {
BingxAccountProfitLossFundFlowEndpoint,
BingxAccountProfitLossFundFlowOptions,
} from 'bingx-api/bingx/endpoints/bingx-account-profit-loss-fund-flow-endpoint';

export class AccountService {
constructor(private readonly requestExecutor: RequestExecutorInterface) {}
Expand All @@ -17,4 +21,13 @@ export class AccountService {
new BingxPerpetualSwapPositionsEndpoint(symbol, account),
);
}

public getAccountProfitLossFundFlow(
options: BingxAccountProfitLossFundFlowOptions,
account: AccountInterface,
) {
return this.requestExecutor.execute(
new BingxAccountProfitLossFundFlowEndpoint(options, account),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
AccountInterface,
BingxResponse,
DefaultSignatureParameters,
EndpointInterface,
SignatureParametersInterface,
} from 'bingx-api/bingx';
import { Endpoint } from 'bingx-api/bingx/endpoints/endpoint';

export interface BingxAccountProfitLossFundFlowOptions {
symbol?: string;
incomeType?: string;
startTime?: Date | number;
endTime?: Date | number;
limit?: number;
recvWindow?: string | number;
}

export interface BingxAccountProfitLossFundFlowData {
symbol: string;
incomeType: string;
income: string;
asset: string;
info: string;
time: number;
tranId: string;
tradeId: string;
}

export class BingxAccountProfitLossFundFlowEndpoint<
R = BingxAccountProfitLossFundFlowData[],
>
extends Endpoint<BingxResponse<R>>
implements EndpointInterface<BingxResponse<R>>
{
constructor(
private readonly options: BingxAccountProfitLossFundFlowOptions,
account: AccountInterface,
) {
super(account);
}

method(): 'get' | 'post' | 'put' | 'patch' | 'delete' {
return 'get';
}

parameters(): SignatureParametersInterface {
const parameters: Record<string, string> = {};

if (this.options.symbol !== undefined) {
parameters.symbol = this.options.symbol;
}

if (this.options.incomeType !== undefined) {
parameters.incomeType = this.options.incomeType;
}

if (this.options.startTime !== undefined) {
parameters.startTime = this.timestampAsString(this.options.startTime);
}

if (this.options.endTime !== undefined) {
parameters.endTime = this.timestampAsString(this.options.endTime);
}

if (this.options.limit !== undefined) {
parameters.limit = this.options.limit.toString(10);
}

if (this.options.recvWindow !== undefined) {
parameters.recvWindow = this.options.recvWindow.toString();
}

return new DefaultSignatureParameters(parameters);
}

path(): string {
return '/openApi/swap/v2/user/income';
}

private timestampAsString(value: Date | number): string {
return value instanceof Date
? value.getTime().toString(10)
: value.toString();
}

readonly t!: BingxResponse<R>;
}
1 change: 1 addition & 0 deletions src/bingx/endpoints/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './bingx-cancel-all-orders-endpoint';
export * from './bingx-account-profit-loss-fund-flow-endpoint';
export * from './bingx-close-all-positions-endpoint';
export * from './bingx-generate-listen-key-endpoint';
export * from './bingx-generate-listen-key-response';
Expand Down