docker files fixes from liaqat
This commit is contained in:
70
src/Managing.Infrastructure.Web3/Services/ChainService.cs
Normal file
70
src/Managing.Infrastructure.Web3/Services/ChainService.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Evm;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Services;
|
||||
|
||||
public static class ChainService
|
||||
{
|
||||
//private const string RPC_ARBITRUM = "https://convincing-smart-arm.arbitrum-mainnet.discover.quiknode.pro/561ad3fa1db431a2c728c2fdb1a62e8f94acf703/";
|
||||
private const string RPC_ARBITRUM = "https://arb1.arbitrum.io/rpc";
|
||||
private const string RPC_ARBITRUM_GOERLI = "https://arb-goerli.g.alchemy.com/v2/ZMkIiKtNvgY03UtWOjho0oqkQrNt_pyc";
|
||||
private const string RPC_ETHEREUM = "https://mainnet.infura.io/v3/58f44d906ab345beadd03dd2b76348af";
|
||||
private const string RPC_ETHEREUM_GOERLI = "https://eth-goerli.g.alchemy.com/v2/xbc-eM-vxBmM9Uf1-RjjGjLp8Ng-FIc6";
|
||||
|
||||
public static Chain GetChain(string chainName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(chainName))
|
||||
throw new Exception("Chain name is null or empty");
|
||||
|
||||
return GetChains().FirstOrDefault(c => c.Name == chainName);
|
||||
}
|
||||
|
||||
public static List<Chain> GetChains()
|
||||
{
|
||||
var chains = new List<Chain>()
|
||||
{
|
||||
GetArbitrum(),
|
||||
GetEthereum(),
|
||||
//GetArbitrumGoerli(),
|
||||
//GetGoerli()
|
||||
};
|
||||
|
||||
return chains;
|
||||
}
|
||||
|
||||
public static Chain GetArbitrum()
|
||||
{
|
||||
return new Chain()
|
||||
{
|
||||
Name = Constants.Chains.Arbitrum,
|
||||
RpcUrl = RPC_ARBITRUM
|
||||
};
|
||||
}
|
||||
|
||||
public static Chain GetEthereum()
|
||||
{
|
||||
return new Chain()
|
||||
{
|
||||
Name = Constants.Chains.Ethereum,
|
||||
RpcUrl = RPC_ETHEREUM
|
||||
};
|
||||
}
|
||||
|
||||
public static Chain GetArbitrumGoerli()
|
||||
{
|
||||
return new Chain()
|
||||
{
|
||||
Name = Constants.Chains.ArbitrumGoerli,
|
||||
RpcUrl = RPC_ARBITRUM_GOERLI
|
||||
};
|
||||
}
|
||||
|
||||
public static Chain GetGoerli()
|
||||
{
|
||||
return new Chain()
|
||||
{
|
||||
Name = Constants.Chains.Goerli,
|
||||
RpcUrl = RPC_ETHEREUM_GOERLI
|
||||
};
|
||||
}
|
||||
}
|
||||
139
src/Managing.Infrastructure.Web3/Services/Gmx/GmxHelpers.cs
Normal file
139
src/Managing.Infrastructure.Web3/Services/Gmx/GmxHelpers.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using Managing.Domain.Trades;
|
||||
using Managing.Infrastructure.Evm.Models.Gmx;
|
||||
using Managing.Infrastructure.Evm.Referentials;
|
||||
using Nethereum.Web3;
|
||||
using System.Numerics;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Services.Gmx;
|
||||
|
||||
public static class GmxHelpers
|
||||
{
|
||||
|
||||
public static decimal GetQuantityForLeverage(decimal quantity, decimal? leverage)
|
||||
{
|
||||
return leverage.HasValue ? leverage.Value * quantity : quantity;
|
||||
}
|
||||
|
||||
public static (List<string> CollateralTokens, List<string> IndexTokens, List<bool> IsLong) GetPositionQueryData(List<string> contractAddress)
|
||||
{
|
||||
var collateralToken = new List<string>();
|
||||
var indexTokens = new List<string>();
|
||||
var isLongs = new List<bool>();
|
||||
|
||||
foreach (var token in contractAddress)
|
||||
{
|
||||
collateralToken.Add(token);
|
||||
indexTokens.Add(token);
|
||||
isLongs.Add(true);
|
||||
}
|
||||
|
||||
foreach (var token in contractAddress)
|
||||
{
|
||||
collateralToken.Add(Arbitrum.Address.USDC);
|
||||
indexTokens.Add(token);
|
||||
isLongs.Add(false);
|
||||
}
|
||||
|
||||
return (collateralToken, indexTokens, isLongs);
|
||||
}
|
||||
|
||||
public static List<BigInteger> GetIndexesRange(int lastIndex)
|
||||
{
|
||||
var indexes = new List<BigInteger>();
|
||||
|
||||
var limit = 15;
|
||||
var from = (lastIndex - limit) < 0 ? 0 : lastIndex - limit;
|
||||
|
||||
for (int i = from; i <= lastIndex; i++)
|
||||
{
|
||||
indexes.Add(new BigInteger(i));
|
||||
}
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
public static BigInteger GetAcceptablePrice(decimal price, bool isLong)
|
||||
{
|
||||
decimal priceBasisPoints;
|
||||
var basisPointDivisor = 10000m;
|
||||
var allowedSlippage = 34m;
|
||||
var toDecimal = 30;
|
||||
|
||||
if (isLong)
|
||||
{
|
||||
priceBasisPoints = basisPointDivisor - allowedSlippage;
|
||||
}
|
||||
else
|
||||
{
|
||||
priceBasisPoints = basisPointDivisor + allowedSlippage;
|
||||
}
|
||||
var test = Web3.Convert.ToWei(price, toDecimal) * new BigInteger(priceBasisPoints);
|
||||
|
||||
var priceLimit = test / Web3.Convert.ToWei(basisPointDivisor, 0);
|
||||
|
||||
return priceLimit;
|
||||
}
|
||||
|
||||
internal static BigInteger? GetGasLimit()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
internal static List<Trade> Map(List<GmxOrder> orders, Ticker ticker)
|
||||
{
|
||||
return orders.ConvertAll(order => Map(order, ticker));
|
||||
|
||||
}
|
||||
private static Trade Map(GmxOrder order, Ticker ticker)
|
||||
{
|
||||
var trade = new Trade(DateTime.UtcNow,
|
||||
order.IsLong ? TradeDirection.Short : TradeDirection.Long,
|
||||
TradeStatus.Requested,
|
||||
GetTradeType(order.IsLong, order.TriggerAboveThreshold),
|
||||
ticker,
|
||||
Convert.ToDecimal(order.SizeDelta),
|
||||
Convert.ToDecimal(order.TriggerPrice),
|
||||
null,
|
||||
"", ""
|
||||
);
|
||||
|
||||
return trade;
|
||||
}
|
||||
|
||||
public static bool GetTriggerAboveThreshold(bool isLong, TradeType tradeType)
|
||||
{
|
||||
if ((isLong && tradeType == TradeType.TakeProfit) ||
|
||||
(!isLong && tradeType == TradeType.StopLoss))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static TradeType GetTradeType(bool isLong, bool isAboveThreshold)
|
||||
{
|
||||
if ((isLong && isAboveThreshold) ||
|
||||
(!isLong && !isAboveThreshold))
|
||||
{
|
||||
return TradeType.TakeProfit;
|
||||
}
|
||||
|
||||
return TradeType.StopLoss;
|
||||
}
|
||||
|
||||
internal static string GeTimeframe(Timeframe timeframe)
|
||||
{
|
||||
return timeframe switch
|
||||
{
|
||||
Timeframe.FiveMinutes => "5m",
|
||||
Timeframe.FifteenMinutes => "15m",
|
||||
Timeframe.ThirtyMinutes => "30m",
|
||||
Timeframe.OneHour => "1h",
|
||||
Timeframe.FourHour => "4h",
|
||||
Timeframe.OneDay => "1d",
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
177
src/Managing.Infrastructure.Web3/Services/Gmx/GmxMappers.cs
Normal file
177
src/Managing.Infrastructure.Web3/Services/Gmx/GmxMappers.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Trades;
|
||||
using Managing.Infrastructure.Evm.Models.Gmx;
|
||||
using Managing.Infrastructure.Evm.Referentials;
|
||||
using Nethereum.Web3;
|
||||
using System.Numerics;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Services.Gmx;
|
||||
|
||||
public static class GmxMappers
|
||||
{
|
||||
internal static Trade Map(GmxPosition? position, Ticker ticker)
|
||||
{
|
||||
if (position == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var leverage = position.SizeDelta / position.Collateral;
|
||||
|
||||
var trade = new Trade(
|
||||
DateHelpers.GetFromUnixTimestamp((int)position.LastIncreasedTime),
|
||||
position.IsLong ? TradeDirection.Long : TradeDirection.Short,
|
||||
TradeStatus.Filled,
|
||||
TradeType.Limit,
|
||||
ticker,
|
||||
Web3.Convert.FromWei(position.SizeDelta, 30),
|
||||
Web3.Convert.FromWei(position.AveragePrice, 30),
|
||||
(decimal)leverage,
|
||||
"",
|
||||
"");
|
||||
|
||||
return trade;
|
||||
}
|
||||
|
||||
public static List<GmxPosition> MapPositions(List<BigInteger> positionData,
|
||||
(List<string> CollateralTokens, List<string> IndexTokens, List<bool> IsLong) queryData)
|
||||
{
|
||||
var gmxPositions = new List<GmxPosition>();
|
||||
var propLength = 9;
|
||||
|
||||
for (int i = 0; i < queryData.CollateralTokens.Count; i++)
|
||||
{
|
||||
var gmxPosition = new GmxPosition
|
||||
{
|
||||
CollateralToken = queryData.CollateralTokens[i],
|
||||
IndexToken = queryData.IndexTokens[i],
|
||||
IsLong = queryData.IsLong[i],
|
||||
SizeDelta = positionData[i * propLength],
|
||||
Collateral = positionData[i * propLength + 1],
|
||||
AveragePrice = positionData[i * propLength + 2],
|
||||
EntryFundingRate = positionData[i * propLength + 3],
|
||||
//gmxPosition.CumulativeFundingRate = collateralToken.cumulativeFundingRate;
|
||||
HasRealisedProfit = positionData[i * propLength + 4].Equals(1),
|
||||
RealisedPnl = positionData[i * propLength + 5],
|
||||
LastIncreasedTime = positionData[i * propLength + 6],
|
||||
HasProfit = positionData[i * propLength + 7],
|
||||
Delta = positionData[i * propLength + 8]
|
||||
};
|
||||
|
||||
if (!gmxPosition.SizeDelta.IsZero)
|
||||
gmxPositions.Add(gmxPosition);
|
||||
}
|
||||
|
||||
return gmxPositions;
|
||||
}
|
||||
|
||||
public static List<GmxOrder> MapIncrease(
|
||||
List<BigInteger> orderData,
|
||||
List<string> addressData,
|
||||
List<BigInteger> indexes)
|
||||
{
|
||||
var extractor = (List<BigInteger> orderProperty, List<string> address) =>
|
||||
{
|
||||
var order = new GmxOrder
|
||||
{
|
||||
PurchaseToken = address[0].ToString(),
|
||||
CollateralToken = address[1].ToString(),
|
||||
IndexToken = address[2].ToString(),
|
||||
PurchaseTokenAmount = Web3.Convert.FromWeiToBigDecimal(orderProperty[0], 18).ToString(),
|
||||
SizeDelta = Web3.Convert.FromWeiToBigDecimal(orderProperty[1], 30).ToString(),
|
||||
IsLong = orderProperty[2].ToString() == "1",
|
||||
TriggerPrice = Web3.Convert.FromWeiToBigDecimal(orderProperty[3], 30).ToString(),
|
||||
TriggerAboveThreshold = orderProperty[4].ToString() == "1",
|
||||
Type = GmxOrderType.Increase
|
||||
};
|
||||
|
||||
return order;
|
||||
};
|
||||
|
||||
return ParseOrdersData(orderData, addressData, extractor, indexes, 5, 3);
|
||||
}
|
||||
|
||||
public static List<GmxOrder> MapDecrease(
|
||||
List<BigInteger> orderData,
|
||||
List<string> addressData,
|
||||
List<BigInteger> indexes)
|
||||
{
|
||||
var extractor = (List<BigInteger> orderProperty, List<string> address) =>
|
||||
{
|
||||
var order = new GmxOrder
|
||||
{
|
||||
CollateralToken = address[0],
|
||||
IndexToken = address[1],
|
||||
CollateralDelta = orderProperty[0].ToString(),
|
||||
SizeDelta = Web3.Convert.FromWeiToBigDecimal(orderProperty[1], 30).ToString(),
|
||||
IsLong = orderProperty[2].ToString() == "1",
|
||||
TriggerPrice = Web3.Convert.FromWeiToBigDecimal(orderProperty[3], 30).ToString(),
|
||||
TriggerAboveThreshold = orderProperty[4].ToString() == "1",
|
||||
Type = GmxOrderType.Decrease
|
||||
};
|
||||
|
||||
return order;
|
||||
};
|
||||
|
||||
return ParseOrdersData(orderData, addressData, extractor, indexes, 5, 2);
|
||||
}
|
||||
|
||||
public static List<GmxOrder> ParseOrdersData(
|
||||
List<BigInteger> orderData,
|
||||
List<string> addressData,
|
||||
Func<List<BigInteger>, List<string>, GmxOrder> extractor,
|
||||
List<BigInteger> indexes,
|
||||
int uintPropsLength,
|
||||
int addressPropsLength)
|
||||
{
|
||||
if (orderData.Count == 0 || addressData.Count == 0)
|
||||
{
|
||||
return new List<GmxOrder>();
|
||||
}
|
||||
|
||||
var count = orderData.Count / uintPropsLength;
|
||||
|
||||
var orders = new List<GmxOrder>();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var slicedAddress = addressData
|
||||
.Skip(addressPropsLength * i)
|
||||
.Take(addressPropsLength)
|
||||
.ToList();
|
||||
|
||||
if (slicedAddress[0] == Arbitrum.Address.Zero && slicedAddress[1] == Arbitrum.Address.Zero)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var slicedProperty = orderData
|
||||
.Skip(uintPropsLength * i)
|
||||
.Take(uintPropsLength * i)
|
||||
.ToList();
|
||||
|
||||
var order = extractor(slicedProperty, slicedAddress);
|
||||
order.Index = indexes[i];
|
||||
orders.Add(order);
|
||||
}
|
||||
|
||||
return orders;
|
||||
}
|
||||
|
||||
internal static Candle Map(GmxOhlc price, Ticker ticker, Timeframe timeframe)
|
||||
{
|
||||
return new Candle()
|
||||
{
|
||||
Date = DateHelpers.GetFromUnixTimestamp(price.t),
|
||||
OpenTime = DateHelpers.GetFromUnixTimestamp(price.t).AddSeconds(-1),
|
||||
Open = Convert.ToDecimal(price.o),
|
||||
High = Convert.ToDecimal(price.h),
|
||||
Low = Convert.ToDecimal(price.l),
|
||||
Close = Convert.ToDecimal(price.c),
|
||||
Exchange = TradingExchanges.Evm,
|
||||
Ticker = ticker.ToString(),
|
||||
Timeframe = timeframe
|
||||
};
|
||||
}
|
||||
}
|
||||
458
src/Managing.Infrastructure.Web3/Services/Gmx/GmxService.cs
Normal file
458
src/Managing.Infrastructure.Web3/Services/Gmx/GmxService.cs
Normal file
@@ -0,0 +1,458 @@
|
||||
using Managing.Domain.Trades;
|
||||
using Managing.Infrastructure.Evm.Models.Gmx;
|
||||
using Managing.Infrastructure.Evm.Referentials;
|
||||
using Managing.Tools.OrderBook;
|
||||
using Managing.Tools.OrderBook.ContractDefinition;
|
||||
using Managing.Tools.OrderBookReader;
|
||||
using Managing.Tools.OrderBookReader.ContractDefinition;
|
||||
using Managing.Tools.PositionRouter;
|
||||
using Managing.Tools.PositionRouter.ContractDefinition;
|
||||
using Managing.Tools.Reader;
|
||||
using Managing.Tools.Reader.ContractDefinition;
|
||||
using Managing.Tools.Router;
|
||||
using Managing.Tools.Router.ContractDefinition;
|
||||
using Nethereum.Contracts.Standards.ERC20;
|
||||
using Nethereum.Contracts.Standards.ERC20.ContractDefinition;
|
||||
using Nethereum.Util;
|
||||
using Nethereum.Web3;
|
||||
using System.Numerics;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Services.Gmx;
|
||||
|
||||
public static class GmxService
|
||||
{
|
||||
private const decimal _orderFeesExecution = 0.0003m;
|
||||
private const decimal _positionUpdateFees = 0.0001m;
|
||||
|
||||
public async static Task InitAccountForTrading(Web3 web3, string publicAddress, List<Ticker> tickers)
|
||||
{
|
||||
var router = new RouterService(web3, Arbitrum.Address.Router);
|
||||
if (!await IsPluginAdded(web3, publicAddress, Arbitrum.Address.PositionRouter))
|
||||
{
|
||||
var routerApproval = await router
|
||||
.ApprovePluginRequestAndWaitForReceiptAsync(Arbitrum.Address.PositionRouter);
|
||||
}
|
||||
|
||||
|
||||
if (!await IsPluginAdded(web3, publicAddress, Arbitrum.Address.OrderBook))
|
||||
{
|
||||
var routerApproval = await router
|
||||
.ApprovePluginRequestAsync(Arbitrum.Address.OrderBook);
|
||||
}
|
||||
|
||||
foreach (var ticker in tickers)
|
||||
{
|
||||
var conntractAddress = TokenService.GetContractAddress(ticker);
|
||||
await ApproveToken(web3, publicAddress, conntractAddress);
|
||||
}
|
||||
}
|
||||
|
||||
public async static Task<bool> IsPluginAdded(Web3 web3, string publicAddress, string pluginAddress)
|
||||
{
|
||||
var router = new RouterService(web3, Arbitrum.Address.Router);
|
||||
var function = new ApprovedPluginsFunction
|
||||
{
|
||||
ReturnValue1 = publicAddress,
|
||||
ReturnValue2 = pluginAddress
|
||||
};
|
||||
|
||||
var isAdded = await router.ApprovedPluginsQueryAsync(function);
|
||||
return isAdded;
|
||||
}
|
||||
|
||||
public async static Task ApproveToken(Web3 web3, string publicAddress, string contractAddress)
|
||||
{
|
||||
var input = new Nethereum.Contracts.Standards.ERC20.ContractDefinition.ApproveFunction
|
||||
{
|
||||
Spender = publicAddress
|
||||
};
|
||||
|
||||
var contract = new ERC20ContractService(web3.Eth, contractAddress);
|
||||
var approval = await contract.ApproveRequestAsync(input);
|
||||
}
|
||||
|
||||
public static async Task<bool> ApproveOrder(Web3 web3, Ticker ticker, string publicAddress, decimal amount)
|
||||
{
|
||||
var contractAddress = TokenService.GetContractAddress(ticker);
|
||||
var contract = new ERC20ContractService(web3.Eth, contractAddress);
|
||||
|
||||
var allowanceQuery = new AllowanceFunction
|
||||
{
|
||||
Owner = publicAddress,
|
||||
Spender = Arbitrum.Address.Router
|
||||
};
|
||||
|
||||
var allowance = await contract.AllowanceQueryAsync(allowanceQuery);
|
||||
|
||||
if (allowance.IsZero) return false;
|
||||
|
||||
var approveQuery = new Nethereum.Contracts.Standards.ERC20.ContractDefinition.ApproveFunction
|
||||
{
|
||||
FromAddress = publicAddress,
|
||||
Spender = Arbitrum.Address.Router,
|
||||
Value = Web3.Convert.ToWei(amount)
|
||||
};
|
||||
|
||||
var approval = await contract.ApproveRequestAsync(approveQuery);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async static Task<Trade> IncreasePosition(Web3 web3, string publicAddress, Ticker ticker, TradeDirection direction, decimal price, decimal quantity, decimal? leverage)
|
||||
{
|
||||
var quantityLeveraged = GmxHelpers.GetQuantityForLeverage(quantity, leverage);
|
||||
var orderBook = new OrderBookService(web3, Arbitrum.Address.OrderBook);
|
||||
var contractAddress = TokenService.GetContractAddress(ticker);
|
||||
var isLong = direction == TradeDirection.Long;
|
||||
var function = new CreateIncreaseOrderFunction();
|
||||
|
||||
// Forcing path to use USDC to pay the trade
|
||||
function.Path = new List<string> { Arbitrum.Address.USDC };
|
||||
function.AmountIn = Web3.Convert.ToWei(quantity * price, 6); // Price in $ to pay the long/short. Ex 11.42$
|
||||
function.IndexToken = contractAddress; // Token to long/short
|
||||
function.MinOut = new BigInteger(0);
|
||||
// Size of the position with the leveraged quantity
|
||||
// Ex : Long 11$ x3. SizeDelta = 33$
|
||||
function.SizeDelta = Web3.Convert.ToWei(quantityLeveraged * price, UnitConversion.EthUnit.Tether);
|
||||
function.CollateralToken = Arbitrum.Address.USDC; // USDC
|
||||
function.IsLong = isLong;
|
||||
function.TriggerPrice = Web3.Convert.ToWei(price, 30); // Price of the order execution
|
||||
function.TriggerAboveThreshold = false;
|
||||
function.ExecutionFee = Web3.Convert.ToWei(_orderFeesExecution); // Fee required to execute tx
|
||||
function.ShouldWrap = false;
|
||||
|
||||
// Specify the tx opts
|
||||
function.AmountToSend = Web3.Convert.ToWei(_orderFeesExecution);
|
||||
function.FromAddress = publicAddress;
|
||||
//function.MaxFeePerGas = await orderBook.ContractHandler.EstimateGasAsync(function);
|
||||
function.GasPrice = GetGasPrice();
|
||||
|
||||
// Approving Router to transfer ERC20 token
|
||||
var approval = await ApproveOrder(web3, Ticker.USDC, publicAddress, _orderFeesExecution);
|
||||
|
||||
if (!approval) return null;
|
||||
|
||||
var receipt = await orderBook
|
||||
.CreateIncreaseOrderRequestAndWaitForReceiptAsync(function);
|
||||
|
||||
var trade = new Trade(DateTime.UtcNow,
|
||||
direction,
|
||||
TradeStatus.Requested,
|
||||
TradeType.Limit,
|
||||
ticker,
|
||||
quantity,
|
||||
price,
|
||||
leverage,
|
||||
receipt.TransactionHash,
|
||||
"");
|
||||
|
||||
return trade;
|
||||
}
|
||||
|
||||
public async static Task<Trade> DecreasePosition(Web3 web3, string publicAddress, Ticker ticker, TradeDirection direction, decimal price, decimal quantity, decimal? leverage)
|
||||
{
|
||||
var trade = new Trade(DateTime.UtcNow,
|
||||
direction,
|
||||
TradeStatus.Cancelled,
|
||||
TradeType.Market,
|
||||
ticker,
|
||||
quantity,
|
||||
price,
|
||||
leverage,
|
||||
"",
|
||||
"");
|
||||
|
||||
// Check if there is quantity in position
|
||||
if (await QuantityInPosition(web3, publicAddress, ticker) == 0) return trade;
|
||||
|
||||
var quantityLeveraged = GmxHelpers.GetQuantityForLeverage(quantity, leverage);
|
||||
var positionRouter = new PositionRouterService(web3, Arbitrum.Address.PositionRouter);
|
||||
var contractAddress = TokenService.GetContractAddress(ticker);
|
||||
var isLong = direction == TradeDirection.Long;
|
||||
var function = new CreateDecreasePositionFunction();
|
||||
|
||||
// Forcing path to use contract address to widthdraw funds
|
||||
// The address for closing a short, should be USDC
|
||||
//function.Path = new List<string> { contractAddress };
|
||||
function.Path = new List<string> { Arbitrum.Address.USDC };
|
||||
// the index token of the position
|
||||
function.IndexToken = contractAddress; // Token to long/short
|
||||
// the amount of collateral in USD value to withdraw
|
||||
function.CollateralDelta = new BigInteger(0); // Price in $ to pay the long/short. Ex 11.42$
|
||||
//function.CollateralDelta = Web3.Convert.ToWei(quantity * price, 6); // Price in $ to pay the long/short. Ex 11.42$
|
||||
// the USD value of the change in position size
|
||||
function.SizeDelta = Web3.Convert.ToWei(quantity, UnitConversion.EthUnit.Tether);
|
||||
function.IsLong = isLong;
|
||||
// the address to receive the withdrawn tokens
|
||||
function.Receiver = publicAddress;
|
||||
// the USD value of the min (for longs) or max (for shorts) index price acceptable when executing the request
|
||||
function.AcceptablePrice = GmxHelpers.GetAcceptablePrice(price, isLong);
|
||||
// the min output token amount
|
||||
function.MinOut = new BigInteger(0);
|
||||
function.ExecutionFee = Web3.Convert.ToWei(_positionUpdateFees); // Fee required to execute tx
|
||||
function.WithdrawETH = false;
|
||||
function.CallbackTarget = Arbitrum.Address.Zero;
|
||||
|
||||
// Specify the tx opts
|
||||
function.AmountToSend = Web3.Convert.ToWei(_positionUpdateFees);
|
||||
function.FromAddress = publicAddress;
|
||||
function.MaxFeePerGas = await positionRouter.ContractHandler.EstimateGasAsync(function);
|
||||
function.GasPrice = GetGasPrice();
|
||||
|
||||
var approval = await ApproveOrder(web3, ticker, publicAddress, _positionUpdateFees);
|
||||
|
||||
if (!approval) return null;
|
||||
|
||||
var receipt = await positionRouter
|
||||
.CreateDecreasePositionRequestAndWaitForReceiptAsync(function);
|
||||
|
||||
trade.SetExchangeOrderId(receipt.TransactionHash);
|
||||
trade.SetStatus(receipt.Status.Value.IsOne ? TradeStatus.Requested : TradeStatus.Cancelled);
|
||||
|
||||
return trade;
|
||||
}
|
||||
|
||||
public static async Task<Trade> DecreaseOrder(Web3 web3, TradeType tradeType, string publicAddress, Ticker ticker, TradeDirection direction, decimal price, decimal quantity, decimal? leverage)
|
||||
{
|
||||
var trade = new Trade(DateTime.UtcNow,
|
||||
direction,
|
||||
TradeStatus.Cancelled,
|
||||
tradeType,
|
||||
ticker,
|
||||
quantity,
|
||||
price,
|
||||
leverage,
|
||||
"",
|
||||
"");
|
||||
|
||||
// Check if there is quantity in position
|
||||
var currentPosition = await GetGmxPosition(web3, publicAddress, ticker);
|
||||
|
||||
if (currentPosition == null || currentPosition?.SizeDelta == 0) return trade;
|
||||
|
||||
var quantityLeveraged = GmxHelpers.GetQuantityForLeverage(quantity, leverage);
|
||||
var orderbook = new OrderBookService(web3, Arbitrum.Address.OrderBook);
|
||||
var contractAddress = TokenService.GetContractAddress(ticker);
|
||||
var isLong = direction != TradeDirection.Long;
|
||||
var function = new CreateDecreaseOrderFunction();
|
||||
|
||||
// the index token of the position
|
||||
function.IndexToken = contractAddress; // Token to long/short
|
||||
// the USD value of the change in position size
|
||||
function.SizeDelta = currentPosition.SizeDelta;
|
||||
function.CollateralToken = Arbitrum.Address.USDC;
|
||||
// the amount of collateral in USD value to withdraw
|
||||
function.CollateralDelta = new BigInteger(0); // Price in $ to pay the long/short. Ex 11.42$
|
||||
//function.CollateralDelta = Web3.Convert.ToWei(quantity * price, 6); // Price in $ to pay the long/short. Ex 11.42$
|
||||
function.IsLong = isLong;
|
||||
// the USD value of the min (for longs) or max (for shorts) index price acceptable when executing the request
|
||||
function.TriggerPrice = GmxHelpers.GetAcceptablePrice(price, isLong);
|
||||
function.TriggerAboveThreshold = GmxHelpers.GetTriggerAboveThreshold(isLong, tradeType);
|
||||
|
||||
// Specify the tx opts
|
||||
function.AmountToSend = Web3.Convert.ToWei(_orderFeesExecution);
|
||||
function.FromAddress = publicAddress;
|
||||
function.MaxFeePerGas = await orderbook.ContractHandler.EstimateGasAsync(function);
|
||||
function.GasPrice = GetGasPrice();
|
||||
|
||||
var approval = await ApproveOrder(web3, ticker, publicAddress, _positionUpdateFees);
|
||||
|
||||
if (!approval) return null;
|
||||
|
||||
var receipt = await orderbook
|
||||
.CreateDecreaseOrderRequestAndWaitForReceiptAsync(function);
|
||||
|
||||
trade.SetExchangeOrderId(receipt.TransactionHash);
|
||||
trade.SetStatus(TradeStatus.Requested);
|
||||
return trade;
|
||||
}
|
||||
|
||||
public async static Task<bool> CancelOrders(Web3 web3, string publicAddress, Ticker ticker)
|
||||
{
|
||||
var orderBook = new OrderBookService(web3, Arbitrum.Address.OrderBook);
|
||||
var orders = await GetOrders(web3, publicAddress, ticker);
|
||||
|
||||
if (!orders.Any()) return true;
|
||||
|
||||
var function = new CancelMultipleFunction();
|
||||
var increaseOrderIndexes = orders.Where(i => i.Type == GmxOrderType.Increase);
|
||||
function.IncreaseOrderIndexes = increaseOrderIndexes.Select(o => o.Index).ToList();
|
||||
|
||||
var decreaseOrderIndexes = orders.Where(i => i.Type == GmxOrderType.Decrease);
|
||||
function.DecreaseOrderIndexes = decreaseOrderIndexes.Select(o => o.Index).ToList();
|
||||
|
||||
function.SwapOrderIndexes = new List<BigInteger>();
|
||||
|
||||
try
|
||||
{
|
||||
if (function.DecreaseOrderIndexes.Any() || function.IncreaseOrderIndexes.Any())
|
||||
{
|
||||
function.MaxFeePerGas = await orderBook.ContractHandler.EstimateGasAsync(function);
|
||||
function.GasPrice = GetGasPrice();
|
||||
|
||||
var cancellation = await orderBook.CancelMultipleRequestAndWaitForReceiptAsync(function);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static BigInteger GetGasPrice()
|
||||
{
|
||||
return Web3.Convert.ToWei(0.1, UnitConversion.EthUnit.Gwei);
|
||||
}
|
||||
|
||||
public static async Task<List<GmxOrder>> GetOrders(Web3 web3, string publicAddress, Ticker ticker)
|
||||
{
|
||||
var lastIndexes = await GetLastIndex(web3, publicAddress);
|
||||
var orders = new List<GmxOrder>();
|
||||
var orderBookReader = new OrderBookReaderService(web3, Arbitrum.Address.OrderBookReader);
|
||||
|
||||
var increaseOrders = await GetIncreaseOrders(orderBookReader, publicAddress, lastIndexes.IncreaseIndex);
|
||||
var decreaseOrders = await GetDecreaseOrders(orderBookReader, publicAddress, lastIndexes.DecreaseIndex);
|
||||
|
||||
orders.AddRange(increaseOrders);
|
||||
orders.AddRange(decreaseOrders);
|
||||
var contractAddress = TokenService.GetContractAddress(ticker);
|
||||
var ordersFiltered = orders.Where(o => string.Equals(o.IndexToken, contractAddress, StringComparison.CurrentCultureIgnoreCase));
|
||||
|
||||
return ordersFiltered.ToList();
|
||||
}
|
||||
|
||||
private static async Task<List<GmxOrder>> GetIncreaseOrders(OrderBookReaderService orderBookReader, string publicAddress, int lastIndex)
|
||||
{
|
||||
var increaseIndex = GmxHelpers.GetIndexesRange(lastIndex);
|
||||
var increaseOrdersFunction = new GetIncreaseOrdersFunction
|
||||
{
|
||||
OrderBookAddress = Arbitrum.Address.OrderBook,
|
||||
Account = publicAddress,
|
||||
Indices = increaseIndex,
|
||||
};
|
||||
|
||||
var increaseOrders = await orderBookReader.GetIncreaseOrdersQueryAsync(increaseOrdersFunction);
|
||||
|
||||
return GmxMappers.MapIncrease(increaseOrders.ReturnValue1, increaseOrders.ReturnValue2, increaseIndex);
|
||||
}
|
||||
|
||||
private static async Task<List<GmxOrder>> GetDecreaseOrders(OrderBookReaderService orderBookReader, string publicAddress, int lastIndex)
|
||||
{
|
||||
var increaseIndex = GmxHelpers.GetIndexesRange(lastIndex);
|
||||
var increaseOrdersFunction = new GetDecreaseOrdersFunction
|
||||
{
|
||||
OrderBookAddress = Arbitrum.Address.OrderBook,
|
||||
Account = publicAddress,
|
||||
Indices = increaseIndex,
|
||||
};
|
||||
|
||||
var increaseOrders = await orderBookReader.GetDecreaseOrdersQueryAsync(increaseOrdersFunction);
|
||||
|
||||
return GmxMappers.MapDecrease(increaseOrders.ReturnValue1, increaseOrders.ReturnValue2, increaseIndex);
|
||||
}
|
||||
|
||||
public static async Task<GmxOrderIndexes> GetLastIndex(Web3 web3, string publicAddress)
|
||||
{
|
||||
var orderBook = new OrderBookService(web3, Arbitrum.Address.OrderBook);
|
||||
var increaseFunction = new IncreaseOrdersIndexFunction
|
||||
{
|
||||
ReturnValue1 = publicAddress
|
||||
};
|
||||
var decreaseFunction = new DecreaseOrdersIndexFunction
|
||||
{
|
||||
ReturnValue1 = publicAddress
|
||||
};
|
||||
var swapFunction = new SwapOrdersIndexFunction
|
||||
{
|
||||
ReturnValue1 = publicAddress
|
||||
};
|
||||
|
||||
var increaseIndex = await orderBook.IncreaseOrdersIndexQueryAsync(increaseFunction);
|
||||
var decreaseIndex = await orderBook.DecreaseOrdersIndexQueryAsync(decreaseFunction);
|
||||
var swapIndex = await orderBook.SwapOrdersIndexQueryAsync(swapFunction);
|
||||
|
||||
var indexes = new GmxOrderIndexes
|
||||
{
|
||||
SwapIndex = (int)swapIndex > 0 ? (int)swapIndex - 1 : (int)swapIndex,
|
||||
IncreaseIndex = (int)increaseIndex > 0 ? (int)increaseIndex - 1 : (int)increaseIndex,
|
||||
DecreaseIndex = (int)decreaseIndex > 0 ? (int)decreaseIndex - 1 : (int)decreaseIndex
|
||||
};
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
public static async Task<Trade> GetTrade(Web3 web3, string publicAddress, Ticker ticker)
|
||||
{
|
||||
var position = await GetGmxPosition(web3, publicAddress, ticker);
|
||||
return GmxMappers.Map(position, ticker);
|
||||
}
|
||||
|
||||
public static async Task<GmxPosition> GetGmxPosition(Web3 web3, string publicAddress, Ticker ticker)
|
||||
{
|
||||
var reader = new ReaderService(web3, Arbitrum.Address.Reader);
|
||||
var contractAddress = TokenService.GetContractAddress(ticker);
|
||||
var queryData = GmxHelpers.GetPositionQueryData(new List<string> { contractAddress });
|
||||
|
||||
var function = new GetPositionsFunction
|
||||
{
|
||||
Vault = Arbitrum.Address.Vault,
|
||||
Account = publicAddress,
|
||||
CollateralTokens = queryData.CollateralTokens,
|
||||
IndexTokens = queryData.IndexTokens,
|
||||
IsLong = queryData.IsLong
|
||||
};
|
||||
|
||||
var result = await reader.GetPositionsQueryAsync(function);
|
||||
|
||||
var positions = GmxMappers.MapPositions(result, queryData);
|
||||
var position = positions.FirstOrDefault(p => p.IndexToken == contractAddress);
|
||||
return position;
|
||||
}
|
||||
|
||||
public static async Task<decimal> QuantityInPosition(Web3 web3, string key, Ticker ticker)
|
||||
{
|
||||
var position = await GetTrade(web3, key, ticker);
|
||||
return position?.Quantity ?? 0m;
|
||||
}
|
||||
|
||||
public static async Task<decimal> GetFee(Web3 web3, decimal ethPrice)
|
||||
{
|
||||
var positionRouter = new PositionRouterService(web3, Arbitrum.Address.PositionRouter);
|
||||
var contractAddress = TokenService.GetContractAddress(Ticker.BTC);
|
||||
var function = new CreateDecreasePositionFunction();
|
||||
|
||||
function.Path = new List<string> { contractAddress };
|
||||
function.IndexToken = contractAddress; // Token to long/short
|
||||
function.CollateralDelta = new BigInteger(0); // Price in $ to pay the long/short. Ex 11.42$
|
||||
function.SizeDelta = Web3.Convert.ToWei(100, UnitConversion.EthUnit.Tether);
|
||||
function.IsLong = true;
|
||||
function.Receiver = Arbitrum.Address.Zero;
|
||||
function.AcceptablePrice = GmxHelpers.GetAcceptablePrice(100, true);
|
||||
function.MinOut = new BigInteger(0);
|
||||
function.ExecutionFee = Web3.Convert.ToWei(_positionUpdateFees); // Fee required to execute tx
|
||||
function.WithdrawETH = false;
|
||||
function.CallbackTarget = Arbitrum.Address.Zero;
|
||||
|
||||
function.AmountToSend = Web3.Convert.ToWei(_positionUpdateFees);
|
||||
function.FromAddress = Arbitrum.Address.Zero;
|
||||
|
||||
var totalCost = 0m;
|
||||
|
||||
try
|
||||
{
|
||||
var gasCost = await positionRouter.ContractHandler.EstimateGasAsync(function);
|
||||
var gasPrice = GetGasPrice();
|
||||
var gas = gasPrice * gasCost;
|
||||
totalCost = ethPrice * Web3.Convert.FromWei(gas, 18);
|
||||
return totalCost;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
|
||||
return totalCost;
|
||||
}
|
||||
}
|
||||
45
src/Managing.Infrastructure.Web3/Services/NftService.cs
Normal file
45
src/Managing.Infrastructure.Web3/Services/NftService.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Nethereum.Contracts;
|
||||
using Nethereum.Contracts.Standards.ERC721.ContractDefinition;
|
||||
using Nethereum.Web3;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Services;
|
||||
|
||||
public static class NftService
|
||||
{
|
||||
public static async Task<List<EventLog<TransferEventDTO>>> GetNftEvent(Web3 web3, string owner, string contract)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Retrieve transfer event of the contract
|
||||
var transferEvent = web3.Eth.GetEvent<TransferEventDTO>(contract);
|
||||
|
||||
// Create IN & OUT filter to filter the return transfers changes
|
||||
var transferFilterIn = transferEvent.CreateFilterInput<string, string>(null, owner);
|
||||
var transferFilterOut = transferEvent.CreateFilterInput(owner);
|
||||
|
||||
// Retrieve changes based on filter
|
||||
var transferInLogs = await transferEvent.GetAllChangesAsync(transferFilterIn);
|
||||
var transferOutLogs = await transferEvent.GetAllChangesAsync(transferFilterOut);
|
||||
|
||||
var list = new List<EventLog<TransferEventDTO>>();
|
||||
|
||||
// For each transfer IN, we add the event into the list
|
||||
foreach (var ins in transferInLogs)
|
||||
{
|
||||
list.Add(ins);
|
||||
}
|
||||
|
||||
// Remove all transfer OUT of the list because the use might already send the token
|
||||
foreach (var ins in transferOutLogs)
|
||||
{
|
||||
list.Remove(ins);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/Managing.Infrastructure.Web3/Services/SubgraphService.cs
Normal file
45
src/Managing.Infrastructure.Web3/Services/SubgraphService.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using GraphQL.Client.Http;
|
||||
using GraphQL.Client.Serializer.SystemTextJson;
|
||||
using Managing.Domain.Evm;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Services;
|
||||
|
||||
public static class SubgraphService
|
||||
{
|
||||
private const string SUBGRAPH_UNISWAP_V2 = "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2";
|
||||
private const string SUBGRAPH_CHAINLINK = "https://api.thegraph.com/subgraphs/name/openpredict/chainlink-prices-subgraph";
|
||||
private const string SUBGRAPH_CHAINLINK_GMX = "https://api.thegraph.com/subgraphs/name/deividask/chainlink";
|
||||
private const string SUBGRAPH_GBC = "https://api.thegraph.com/subgraphs/name/nissoh/gmx-arbitrum";
|
||||
|
||||
public static GraphQLHttpClient GetSubgraphClient(SubgraphProvider subgraphProvider)
|
||||
{
|
||||
var url = GetSubgraph(subgraphProvider).Url;
|
||||
var graphQLOptions = new GraphQLHttpClientOptions
|
||||
{
|
||||
EndPoint = new Uri(url)
|
||||
};
|
||||
return new GraphQLHttpClient(graphQLOptions, new SystemTextJsonSerializer());
|
||||
}
|
||||
|
||||
private static Subgraph GetSubgraph(SubgraphProvider subgraphProvider)
|
||||
{
|
||||
return new Subgraph()
|
||||
{
|
||||
SubgraphProvider = subgraphProvider,
|
||||
Url = GetSubgraphUrl(subgraphProvider)
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetSubgraphUrl(SubgraphProvider subgraphProvider)
|
||||
{
|
||||
return subgraphProvider switch
|
||||
{
|
||||
SubgraphProvider.UniswapV2 => SUBGRAPH_UNISWAP_V2,
|
||||
SubgraphProvider.ChainlinkPrice => SUBGRAPH_CHAINLINK,
|
||||
SubgraphProvider.ChainlinkGmx => SUBGRAPH_CHAINLINK_GMX,
|
||||
SubgraphProvider.Gbc => SUBGRAPH_GBC,
|
||||
_ => throw new Exception("No url for subgraphprovider")
|
||||
};
|
||||
}
|
||||
}
|
||||
65
src/Managing.Infrastructure.Web3/Services/TokenService.cs
Normal file
65
src/Managing.Infrastructure.Web3/Services/TokenService.cs
Normal file
File diff suppressed because one or more lines are too long
98
src/Managing.Infrastructure.Web3/Services/TradaoService.cs
Normal file
98
src/Managing.Infrastructure.Web3/Services/TradaoService.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Domain.Statistics;
|
||||
using Managing.Domain.Trades;
|
||||
using Managing.Infrastructure.Evm.Models;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace Managing.Infrastructure.Evm.Services;
|
||||
|
||||
public class TradaoService : ITradaoService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public TradaoService()
|
||||
{
|
||||
_httpClient = new HttpClient(); ;
|
||||
}
|
||||
|
||||
public async Task<List<Trader>> GetBadTrader()
|
||||
{
|
||||
var bestTraders = await _httpClient.GetFromJsonAsync<TradaoList>($"https://api.tradao.xyz/v1/td/dashboard/42161/gmx/pnlTop/500/asc/2592000/0/?current=1&limit=500&order=asc&window=2592000&chain=42161&exchange=gmx&openPosition=0");
|
||||
|
||||
if (bestTraders == null || bestTraders.row.Count == 0)
|
||||
{
|
||||
return new List<Trader>();
|
||||
}
|
||||
|
||||
return await GetTraderDetails(bestTraders);
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<Trader>> GetBestTrader()
|
||||
{
|
||||
var bestTraders = await _httpClient.GetFromJsonAsync<TradaoList>($"https://api.tradao.xyz/v1/td/dashboard/42161/gmx/pnlTop/500/desc/2592000/0/?current=1&limit=500&order=desc&window=2592000&chain=42161&exchange=gmx&openPosition=0");
|
||||
|
||||
if (bestTraders == null || bestTraders.row.Count == 0)
|
||||
{
|
||||
return new List<Trader>();
|
||||
}
|
||||
|
||||
return await GetTraderDetails(bestTraders);
|
||||
}
|
||||
|
||||
public async Task<List<Trade>> GetTrades(string address)
|
||||
{
|
||||
var response = await _httpClient.GetFromJsonAsync<TradaoUserDetails>($"https://api.tradao.xyz/v1/td/trader/42161/gmx/insights/{address}");
|
||||
|
||||
var trades = new List<Trade>();
|
||||
|
||||
if (response == null) return trades;
|
||||
|
||||
foreach (var position in response.openPositions)
|
||||
{
|
||||
var trade = new Trade(
|
||||
DateTime.UtcNow,
|
||||
position.isLong ? Common.Enums.TradeDirection.Long : Common.Enums.TradeDirection.Short,
|
||||
Common.Enums.TradeStatus.Filled,
|
||||
Common.Enums.TradeType.Market,
|
||||
TokenService.GetTicker(position.indexTokenAddress),
|
||||
Convert.ToDecimal(position.collateral),
|
||||
Convert.ToDecimal(position.averagePrice),
|
||||
Convert.ToDecimal(position.position) / Convert.ToDecimal(position.collateral),
|
||||
address, position.liqPrice
|
||||
);
|
||||
|
||||
trades.Add(trade);
|
||||
}
|
||||
|
||||
return trades;
|
||||
}
|
||||
|
||||
private async Task<List<Trader>> GetTraderDetails(TradaoList traders)
|
||||
{
|
||||
var result = new List<Trader>();
|
||||
foreach (var trader in traders.row)
|
||||
{
|
||||
var response = await _httpClient.GetFromJsonAsync<TradaoUserDetails>($"https://api.tradao.xyz/v1/td/trader/42161/gmx/insights/{trader.user}");
|
||||
|
||||
if (response != null)
|
||||
result.Add(Map(response, trader.user));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Trader Map(TradaoUserDetails response, string address)
|
||||
{
|
||||
return new Trader
|
||||
{
|
||||
Address = address,
|
||||
Winrate = (int)(response.summary.winRate * 100),
|
||||
Pnl = Convert.ToDecimal(response.summary.pnl),
|
||||
TradeCount = response.summary.trades,
|
||||
AverageWin = Convert.ToDecimal(response.summary.averageWin),
|
||||
AverageLoss = Convert.ToDecimal(response.summary.averageLoss),
|
||||
Roi = Convert.ToDecimal(response.summary.roi)
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user