import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import axios from 'axios';
import { ConfigService } from '@nestjs/config';
import { OrderService } from './order.service';
import { CreateOrderDto } from 'src/dto/create-order.dto';
@Injectable()
export class OrderPollingService {
private readonly spotwareApiUrl: string;
private readonly apiToken: string;
private readonly logger = new Logger(OrderPollingService.name);
constructor(
private readonly configService: ConfigService,
private readonly orderService: OrderService,
) {
this.spotwareApiUrl = `${this.configService.get<string>('SPOTWARE_API_URL')}`;
this.apiToken = this.configService.get<string>('SPOTWARE_API_TOKEN');
}
// Poll Spotware API every minute
@Cron(CronExpression.EVERY_MINUTE)
async pollPositions() {
this.logger.log('Polling for open and closed positions...');
try {
const openPositions = await this.fetchOpenPositions();
const closedPositions = await this.fetchClosedPositions();
// Process and push positions data to Xano
await this.updateXanoWithPositions(openPositions, closedPositions);
} catch (error) {
this.logger.error(`Error polling positions: ${error.message}`);
}
}
private async fetchOpenPositions() {
try {
const response = await axios.get(`${this.spotwareApiUrl}/v2/webserv/openPositions`, {
headers: { Authorization: `Bearer ${this.apiToken}` },
params: { token: this.apiToken },
});
this.logger.log('Fetched open positions from Spotware');
console.log('Open Positions Data:', response.data);
return this.parseCsvData(response.data);
} catch (error) {
this.logger.error(`Failed to fetch open positions: ${error.message}`);
throw new HttpException('Failed to fetch open positions', HttpStatus.FORBIDDEN);
}
}
private async fetchClosedPositions() {
const now = new Date();
const to = now.toISOString();
const from = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(); // 2-day range
try {
const response = await axios.get(`${this.spotwareApiUrl}/v2/webserv/closedPositions`, {
headers: { Authorization: `Bearer ${this.apiToken}` },
params: { from, to, token: this.apiToken },
});
this.logger.log('Fetched closed positions from Spotware');
console.log('Closed Positions Data:', response.data);
return this.parseCsvData(response.data);
} catch (error) {
this.logger.error(`Failed to fetch closed positions: ${error.message}`);
throw new HttpException('Failed to fetch closed positions', HttpStatus.FORBIDDEN);
}
}
// Parse CSV data from Spotware response
private parseCsvData(csvData: string): any[] {
const rows = csvData.split('\n').slice(1); // Skip header row
return rows
.filter((row) => row.trim())
.map((row) => {
const columns = row.split(',');
return {
login: columns[0],
positionId: columns[1],
openTimestamp: columns[2],
entryPrice: columns[3],
direction: columns[4],
volume: columns[5],
symbol: columns[6],
commission: columns[7],
swap: columns[8],
bookType: columns[9],
stake: columns[10],
spreadBetting: columns[11],
usedMargin: columns[12],
};
});
}
private async updateXanoWithPositions(openPositions: any[], closedPositions: any[]) {
// Process each open position
for (const pos of openPositions) {
const openOrderData: CreateOrderDto = this.mapOpenPositionToOrderDto(pos);
try {
// Log the payload for inspection
console.log('Open Position Payload for Xano:', openOrderData);
await this.orderService.createOrUpdateOrder(openOrderData);
this.logger.log(`Open Position sent to Xano: ${JSON.stringify(openOrderData)}`);
} catch (error) {
this.logger.error(`Failed to send open position to Xano: ${error.message}`);
}
}
// Process each closed position
for (const pos of closedPositions) {
const closedOrderData: CreateOrderDto = this.mapClosedPositionToOrderDto(pos);
try {
// Log the payload for inspection
console.log('Closed Position Payload for Xano:', closedOrderData);
await this.orderService.createOrUpdateOrder(closedOrderData);
this.logger.log(`Closed Position sent to Xano: ${JSON.stringify(closedOrderData)}`);
} catch (error) {
this.logger.error(`Failed to send closed position to Xano: ${error.message}`);
}
}
}
private mapOpenPositionToOrderDto(position: any): CreateOrderDto {
return {
key: position.positionId.toString(),
ticket_id: Number(position.positionId),
account: Number(position.login),
type: position.direction,
symbol: position.symbol,
volume: parseFloat(position.volume), // Ensure volume is a number
entry_price: parseFloat(position.entryPrice),
entry_date: position.openTimestamp,
broker: 'Spotware',
open_reason: position.bookType || 'AUTO',
};
}
private mapClosedPositionToOrderDto(position: any): CreateOrderDto {
return {
key: position.positionId.toString(),
ticket_id: Number(position.positionId),
account: Number(position.login),
type: position.direction,
symbol: position.symbol,
volume: parseFloat(position.volume),
entry_price: parseFloat(position.entryPrice),
entry_date: position.openTimestamp,
close_price: parseFloat(position.closePrice),
close_date: position.closeTimestamp,
profit: parseFloat(position.pnl),
broker: 'Spotware',
open_reason: position.bookType || 'AUTO',
close_reason: 'AUTO',
};
}
}