// Cryptocurrency — Funding Rate Arbitrage
//*
// Initial capital: 2S. Use S to buy spot (long) and S to short perpetual contracts.
// Collect funding payments from the short perpetual position.

// Trading logic:
// Cash-and-carry arbitrage: determine long/short perpetual positions based on the funding rate,
// to effectively hedge spot holdings and stay market-neutral.
// ------- Short contract: if funding rate > 0.03%, short perpetual and buy equivalent spot.
// ------------------------ Close position: when funding rate turns negative.
// ------- Long contract: if funding rate < -0.03%, long perpetual and sell equivalent spot.
// ------------------------ Close position: when funding rate turns positive.
// Only short-side logic is implemented in this example. Long-side logic is similar.


//login("admin","123456")
//loadPlugin("Backtest")
//loadPlugin("MatchingEngineSimulator")

// Step 1: Strategy Implementation
def initialize(mutable context, userParam){
	// Initialization
	print("initialize")
	context["fundingRate"] = userParam
	context["lastlastFunding"] = dict(SYMBOL,ANY)  // Store previous funding rate for close-condition checks.
}
def beforeTrading(mutable context){
	// Daily pre-market callback:
	// Use context["tradeDate"] to get the current trading day.
	print ("beforeTrading: " + context["tradeDate"])
	// Retrieve the current funding rate table and convert to a symbol-keyed dictionary.
	fundingRate = context["fundingRate"]
	d = dict(STRING,ANY)
	for (i in distinct(fundingRate.symbol))
	{
		temp = select * from fundingRate where symbol = i and date(settlementTime) = context["tradeDate"]
		d[i] = dict(temp.settlementTime,temp.lastFundingRate, true)
	}
	context["dailyLastFundingPrice"] = d	
}

def onBar(mutable context, msg, indicator = NULL){
	expireTime = timestamp()      // Order expiration time
	upperLimit = decimal128(0.0, 8)   // Take-profit price
	lowerLimit = decimal128(0.0, 8)   // Stop-loss price
	slippage = decimal128(0.0, 8)       // Slippage 
	qty = decimal128(0.01,8)      // Fixed order quantity
	dailyFundingRate = context["dailyLastFundingPrice"]
	// Iterate through multiple symbols:
	for(i in msg.keys()){
		istockSymbol = msg[i]["symbol"]  
		istock = split(istockSymbol,"_")[0]
		source = msg[i]["symbolSource"]
		closePrice = msg[i]["close"]
		// Get the current symbol’s funding rate by matching the time window.
		if(second(context["tradeTime"]) >= 16:00:00){fundingRateTime = temporalAdd(datetime(context["tradeDate"]),16,"h")}
		if(second(context["tradeTime"]) >= 08:00:00 and second(context["tradeTime"]) < 16:00:00){fundingRateTime = temporalAdd(datetime(context["tradeDate"]),8,"h")}
		if(second(context["tradeTime"]) < 08:00:00){fundingRateTime = datetime(context["tradeDate"])}
		lastFundingPrice = dailyFundingRate[istock][fundingRateTime]
		// Get current positions: spot and contract.
		spotPos = Backtest::getPosition(context["engine"], istock+"_0", "spot")
		futurePos = Backtest::getPosition(context["engine"], istock+"_2", "futures")
		// If funding rate > 0.03%, short perpetual and buy equivalent spot.
		// Max order size is 0.1. Set to 0 to fully close positions before re-opening.   		
		if(spotPos.longPosition[0] <= 0.1 and futurePos.shortPosition[0] < 0.1 and lastFundingPrice > 0.0003){
			// Open spot position
			if(istockSymbol == istock+"_0"){					
				Backtest::submitOrder(context["engine"], (istockSymbol, source, context["tradeTime"], 5, closePrice,
			            lowerLimit, upperLimit, qty,1, slippage, 1, expireTime ), "buyopen_spot", 0, "spot")	 	 
				}
			// Open futures position
			if(istockSymbol == istock+"_2"){		
				Backtest::submitOrder(context["engine"], (istockSymbol, source, context["tradeTime"], 5, closePrice,
			            lowerLimit, upperLimit, qty, 2, slippage, 1, expireTime ), "sellopen_contract", 0, "futures")		
				}
			}
			context["lastlastFunding"][istock] = lastFundingPrice
		}
		// Closing logic: funding rate changes from positive to negative.	   
	           if(spotPos.longPosition[0] > 0.1  and futurePos.shortPosition[0] > 0.1 and context["lastlastFunding"][istock] >= 0 and lastFundingPrice < 0){
	           		// Close spot position
	           		if(istockSymbol == istock+"_0"){
	           			Backtest::submitOrder(context["engine"], (istockSymbol, source, context["tradeTime"], 5, closePrice,
				           lowerLimit, upperLimit, qty,3, slippage, 1,expireTime ), "sellclose_spot", 0, "spot")  
	           			}
	           		// Close futures position
	           		if(istockSymbol == istock+"_2"){
	           			Backtest::submitOrder(context["engine"], (istockSymbol, source, context["tradeTime"], 5, closePrice,
			                  	lowerLimit, upperLimit, qty, 4, slippage, 1,expireTime ), "buyclose_contract", 0, "futures")
	           			}
	           }
}

def onOrder(mutable context,orders){
}

def onTrade(mutable context,trades){	
}

def finalize (mutable context){
 	print("finalized")
}

// Step 2: Strategy Configuration and Engine Setup
startDate = 2024.01.01
endDate = 2024.01.15  
config = dict(STRING,ANY)
config["startDate"] = startDate
config["endDate"] = endDate
config["strategyGroup"] = "cryptocurrency"  // Strategy type: cryptocurrency
cash = dict(STRING,DOUBLE)  // Initial account capital
cash["spot"] = 1000000.
cash["futures"] = 1000000.
cash["option"] = 1000000.
config["cash"] = cash
config["dataType"] = 3     // Market data type: minute-level
// Funding rate table: passed via userParam
CryptoFundingRate = loadText("/root/JIT_8848/server/data/CryptoFundingRate.csv")   // Modify file paths based on actual data locations
config["fundingRate"] = select  symbol, settlementTime, decimal128(lastFundingRate, 8) as lastFundingRate from CryptoFundingRate where  date(settlementTime)>= startDate and date(settlementTime) <= endDate  order by settlementTime
userParam = table(1:0, [`symbol,`settlementTime,`lastFundingRate], [SYMBOL,TIMESTAMP,DECIMAL128(8)])
userParam = config["fundingRate"]

// Market data: extract and transform to required structure and types
cryptoMinData = loadText("/root/JIT_8848/server/data/Crypto1minData.csv")   // Modify file paths as needed
testData  = select symbol+"_"+string(contractType) as symbol, symbolSource, tradeTime, tradingDay, decimal128(open,8) as open, decimal128(low,8) as low,
decimal128(high,8) as high,decimal128(close,8) as close,decimal128(volume,8) as volume,decimal128(amount,8) as amount,
decimal128(upLimitPrice,8) as upLimitPrice,decimal128(downLimitPrice,8) as downLimitPrice,signal,decimal128(prevClosePrice,8) as prevClosePrice,
decimal128(settlementPrice,8) as settlementPrice,decimal128(prevSettlementPrice,8) as prevSettlementPrice,contractType from cryptoMinData  where  date(tradeTime)>=startDate and date(tradeTime) <= endDate order by tradeTime


// Security reference table
securityReference = select last(contractType)  as contractType from testData group by symbol 
update securityReference set optType = 1
update securityReference set strikePrice = decimal128(0, 8)
update securityReference set contractSize = decimal128(100.,8)
update securityReference set marginRatio = decimal128(0.2,8)
update securityReference set tradeUnit = decimal128(0.2,8)
update securityReference set priceUnit = decimal128(0.,8)
update securityReference set priceTick = decimal128(0.,8)
update securityReference set takerRate = decimal128(0.,8)
update securityReference set makerRate = decimal128(0.,8)
update securityReference set deliveryCommissionMode = iif(contractType!=2,1,2)    // Fee model
update securityReference set fundingSettlementMode = iif(contractType==2,1,2)     // Settlement model
update securityReference set lastTradeTime = timestamp()   // Final settlement date

// Create a backtesting engine
strategyName = "Cryptocurrency"
try{Backtest::dropBacktestEngine(strategyName)}catch(ex){print ex}
engine = Backtest::createBacktestEngine(strategyName, config, securityReference, initialize{,userParam}, beforeTrading, onBar,, onOrder, onTrade,, finalize)
go

// Backtest completion signal
endFlag = select top 1* from testData order by tradeTime desc
update endFlag set symbol = "END"
insert into testData endFlag
// Insert market data for backtesting
timer Backtest::appendQuotationMsg(engine, testData)


// Step 4: Retrieve Backtest Results
Backtest::getTradeDetails(engine, 'spot')   // Get trade details; returns all if account type not specified
Backtest::getDailyPosition(engine, 'futures')    // Get daily positions; can be filtered by account type
Backtest::getDailyPosition(engine, 'spot')
Backtest::getAvailableCash(engine, "futures")  // Available cash
Backtest::getAvailableCash(engine, "spot")
Backtest::getDailyTotalPortfolios(engine, "futures")   // Daily portfolio metrics
Backtest::getDailyTotalPortfolios(engine, "spot")
Backtest::getReturnSummary(engine)  // Summary of backtest results