Skip to content
103 changes: 103 additions & 0 deletions migration/1768494786424-ScryptTradingActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* Add Scrypt SELL actions for CHF->USDT and EUR->USDT trading
* and link them to existing rules 312 (CHF) and 313 (EUR)
*
* @class
* @implements {MigrationInterface}
*/
module.exports = class ScryptTradingActions1768494786424 {
name = 'ScryptTradingActions1768494786424'

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
// Create Scrypt SELL action for CHF -> USDT
await queryRunner.query(`
INSERT INTO "dbo"."liquidity_management_action" ("system", "command", "params")
VALUES ('Scrypt', 'sell', '{"tradeAsset":"USDT"}')
`);

// Get the ID of the newly created CHF action
const chfActionResult = await queryRunner.query(`
SELECT TOP 1 "id" FROM "dbo"."liquidity_management_action"
WHERE "system" = 'Scrypt' AND "command" = 'sell'
ORDER BY "id" DESC
`);
const chfActionId = chfActionResult[0].id;

// Update Rule 312 (Scrypt CHF) with maximal=1000 and link to SELL action
await queryRunner.query(`
UPDATE "dbo"."liquidity_management_rule"
SET "minimal" = 0,
"optimal" = 0,
"maximal" = 1000,
"redundancyStartActionId" = ${chfActionId}
WHERE "id" = 312
`);

// Create Scrypt SELL action for EUR -> USDT (separate action for clarity)
await queryRunner.query(`
INSERT INTO "dbo"."liquidity_management_action" ("system", "command", "params")
VALUES ('Scrypt', 'sell', '{"tradeAsset":"USDT"}')
`);

// Get the ID of the newly created EUR action
const eurActionResult = await queryRunner.query(`
SELECT TOP 1 "id" FROM "dbo"."liquidity_management_action"
WHERE "system" = 'Scrypt' AND "command" = 'sell'
ORDER BY "id" DESC
`);
const eurActionId = eurActionResult[0].id;

// Update Rule 313 (Scrypt EUR) with maximal=1000 and link to SELL action
await queryRunner.query(`
UPDATE "dbo"."liquidity_management_rule"
SET "minimal" = 0,
"optimal" = 0,
"maximal" = 1000,
"redundancyStartActionId" = ${eurActionId}
WHERE "id" = 313
`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
// Get action IDs linked to rules 312 and 313
const rule312 = await queryRunner.query(`
SELECT "redundancyStartActionId" FROM "dbo"."liquidity_management_rule" WHERE "id" = 312
`);
const rule313 = await queryRunner.query(`
SELECT "redundancyStartActionId" FROM "dbo"."liquidity_management_rule" WHERE "id" = 313
`);

// Remove action links from rules and reset values
await queryRunner.query(`
UPDATE "dbo"."liquidity_management_rule"
SET "minimal" = NULL,
"optimal" = NULL,
"maximal" = NULL,
"redundancyStartActionId" = NULL
WHERE "id" IN (312, 313)
`);

// Delete actions if they exist
if (rule312[0]?.redundancyStartActionId) {
await queryRunner.query(`
DELETE FROM "dbo"."liquidity_management_action" WHERE "id" = ${rule312[0].redundancyStartActionId}
`);
}
if (rule313[0]?.redundancyStartActionId && rule313[0].redundancyStartActionId !== rule312[0]?.redundancyStartActionId) {
await queryRunner.query(`
DELETE FROM "dbo"."liquidity_management_action" WHERE "id" = ${rule313[0].redundancyStartActionId}
`);
}
}
}
188 changes: 188 additions & 0 deletions src/integration/exchange/dto/scrypt.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// --- TRANSACTION TYPES --- //

export enum ScryptTransactionType {
WITHDRAWAL = 'Withdrawal',
DEPOSIT = 'Deposit',
}

export enum ScryptTransactionStatus {
COMPLETED = 'Completed',
FAILED = 'Failed',
REJECTED = 'Rejected',
}

export interface ScryptBalance {
Currency: string;
Amount: string;
AvailableAmount: string;
Equivalent?: {
Currency: string;
Amount: string;
AvailableAmount: string;
};
}

export interface ScryptBalanceTransaction {
TransactionID: string;
ClReqID?: string;
Currency: string;
TransactionType: ScryptTransactionType;
Status: ScryptTransactionStatus;
Quantity: string;
Fee?: string;
TxHash?: string;
RejectReason?: string;
RejectText?: string;
Timestamp?: string;
TransactTime?: string;
}

export interface ScryptWithdrawResponse {
id: string;
status: ScryptTransactionStatus;
}

export interface ScryptWithdrawStatus {
id: string;
status: ScryptTransactionStatus;
txHash?: string;
amount?: number;
rejectReason?: string;
rejectText?: string;
}

// --- TRADE TYPES --- //

export enum ScryptTradeSide {
BUY = 'Buy',
SELL = 'Sell',
}

export enum ScryptTradeStatus {
PENDING = 'Pending',
CONFIRMED = 'Confirmed',
CANCELED = 'Canceled',
}

export interface ScryptTrade {
Timestamp: string;
Symbol: string;
OrderID: string;
TradeID: string;
Side: ScryptTradeSide;
TransactTime: string;
ExecType: string;
Currency: string;
Price?: string;
Quantity: string;
Amount: string;
Fee: string;
FeeCurrency?: string;
TradeStatus: ScryptTradeStatus;
AmountCurrency: string;
QuoteID?: string;
RFQID?: string;
CustomerUser?: string;
AggressorSide?: ScryptTradeSide;
DealtCurrency?: string;
}

// --- ORDER TYPES --- //

export enum ScryptOrderStatus {
NEW = 'New',
PARTIALLY_FILLED = 'PartiallyFilled',
FILLED = 'Filled',
CANCELLED = 'Cancelled',
REJECTED = 'Rejected',
}

export enum ScryptOrderSide {
BUY = 'Buy',
SELL = 'Sell',
}

export enum ScryptOrderType {
MARKET = 'Market',
LIMIT = 'Limit',
}

export enum ScryptTimeInForce {
FILL_AND_KILL = 'FillAndKill',
FILL_OR_KILL = 'FillOrKill',
GOOD_TILL_CANCEL = 'GoodTillCancel',
}

export interface ScryptExecutionReport {
ClOrdID: string;
OrigClOrdID?: string;
OrderID?: string;
Symbol: string;
Side: string;
OrdStatus: ScryptOrderStatus;
ExecType?: string;
OrderQty: string;
CumQty: string;
LeavesQty: string;
AvgPx?: string;
Price?: string;
RejectReason?: string;
Text?: string;
}

export interface ScryptOrderResponse {
id: string;
status: ScryptOrderStatus;
}

export interface ScryptOrderInfo {
id: string;
orderId?: string;
symbol: string;
side: string;
status: ScryptOrderStatus;
quantity: number;
filledQuantity: number;
remainingQuantity: number;
avgPrice?: number;
price?: number;
rejectReason?: string;
}

// --- MARKET DATA TYPES --- //

export interface ScryptPriceLevel {
Price: string;
Size: string;
}

export interface ScryptMarketDataSnapshot {
Timestamp: string;
Symbol: string;
Status: string;
Bids: ScryptPriceLevel[];
Offers: ScryptPriceLevel[];
}

export interface ScryptOrderBook {
bids: Array<{ price: number; size: number }>;
offers: Array<{ price: number; size: number }>;
}

// --- SECURITY TYPES --- //

export interface ScryptSecurity {
Symbol: string;
MinimumSize?: string;
MaximumSize?: string;
MinPriceIncrement?: string;
MinSizeIncrement?: string;
}

export interface ScryptSecurityInfo {
symbol: string;
minSize: number;
maxSize: number;
minPriceIncrement: number;
minSizeIncrement: number;
}
1 change: 1 addition & 0 deletions src/integration/exchange/entities/exchange-tx.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,5 @@ export const ExchangeSyncs: ExchangeSync[] = [
tokenReplacements: [],
},
{ exchange: ExchangeName.BINANCE, tradeTokens: ['BTC', 'USDT'], tokenReplacements: [['BTCB', 'BTC']] },
{ exchange: ExchangeName.SCRYPT, tokens: [], tokenReplacements: [] },
];
78 changes: 78 additions & 0 deletions src/integration/exchange/mappers/exchange-tx.mapper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { Trade, Transaction } from 'ccxt';
import { ExchangeTxDto } from '../dto/exchange-tx.dto';
import {
ScryptBalanceTransaction,
ScryptTrade,
ScryptTradeSide,
ScryptTradeStatus,
ScryptTransactionStatus,
ScryptTransactionType,
} from '../dto/scrypt.dto';
import { ExchangeTxType } from '../entities/exchange-tx.entity';
import { ExchangeName } from '../enums/exchange.enum';

Expand Down Expand Up @@ -70,4 +78,74 @@ export class ExchangeTxMapper {
side: t.side,
}));
}

static mapScryptTransactions(transactions: ScryptBalanceTransaction[], exchange: ExchangeName): ExchangeTxDto[] {
return transactions.map((t) => ({
exchange,
type: this.mapScryptTransactionType(t.TransactionType),
externalId: t.TransactionID,
externalCreated: t.TransactTime ? new Date(t.TransactTime) : new Date(),
externalUpdated: t.Timestamp ? new Date(t.Timestamp) : new Date(),
status: this.mapScryptStatus(t.Status),
amount: parseFloat(t.Quantity) || 0,
feeAmount: t.Fee ? parseFloat(t.Fee) : 0,
feeCurrency: t.Currency,
currency: t.Currency,
txId: t.TxHash,
}));
}

private static mapScryptTransactionType(type: ScryptTransactionType): ExchangeTxType {
switch (type) {
case ScryptTransactionType.DEPOSIT:
return ExchangeTxType.DEPOSIT;
case ScryptTransactionType.WITHDRAWAL:
return ExchangeTxType.WITHDRAWAL;
default:
throw new Error(`Unknown Scrypt transaction type: ${type}`);
}
}

private static mapScryptStatus(status: ScryptTransactionStatus): string {
switch (status) {
case ScryptTransactionStatus.COMPLETED:
return 'ok';
case ScryptTransactionStatus.FAILED:
case ScryptTransactionStatus.REJECTED:
return 'failed';
default:
return 'pending';
}
}

static mapScryptTrades(trades: ScryptTrade[], exchange: ExchangeName): ExchangeTxDto[] {
return trades.map((t) => ({
exchange,
type: ExchangeTxType.TRADE,
externalId: t.TradeID,
externalCreated: new Date(t.TransactTime),
externalUpdated: new Date(t.Timestamp),
status: this.mapScryptTradeStatus(t.TradeStatus),
amount: parseFloat(t.Quantity) || 0,
feeAmount: parseFloat(t.Fee) || 0,
feeCurrency: t.FeeCurrency ?? t.Currency,
symbol: t.Symbol.replace('-', '/'),
side: t.Side === ScryptTradeSide.BUY ? 'buy' : 'sell',
price: t.Price ? parseFloat(t.Price) : undefined,
cost: parseFloat(t.Amount) || 0,
order: t.OrderID,
}));
}

private static mapScryptTradeStatus(status: ScryptTradeStatus): string {
switch (status) {
case ScryptTradeStatus.CONFIRMED:
return 'ok';
case ScryptTradeStatus.CANCELED:
return 'canceled';
case ScryptTradeStatus.PENDING:
default:
return 'pending';
}
}
}
Loading
Loading