FVG Structure Liquidity Template_3 | Foreign Exchange EA Download - MT4/MT5 Resources | MT5 EA Download - MetaTrader 5 Resources
//+------------------------------------------------------------------+ //| FVGStructureLiquidityEA.mq4 | //| Paper-trading EA that evaluates FVG + structure + liquidity | //| sweeps with optional session filtering. | //| The EA never sends real orders; it only simulates fills, issues | //| alerts, and tracks virtual performance metrics. | //+------------------------------------------------------------------+ #property strict input int SwingDepth = 3; int LiquidityLookbackBars = 20; // bars to look back for liquidity sweep confirmation input double FVGMinPips = 3.0; // minimum gap size (pips) input int MaxActiveFVG = 15; // max simultaneous FVG slots tracked input int FVGExpiryMinutes = 90; // gap expiry window input double BufferPips = 2.0; // stop buffer beyond FVG boundary input double RiskPercent = 1.0; // virtual risk per trade (%) input double PartialTPRatio = 1.0; // R-multiple for partial TP input double FinalTPRatio = 2.5; // R-multiple for final TP input bool EnablePartialScaleOut = true; // move stop to break-even after partial TP enum ENUM_SESSION_MODE { SESSION_ALL = 0, SESSION_LONDON, SESSION_NEWYORK, SESSION_CUSTOM }; input ENUM_SESSION_MODE SessionMode = SESSION_CUSTOM; input string CustomSession1 = "08:00-11:30"; // HH:MM-HH:MM input string CustomSession2 = "13:30-16:30"; // optional second window input bool EnableCsvLogging = false; //--- constants #define MAX_SWING_POINTS 50 #define MAX_LIQUIDITY_EVENTS 40 #define MAX_VIRTUAL_ORDERS 20 #define MAX_FVG_SLOTS 30 struct SwingPoint { datetime time; double price; int type; // 1 = high, -1 = low }; struct FVGSlot { datetime time; datetime expiry; double upper; double lower; int direction; // 1 = bullish, -1 = bearish bool active; double score; }; struct LiquidityEvent { datetime time; double level; int direction; // 1 = swept downside liquidity (long bias), -1 = swept upside (short bias) }; struct VirtualOrder { bool active; datetime openTime; double entryPrice; double stopLoss; double tpPartial; double tpFinal; int direction; // 1 buy, -1 sell double lots; double riskPips; bool partialTaken; double closedPnL; }; //--- globals SwingPoint g_swings[MAX_SWING_POINTS]; int g_swingCount = 0; FVGSlot g_fvgs[MAX_FVG_SLOTS]; int g_fvgCount = 0; LiquidityEvent g_liquidityEvents[MAX_LIQUIDITY_EVENTS]; int g_liquidityCount = 0; VirtualOrder g_orders[MAX_VIRTUAL_ORDERS]; int g_orderCount = 0; double g_equityCurve = 100000.0; // virtual equity baseline int g_totalTrades = 0; int g_winningTrades = 0; int g_losingTrades = 0; double g_totalPnL = 0.0; double g_maxDrawdown = 0.0; datetime g_lastProcessedBar = 0; // session storage (minutes from 00:00) int g_sessionStarts[4]; int g_sessionEnds[4]; int g_sessionWindows = 0; int g_logHandle = INVALID_HANDLE; int g_maxActiveFvgSlots = 0; //--- function declarations int OnInit(); void OnDeinit(const int reason); void OnTick(); bool ProcessNewBar(int shift); bool IsSessionAllowed(datetime t); bool IsSwingHigh(int shift); bool IsSwingLow(int shift); void PushSwingPoint(datetime t, double price, int type); int DetermineStructureBias(); void CheckLiquiditySweep(int shift); bool LiquidityRecentlyConfirmed(int direction, int lookbackMinutes); void DetectFVG(int shift); void UpdateFVGStates(int shift); void EvaluateEntries(int shift, int structureBias); bool PriceTouchesFVG(const FVGSlot &slot, int shift); void TriggerVirtualOrder(const FVGSlot &slot, int direction, datetime t); void UpdateVirtualOrders(); void CloseVirtualOrder(int index, const string reason, double exitPrice, datetime t); double ComputeVirtualLots(double stopPips); double PipValuePerLot(); double GetPointPips(); void AppendLiquidityEvent(datetime t, double level, int direction); void ParseSessions(); bool ParseWindow(const string window, int &startMinutes, int &endMinutes); int TimeToMinutes(datetime t); void RenderStatusPanel(); void ResetObjects(); void LogEvent(const string &row); string TrimString(string text); //+------------------------------------------------------------------+ int OnInit() { ParseSessions(); ResetObjects(); g_maxActiveFvgSlots = MathMax(1, MathMin(MaxActiveFVG, MAX_FVG_SLOTS)); if(EnableCsvLogging) { string filePath = "FVG_PaperTradeLog.csv"; g_logHandle = FileOpen(filePath, FILE_CSV|FILE_WRITE|FILE_READ|FILE_SHARE_READ, ';'); if(g_logHandle != INVALID_HANDLE && FileSize(g_logHandle) == 0) { FileWrite(g_logHandle, "Timestamp", "Event", "Direction", "Entry", "Stop", "TP1", "TP2", "Lots", "PnL", "Equity"); } } return(INIT_SUCCEEDED); } //+--------------------------------------------------------------------------------+ void OnDeinit(const int reason) { ResetObjects(); if(g_logHandle != INVALID_HANDLE) { FileClose(g_logHandle); g_logHandle = INVALID_HANDLE; } } //+------------------------------------------------------------------+ void OnTick() { datetime currentBarTime = iTime(_Symbol, _Period, 0); if(currentBarTime == g_lastProcessedBar) { UpdateVirtualOrders(); return; } if(ProcessNewBar(1)) g_lastProcessedBar = currentBarTime; UpdateVirtualOrders(); RenderStatusPanel(); } //+----------------------------------------------------------------------------------+ bool ProcessNewBar(int shift) { if(shift < SwingDepth + 2 || Bars(_Symbol, _Period) <= shift + SwingDepth + 2) return false; datetime barTime = iTime(_Symbol, _Period, shift); bool sessionAllowed = IsSessionAllowed(barTime); if(IsSwingHigh(shift)) PushSwingPoint(barTime, iHigh(_Symbol, _Period, shift), 1); if(IsSwingLow(shift)) PushSwingPoint(barTime, iLow(_Symbol, _Period, shift), -1); int structureBias = DetermineStructureBias(); CheckLiquiditySweep(shift); DetectFVG(shift); UpdateFVGStates(shift); if(structureBias != 0 && sessionAllowed) EvaluateEntries(shift, structureBias); return true; } //+--------------------------------------------------------------------------------+ bool IsSessionAllowed(datetime t) { if(SessionMode == SESSION_ALL) return true; int minutes = TimeToMinutes(t); for(int i = 0; i < g_sessionWindows; ++i) { if(minutes >= g_sessionStarts[i] && minutes <= g_sessionEnds[i]) return true; } return false; } //+--------------------------------------------------------------------------------+ bool IsSwingHigh(int shift) { if(shift - SwingDepth < 0) return false; double target = iHigh(_Symbol, _Period, shift); for(int i = 1; i <= SwingDepth; ++i) { if(iHigh(_Symbol, _Period, shift + i) >= target) return false; if(iHigh(_Symbol, _Period, shift - i) >= target) return false; } return true; } //+------------------------------------------------------------------+ bool IsSwingLow(int shift) { if(shift - SwingDepth < 0) return false; double target = iLow(_Symbol, _Period, shift); for(int i = 1; i <= SwingDepth; ++i) { if(iLow(_Symbol, _Period, shift + i) <= target) return false; if(iLow(_Symbol, _Period, shift - i) <= target) return false; } return true; } //+----------------------------------------------------------------------------------+ void PushSwingPoint(datetime t, double price, int type) { SwingPoint point; point.time = t; point.price = price; point.type = type; if(g_swingCount < MAX_SWING_POINTS) { g_swings[g_swingCount++] = point; } else { for(int i = 1; i < MAX_SWING_POINTS; ++i) g_swings[i - 1] = g_swings[i]; g_swings[MAX_SWING_POINTS - 1] = point; g_swingCount = MAX_SWING_POINTS; } } //+--------------------------------------------------------------------------------+ int DetermineStructureBias() { double lastHigh = -1, prevHigh = -1; double lastLow = -1, prevLow = -1; for(int i = g_swingCount - 1; i >= 0; --i) { if(g_swings[i].type == 1){ if(lastHigh < 0) lastHigh = g_swings[i].price; else { prevHigh = g_swings[i].price; break; } } } for(int j = g_swingCount - 1; j >= 0; --j) { if(g_swings[j].type == -1) { if(lastLow < 0) lastLow = g_swings[j].price; else { prevLow = g_swings[j].price; break; } } } if(lastHigh < 0 || prevHigh < 0 || lastLow < 0 || prevLow < 0) return 0; if(lastHigh > prevHigh && lastLow > prevLow) return 1; if(lastHigh < prevHigh && lastLow < prevLow) return -1; return 0; } //+------------------------------------------------------------------+ void CheckLiquiditySweep(int shift) { if(g_swingCount < 2) return; SwingPoint lastSwing = g_swings[g_swingCount - 1]; double close = iClose(_Symbol, _Period, shift); double high = iHigh(_Symbol, _Period, shift); double low = iLow(_Symbol, _Period, shift); if(lastSwing.type == 1) { // last swing is high -> look for sweep above if(high > lastSwing.price && close < lastSwing.price) AppendLiquidityEvent(iTime(_Symbol, _Period, shift), lastSwing.price, -1); } else if(lastSwing.type == -1) { if(low < lastSwing.price && close > lastSwing.price) AppendLiquidityEvent(iTime(_Symbol, _Period, shift), lastSwing.price, 1); } } //+------------------------------------------------------------------+ void AppendLiquidityEvent(datetime t, double level, int direction) { LiquidityEvent evt; evt.time = t; evt.level = level; evt.direction = direction; if(g_liquidityCount < MAX_LIQUIDITY_EVENTS) { g_liquidityEvents[g_liquidityCount++] = evt; } else { for(int i = 1; i < MAX_LIQUIDITY_EVENTS; ++i) g_liquidityEvents[i - 1] = g_liquidityEvents[i]; g_liquidityEvents[MAX_LIQUIDITY_EVENTS - 1] = evt; g_liquidityCount = MAX_LIQUIDITY_EVENTS; } } //+-----------------------------------------------------------------+ bool LiquidityRecentlyConfirmed(int direction, int lookbackMinutes) { datetime nowTime = TimeCurrent(); datetime earliest = nowTime - lookbackMinutes 60; for(int i = g_liquidityCount - 1; i >= 0; --i) { if(g_liquidityEvents[i].direction != direction) continue; if(g_liquidityEvents[i].time >= earliest) return true; if(g_liquidityEvents[i].time < earliest) break; } return false; } //+--------------------------------------------------------------------------------+ void DetectFVG(int shift) { double pip = GetPointPips(); double minGap = FVGMinPips pip; if(shift + 2 >= Bars(_Symbol, _Period)) return; double highPrev1 = iHigh(_Symbol, _Period, shift + 1); double highPrev2 = iHigh(_Symbol, _Period, shift + 2); double lowPrev1 = iLow(_Symbol, _Period, shift + 1); double lowPrev2 = iLow(_Symbol, _Period, shift + 2); double lowCurr = iLow(_Symbol, _Period, shift); double highCurr = iHigh(_Symbol, _Period, shift); bool bullishGap = (lowCurr - highPrev1) > minGap && (lowCurr - highPrev2) > minGap; bool bearishGap = (lowPrev1 - highCurr) > minGap && (lowPrev2 - highCurr) > minGap; datetime barTime = iTime(_Symbol, _Period, shift); if(bullishGap) { FVGSlot slot; slot.time = barTime; slot.expiry = barTime + FVGExpiryMinutes 60; slot.lower = highPrev1; slot.upper = lowCurr; slot.direction = 1; slot.active = true; slot.score = 1.0; if(g_fvgCount < g_maxActiveFvgSlots) { g_fvgs[g_fvgCount++] = slot; } else { for(int i = 1; i < g_maxActiveFvgSlots; ++i) g_fvgs[i - 1] = g_fvgs[i]; g_fvgs[g_maxActiveFvgSlots - 1] = slot; g_fvgCount = g_maxActiveFvgSlots; } } else if(bearishGap) { FVGSlot slot; slot.time = barTime; slot.expiry = barTime + FVGExpiryMinutes 60; slot.upper = lowPrev1; slot.lower = highCurr; slot.direction = -1; slot.active = true; slot.score = 1.0; if(g_fvgCount < g_maxActiveFvgSlots) { g_fvgs[g_fvgCount++] = slot; } else { for(int j = 1; j < g_maxActiveFvgSlots; ++j) g_fvgs[j - 1] = g_fvgs[j]; g_fvgs[g_maxActiveFvgSlots - 1] = slot; g_fvgCount = g_maxActiveFvgSlots; } } } //+--------------------------------------------------------------------------------+ void UpdateFVGStates(int shift) { double high = iHigh(_Symbol, _Period, shift); double low = iLow(_Symbol, _Period, shift); datetime barTime = iTime(_Symbol, _Period, shift); for(int i = 0; i < g_fvgCount; ++i) { if(!g_fvgs[i].active) continue; if(barTime >= g_fvgs[i].expiry) { g_fvgs[i].active = false; continue; } if(g_fvgs[i].direction == 1 && low <= g_fvgs[i].lower) g_fvgs[i].active = false; if(g_fvgs[i].direction == -1 && high >= g_fvgs[i].upper) g_fvgs[i].active = false; } } //+--------------------------------------------------------------------------------+ void EvaluateEntries(int shift, int structureBias) { for(int i = 0; i < g_fvgCount; ++i) { if(!g_fvgs[i].active) continue; if(g_fvgs[i].direction != structureBias) continue; int direction = g_fvgs[i].direction; double periodSeconds = (double)PeriodSeconds(_Period); if(periodSeconds <= 0) periodSeconds = 60.0; int lookbackMinutes = (int)MathMax(1.0, LiquidityLookbackBars periodSeconds / 60.0); if(!LiquidityRecentlyConfirmed(direction, lookbackMinutes)) continue; if(PriceTouchesFVG(g_fvgs[i], shift)) { TriggerVirtualOrder(g_fvgs[i], direction, iTime(_Symbol, _Period, shift)); g_fvgs[i].active = false; } } } //+---------------------------------------------------------------------------------+ bool PriceTouchesFVG(const FVGSlot &slot, int shift) { double high = iHigh(_Symbol, _Period, shift); double low = iLow(_Symbol, _Period, shift); if(slot.direction == 1) return (low <= slot.upper && high >= slot.lower); else return (high >= slot.lower && low <= slot.upper); } //+--------------------------------------------------------------------------------+ void TriggerVirtualOrder(const FVGSlot &slot, int direction, datetime t) { double entryPrice = (slot.lower + slot.upper) 0.5; double stopBuffer = BufferPips GetPointPips(); double stopLoss; if(direction == 1) stopLoss = slot.lower - stopBuffer; else stopLoss = slot.upper + stopBuffer; double riskPips = MathAbs(entryPrice - stopLoss) / GetPointPips(); if(riskPips <= 0.0) return; double lots = ComputeVirtualLots(riskPips); if(lots <= 0.0) return; if(g_orderCount >= MAX_VIRTUAL_ORDERS) { for(int i = 1; i < MAX_VIRTUAL_ORDERS; ++i) g_orders[i - 1] = g_orders[i]; g_orderCount = MAX_VIRTUAL_ORDERS - 1; } VirtualOrder order; order.active = true; order.openTime = t; order.entryPrice = entryPrice; order.stopLoss = stopLoss; order.riskPips = riskPips; order.direction = direction; order.lots = lots; order.partialTaken = false; order.closedPnL = 0.0; double riskPrice = riskPips GetPointPips(); if(direction == 1) { order.tpPartial = entryPrice + riskPrice PartialTPRatio; order.tpFinal = entryPrice + riskPrice FinalTPRatio; } else { order.tpPartial = entryPrice - riskPrice PartialTPRatio; order.tpFinal = entryPrice - riskPrice FinalTPRatio; } g_orders[g_orderCount++] = order; ++g_totalTrades; string msg = StringFormat("Paper trade opened (%s) at %.5f, stop %.5f, TP1 %.5f, TP2 %.5f, lots %.2f", direction > 0 ? "LONG" : "SHORT", order.entryPrice, order.stopLoss, order.tpPartial, order.tpFinal, order.lots); Alert(msg); Print(msg); string logEvent(logRow); } //+-----------------------------------------------------------------+ void UpdateVirtualOrders() { if(g_orderCount == 0) return; double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); datetime nowTime = TimeCurrent(); for(int i = 0; i < g_orderCount; ++i) { if(!g_orders[i].active) continue; double price = (g_orders[i].direction > 0) ? bid : ask; // check stop loss if((g_orders[i].direction > 0 && price <= g_orders[i].stopLoss) || (g_orders[i].direction < 0 && price >= g_orders[i].stopLoss)) { double pnl = -g_orders[i].riskPips PipValuePerLot() g_orders[i].lots; CloseVirtualOrder(i, "STOP", g_orders[i].stopLoss, nowTime); g_equityCurve += pnl; g_totalPnL += pnl; ++g_losingTrades; g_maxDrawdown = MathMin(g_maxDrawdown, g_totalPnL); continue; } // partial TP if(EnablePartialScaleOut && !g_orders[i].partialTaken) { if((g_orders[i].direction > 0 && price >= g_orders[i].tpPartial) || (g_orders[i].direction < 0 && price <= g_orders[i].tpPartial)) { g_orders[i].partialTaken = true; g_orders[i].stopLoss = g_orders[i].entryPrice; double pnl = g_orders[i].riskPips PipValuePerLot() g_orders[i].lots PartialTPRatio; double partialReward = pnl 0.5; g_equityCurve += partialReward; g_totalPnL += partialReward; string partialRow = StringFormat("%s;PARTIAL;%d;%.5f;%.5f;%.5f;%.5f;%.2f;%.2f;%.2f", TimeToString(nowTime, TIME_DATE|TIME_SECONDS), g_orders[i].direction, g_orders[i].entryPrice, g_orders[i].stopLoss, g_orders[i].tpPartial, g_orders[i].tpFinal, g_orders[i].lots, partialReward, g_equityCurve); LogEvent(partialRow); } } // final TP if((g_orders[i].direction > 0 && price >= g_orders[i].tpFinal) || (g_orders[i].direction < 0 && price <= g_orders[i].tpFinal)) { double reward = g_orders[i].riskPips PipValuePerLot() g_orders[i].lots FinalTPRatio; CloseVirtualOrder(i, "TARGET", g_orders[i].tpFinal, nowTime); g_equityCurve += reward; g_totalPnL += reward; ++g_winningTrades; } } } //+-----------------------------------------------------------------+ void CloseVirtualOrder(int index, const string reason, double exitPrice, datetime t) { if(index < 0 || index >= g_orderCount) return; g_orders[index].active = false; string msg = StringFormat("Paper trade closed (%s) at %.5f due to %s", g_orders[index].direction > 0 ? "LONG" : "SHORT", exitPrice, reason); Alert(msg); Print(msg); string logRow = StringFormat("%s;%s;%d;%.5f;%.5f;%.5f;%.5f;%.2f;%.2f;%.2f", TimeToString(t, TIME_DATE|TIME_SECONDS), reason, g_orders[index].direction, g_orders[index].entryPrice, g_orders[index].stopLoss, g_orders[index].tpPartial, g_orders[index].tpFinal, g_orders[index].lots, g_totalPnL, g_equityCurve); LogEvent(logRow); } //+--------------------------------------------------------------------------------+ double ComputeVirtualLots(double stopPips) { double accountEquity = AccountEquity(); if(accountEquity <= 0) accountEquity = g_equityCurve; double riskAmount = accountEquity RiskPercent / 100.0; double pipValue = PipValuePerLot(); if(stopPips <= 0 || pipValue <= 0) return 0.0; double lots = riskAmount / (stopPips pipValue); lots = MathMax(NormalizeDouble(lots, 2), MarketInfo(_Symbol, MODE_MINLOT)); lots = MathMin(lots, MarketInfo(_Symbol, MODE_MAXLOT)); return lots; } //+--------------------------------------------------------------------------------+ double PipValuePerLot() { double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); if(tickSize == 0.0) return 0.0; double pipSize = GetPointPips(); return tickValue (pipSize / tickSize); } //+--------------------------------------------------------------------------------+ double GetPointPips() { int digits = (int)MarketInfo(_Symbol, MODE_DIGITS); if(digits == 3 || digits == 5) return 10 _Point; return _Point; } //+--------------------------------------------------------------------------------+ void ParseSessions() { g_sessionWindows = 0; switch(SessionMode) { case SESSION_LONDON: g_sessionStarts[0] = 8 60; g_sessionEnds[0] = 11 60 + 30; g_sessionWindows = 1; break; case SESSION_NEWYORK: g_sessionStarts[0] = 13 60 + 30; g_sessionEnds[0] = 16 60 + 30; g_sessionWindows = 1; break; case SESSION_CUSTOM: { int startMinutes, endMinutes; if(ParseWindow(CustomSession1, startMinutes, endMinutes)) { g_sessionStarts[g_sessionWindows] = startMinutes; g_sessionEnds[g_sessionWindows] = endMinutes; ++g_sessionWindows; } if(ParseWindow(CustomSession2, startMinutes, endMinutes) && g_sessionWindows < 4) { g_sessionStarts[g_sessionWindows] = startMinutes; g_sessionEnds[g_sessionWindows] = endMinutes; ++g_sessionWindows; } break; } default: g_sessionWindows = 0; break; } } //+------------------------------------------------------------------+ bool ParseWindow(const string window, int &startMinutes, int &endMinutes) { if(StringLen(window) < 5) return false; string parts[]; int count = StringSplit(window, '-', parts); if(count != 2) return false; string startStr = TrimString(parts[0]); string endStr = TrimString(parts[1]); string startVals[]; string endVals[]; if(StringSplit(startStr, ':', startVals) != 2 || StringSplit(endStr, ':', endVals) != 2) return false; int startHour = (int)StringToInteger(startVals[0]); int startMin = (int)StringToInteger(startVals[1]); int endHour = (int)StringToInteger(endVals[0]); int endMin = (int)StringToInteger(endVals[1]); startMinutes = startHour 60 + startMin; endMinutes = endHour 60 + endMin; if(endMinutes < startMinutes) endMinutes += 24 60; return true; } //+------------------------------------------------------------------+ int TimeToMinutes(datetime t) { MqlDateTime md; TimeToStruct(t, md); return md.hour * 60 + md.min; } //+------------------------------------------------------------------+ void RenderStatusPanel() { string name = "FVG_STATUS_PANEL"; if(ObjectFind(0, name) == -1) { ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0); ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 10); ObjectSetInteger(0, name, OBJPROP_YDISTANCE, 20); } string text = StringFormat("Paper Trades: %d (W:%d / L:%d)\nEquity: %.2f | PnL: %.2f | MaxDD: %.2f", g_totalTrades, g_winningTrades, g_losingTrades, g_equityCurve, g_totalPnL, g_maxDrawdown); ObjectSetText(name, text, 10, "Consolas", clrWhite); } //+----------------------------------------------------------------------------------+ void ResetObjects() { ObjectDelete(0, "FVG_STATUS_PANEL"); } //+----------------------------------------------------------------------------------+ void LogEvent(const string &row) { if(g_logHandle == INVALID_HANDLE) return; FileSeek(g_logHandle, 0, SEEK_END); FileWriteString(g_logHandle, row); FileWrite(g_logHandle, "\n"); FileFlush(g_logHandle); } //+--------------------------------------------------------------------------------+ string TrimString(string text) {int len = StringLen(text); while(len > 0 && StringGetChar(text, len - 1) <= ' ') { text = StringSubstr(text, 0, len - 1); len = StringLen(text); } while(len > 0 && StringGetChar(text, 0) <= ' ') { text = StringSubstr(text, 1); len = StringLen(text); } return text; } //+----------------------------------------------------------------------------------+
💡 Featured Recommendations
✍️ Latest by the author
- •
- •
- •
- •
- •
- •
📌 Popular topics
- •
- •
- •
- •
- •
- •
- •
- •
🔗 You May Be Interested In
- •
- •
- •
- •
- •
- •