mirror of
https://github.com/FxPouya/FxMathQuantWebApp.git
synced 2026-07-29 03:07:44 +00:00
Add files via upload
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
/**
|
||||
* Backtester - Fast backtesting engine using typed arrays
|
||||
*/
|
||||
class Backtester {
|
||||
constructor(data, strategy) {
|
||||
this.strategy = strategy;
|
||||
this.dataLength = data.length;
|
||||
|
||||
// Use typed arrays for performance
|
||||
this.open = new Float64Array(data.length);
|
||||
this.high = new Float64Array(data.length);
|
||||
this.low = new Float64Array(data.length);
|
||||
this.close = new Float64Array(data.length);
|
||||
this.time = new Array(data.length);
|
||||
|
||||
// Populate arrays
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
this.open[i] = parseFloat(data[i].open) || 0;
|
||||
this.high[i] = parseFloat(data[i].high) || 0;
|
||||
this.low[i] = parseFloat(data[i].low) || 0;
|
||||
this.close[i] = parseFloat(data[i].close) || 0;
|
||||
this.time[i] = data[i].time || i;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run backtest and return metrics
|
||||
*/
|
||||
run() {
|
||||
console.log('📊 Backtester.run() - Data length:', this.dataLength);
|
||||
const atr = this.calculateATR(this.strategy.atrPeriod);
|
||||
console.log('✅ ATR calculated, period:', this.strategy.atrPeriod);
|
||||
|
||||
let balance = 10000;
|
||||
let position = null;
|
||||
const trades = [];
|
||||
const equity = [balance];
|
||||
|
||||
// Start from ATR period to have enough data
|
||||
for (let i = this.strategy.atrPeriod + 10; i < this.dataLength; i++) {
|
||||
// Check for close at opposite signal first (if enabled)
|
||||
if (position && this.strategy.closeAtOpposite) {
|
||||
if (position.type === 'BUY' && this.checkSellSignal(i)) {
|
||||
// Close BUY and open SELL
|
||||
const closePrice = this.close[i];
|
||||
const profit = closePrice - position.entry;
|
||||
balance += profit;
|
||||
trades.push({
|
||||
type: 'BUY',
|
||||
entry: position.entry,
|
||||
exit: closePrice,
|
||||
profit: profit,
|
||||
reason: 'opposite',
|
||||
openTime: position.openTime,
|
||||
closeTime: this.time[i]
|
||||
});
|
||||
equity.push(balance);
|
||||
position = this.openPosition('SELL', i, atr[i]);
|
||||
continue;
|
||||
} else if (position.type === 'SELL' && this.checkBuySignal(i)) {
|
||||
// Close SELL and open BUY
|
||||
const closePrice = this.close[i];
|
||||
const profit = position.entry - closePrice;
|
||||
balance += profit;
|
||||
trades.push({
|
||||
type: 'SELL',
|
||||
entry: position.entry,
|
||||
exit: closePrice,
|
||||
profit: profit,
|
||||
reason: 'opposite',
|
||||
openTime: position.openTime,
|
||||
closeTime: this.time[i]
|
||||
});
|
||||
equity.push(balance);
|
||||
position = this.openPosition('BUY', i, atr[i]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for normal exit (TP/SL)
|
||||
if (position) {
|
||||
const exitResult = this.checkExit(position, i);
|
||||
if (exitResult) {
|
||||
balance += exitResult.profit;
|
||||
trades.push(exitResult);
|
||||
equity.push(balance);
|
||||
position = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for entry signals (only if no position)
|
||||
if (!position) {
|
||||
if (this.checkBuySignal(i)) {
|
||||
position = this.openPosition('BUY', i, atr[i]);
|
||||
} else if (this.checkSellSignal(i)) {
|
||||
position = this.openPosition('SELL', i, atr[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close any open position at the end
|
||||
if (position) {
|
||||
const exitPrice = this.close[this.dataLength - 1];
|
||||
const profit = exitPrice - position.entry;
|
||||
balance += profit;
|
||||
trades.push({
|
||||
type: 'BUY',
|
||||
entry: position.entry,
|
||||
exit: exitPrice,
|
||||
profit: profit,
|
||||
reason: 'end_of_data'
|
||||
});
|
||||
equity.push(balance);
|
||||
}
|
||||
|
||||
return this.calculateMetrics(trades, balance, equity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if BUY signal is triggered
|
||||
*/
|
||||
checkBuySignal(index) {
|
||||
// All rules must be true
|
||||
for (const rule of this.strategy.rules) {
|
||||
if (!this.evaluateRule(rule, index)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SELL signal is triggered (symmetrical to BUY with inverted operators)
|
||||
*/
|
||||
checkSellSignal(index) {
|
||||
// All SELL rules must be true (SELL rules = BUY rules with inverted operators)
|
||||
for (const rule of this.strategy.rules) {
|
||||
if (!this.evaluateSellRule(rule, index)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a SELL rule (BUY rule with inverted operator)
|
||||
*/
|
||||
evaluateSellRule(rule, index) {
|
||||
let leftValue, rightValue;
|
||||
|
||||
if (rule.type === 'simple') {
|
||||
leftValue = this.getPriceValue(rule.left.price, index - rule.left.shift);
|
||||
rightValue = this.getPriceValue(rule.right.price, index - rule.right.shift);
|
||||
} else {
|
||||
// Arithmetic rule
|
||||
const left1 = this.getPriceValue(rule.left.price1, index - rule.left.shift1);
|
||||
const left2 = this.getPriceValue(rule.left.price2, index - rule.left.shift2);
|
||||
const rightPrice = this.getPriceValue(rule.right.price, index - rule.right.shift);
|
||||
|
||||
switch (rule.left.op) {
|
||||
case '+': leftValue = left1 + left2; break;
|
||||
case '-': leftValue = left1 - left2; break;
|
||||
case '*': leftValue = left1 * left2; break;
|
||||
default: leftValue = left1;
|
||||
}
|
||||
|
||||
rightValue = rightPrice * rule.right.multiplier;
|
||||
}
|
||||
|
||||
// Invert the operator for SELL
|
||||
switch (rule.operator) {
|
||||
case '>': return leftValue <= rightValue;
|
||||
case '<': return leftValue >= rightValue;
|
||||
case '>=': return leftValue < rightValue;
|
||||
case '<=': return leftValue > rightValue;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a single rule
|
||||
*/
|
||||
evaluateRule(rule, index) {
|
||||
let leftValue, rightValue;
|
||||
|
||||
if (rule.type === 'simple') {
|
||||
leftValue = this.getPriceValue(rule.left.price, index - rule.left.shift);
|
||||
rightValue = this.getPriceValue(rule.right.price, index - rule.right.shift);
|
||||
} else {
|
||||
// Arithmetic rule
|
||||
const left1 = this.getPriceValue(rule.left.price1, index - rule.left.shift1);
|
||||
const left2 = this.getPriceValue(rule.left.price2, index - rule.left.shift2);
|
||||
|
||||
switch (rule.left.op) {
|
||||
case '+': leftValue = left1 + left2; break;
|
||||
case '-': leftValue = left1 - left2; break;
|
||||
case '*': leftValue = left1 * left2; break;
|
||||
default: leftValue = left1;
|
||||
}
|
||||
|
||||
const rightPrice = this.getPriceValue(rule.right.price, index - rule.right.shift);
|
||||
rightValue = rightPrice * rule.right.multiplier;
|
||||
}
|
||||
|
||||
// Evaluate operator
|
||||
switch (rule.operator) {
|
||||
case '>': return leftValue > rightValue;
|
||||
case '<': return leftValue < rightValue;
|
||||
case '>=': return leftValue >= rightValue;
|
||||
case '<=': return leftValue <= rightValue;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price value at specific index
|
||||
*/
|
||||
getPriceValue(priceType, index) {
|
||||
if (index < 0 || index >= this.dataLength) return 0;
|
||||
|
||||
switch (priceType) {
|
||||
case 'open': return this.open[index];
|
||||
case 'high': return this.high[index];
|
||||
case 'low': return this.low[index];
|
||||
case 'close': return this.close[index];
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a new position
|
||||
*/
|
||||
openPosition(type, index, atr) {
|
||||
const entry = this.close[index];
|
||||
let sl, tp;
|
||||
|
||||
if (type === 'BUY') {
|
||||
sl = entry - (atr * this.strategy.slMultiplier);
|
||||
tp = entry + (atr * this.strategy.tpMultiplier);
|
||||
} else { // SELL
|
||||
sl = entry + (atr * this.strategy.slMultiplier);
|
||||
tp = entry - (atr * this.strategy.tpMultiplier);
|
||||
}
|
||||
|
||||
return {
|
||||
type: type,
|
||||
entry: entry,
|
||||
sl: sl,
|
||||
tp: tp,
|
||||
openIndex: index,
|
||||
openTime: this.time[index]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if position should be exited
|
||||
*/
|
||||
checkExit(position, index) {
|
||||
const high = this.high[index];
|
||||
const low = this.low[index];
|
||||
|
||||
if (position.type === 'BUY') {
|
||||
// BUY: TP is above entry, SL is below
|
||||
if (high >= position.tp) {
|
||||
return {
|
||||
type: position.type,
|
||||
entry: position.entry,
|
||||
exit: position.tp,
|
||||
profit: position.tp - position.entry,
|
||||
reason: 'take_profit',
|
||||
openTime: position.openTime,
|
||||
closeTime: this.time[index]
|
||||
};
|
||||
}
|
||||
|
||||
if (low <= position.sl) {
|
||||
return {
|
||||
type: position.type,
|
||||
entry: position.entry,
|
||||
exit: position.sl,
|
||||
profit: position.sl - position.entry,
|
||||
reason: 'stop_loss',
|
||||
openTime: position.openTime,
|
||||
closeTime: this.time[index]
|
||||
};
|
||||
}
|
||||
} else { // SELL
|
||||
// SELL: TP is below entry, SL is above
|
||||
if (low <= position.tp) {
|
||||
return {
|
||||
type: position.type,
|
||||
entry: position.entry,
|
||||
exit: position.tp,
|
||||
profit: position.entry - position.tp,
|
||||
reason: 'take_profit',
|
||||
openTime: position.openTime,
|
||||
closeTime: this.time[index]
|
||||
};
|
||||
}
|
||||
|
||||
if (high >= position.sl) {
|
||||
return {
|
||||
type: position.type,
|
||||
entry: position.entry,
|
||||
exit: position.sl,
|
||||
profit: position.entry - position.sl,
|
||||
reason: 'stop_loss',
|
||||
openTime: position.openTime,
|
||||
closeTime: this.time[index]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate ATR (Average True Range)
|
||||
*/
|
||||
calculateATR(period) {
|
||||
const atr = new Float64Array(this.dataLength);
|
||||
const tr = new Float64Array(this.dataLength);
|
||||
|
||||
// Calculate True Range
|
||||
for (let i = 1; i < this.dataLength; i++) {
|
||||
const hl = this.high[i] - this.low[i];
|
||||
const hc = Math.abs(this.high[i] - this.close[i - 1]);
|
||||
const lc = Math.abs(this.low[i] - this.close[i - 1]);
|
||||
tr[i] = Math.max(hl, hc, lc);
|
||||
}
|
||||
|
||||
// Calculate ATR using SMA
|
||||
for (let i = period; i < this.dataLength; i++) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
sum += tr[i - j];
|
||||
}
|
||||
atr[i] = sum / period;
|
||||
}
|
||||
|
||||
return atr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate performance metrics
|
||||
*/
|
||||
calculateMetrics(trades, finalBalance, equity) {
|
||||
if (trades.length === 0) {
|
||||
return {
|
||||
totalTrades: 0,
|
||||
buyTrades: 0,
|
||||
sellTrades: 0,
|
||||
winRate: 0,
|
||||
profitFactor: 0,
|
||||
maxDrawdown: 0,
|
||||
finalBalance: finalBalance,
|
||||
totalProfit: 0,
|
||||
avgWin: 0,
|
||||
avgLoss: 0,
|
||||
largestWin: 0,
|
||||
largestLoss: 0
|
||||
};
|
||||
}
|
||||
|
||||
const winners = trades.filter(t => t.profit > 0);
|
||||
const losers = trades.filter(t => t.profit <= 0);
|
||||
const buyTrades = trades.filter(t => t.type === 'BUY');
|
||||
const sellTrades = trades.filter(t => t.type === 'SELL');
|
||||
|
||||
const grossProfit = winners.reduce((sum, t) => sum + t.profit, 0);
|
||||
const grossLoss = Math.abs(losers.reduce((sum, t) => sum + t.profit, 0));
|
||||
|
||||
const profitFactor = grossLoss > 0 ? grossProfit / grossLoss : (grossProfit > 0 ? 10 : 0);
|
||||
const winRate = (winners.length / trades.length) * 100;
|
||||
|
||||
const maxDD = this.calculateMaxDrawdown(equity);
|
||||
|
||||
const avgWin = winners.length > 0 ? grossProfit / winners.length : 0;
|
||||
const avgLoss = losers.length > 0 ? grossLoss / losers.length : 0;
|
||||
|
||||
const largestWin = winners.length > 0 ? Math.max(...winners.map(t => t.profit)) : 0;
|
||||
const largestLoss = losers.length > 0 ? Math.min(...losers.map(t => t.profit)) : 0;
|
||||
|
||||
// Calculate hourly performance (0-23 hours)
|
||||
const hourlyStats = Array.from({ length: 24 }, () => ({
|
||||
trades: 0,
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
profit: 0
|
||||
}));
|
||||
|
||||
trades.forEach(trade => {
|
||||
if (trade.openTime) {
|
||||
const hour = new Date(trade.openTime).getHours();
|
||||
hourlyStats[hour].trades++;
|
||||
if (trade.profit > 0) {
|
||||
hourlyStats[hour].wins++;
|
||||
} else {
|
||||
hourlyStats[hour].losses++;
|
||||
}
|
||||
hourlyStats[hour].profit += trade.profit;
|
||||
}
|
||||
});
|
||||
|
||||
// Find best and worst hours
|
||||
let bestHour = { hour: 0, profit: -Infinity };
|
||||
let worstHour = { hour: 0, profit: Infinity };
|
||||
|
||||
hourlyStats.forEach((stats, hour) => {
|
||||
if (stats.trades > 0) {
|
||||
if (stats.profit > bestHour.profit) {
|
||||
bestHour = { hour, profit: stats.profit };
|
||||
}
|
||||
if (stats.profit < worstHour.profit) {
|
||||
worstHour = { hour, profit: stats.profit };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
totalTrades: trades.length,
|
||||
buyTrades: buyTrades.length,
|
||||
sellTrades: sellTrades.length,
|
||||
winningTrades: winners.length,
|
||||
losingTrades: losers.length,
|
||||
winRate: winRate,
|
||||
profitFactor: profitFactor,
|
||||
maxDrawdown: maxDD,
|
||||
finalBalance: finalBalance,
|
||||
totalProfit: finalBalance - 10000,
|
||||
grossProfit: grossProfit,
|
||||
grossLoss: grossLoss,
|
||||
avgWin: avgWin,
|
||||
avgLoss: avgLoss,
|
||||
largestWin: largestWin,
|
||||
largestLoss: largestLoss,
|
||||
hourlyStats: hourlyStats,
|
||||
bestHour: bestHour,
|
||||
worstHour: worstHour,
|
||||
equity: equity,
|
||||
trades: trades
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate maximum drawdown
|
||||
*/
|
||||
calculateMaxDrawdown(equity) {
|
||||
let maxDD = 0;
|
||||
let peak = equity[0];
|
||||
|
||||
for (let i = 1; i < equity.length; i++) {
|
||||
if (equity[i] > peak) {
|
||||
peak = equity[i];
|
||||
}
|
||||
const dd = ((peak - equity[i]) / peak) * 100;
|
||||
if (dd > maxDD) {
|
||||
maxDD = dd;
|
||||
}
|
||||
}
|
||||
|
||||
return maxDD;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
// cTrader (cBot) Converter
|
||||
class CTraderConverter {
|
||||
constructor(strategy) {
|
||||
this.strategy = strategy;
|
||||
this.parser = new RuleParser();
|
||||
}
|
||||
|
||||
generate() {
|
||||
const { parameters, buy_rules, sell_rules } = this.strategy;
|
||||
const symbol = parameters.symbol || 'EURUSD';
|
||||
const atrPeriod = parameters.atr_period || 14;
|
||||
const slMultiplier = parameters.sl_multiplier || 2.0;
|
||||
const tpMultiplier = parameters.tp_multiplier || 3.0;
|
||||
|
||||
// Generate random 6-digit magic number
|
||||
const magicNumber = Math.floor(100000 + Math.random() * 900000);
|
||||
|
||||
const buyConditions = this.parser.parseRules(buy_rules, 'csharp');
|
||||
const sellConditions = this.parser.parseRules(sell_rules, 'csharp');
|
||||
|
||||
// Convert MQL-style array access to cTrader MarketSeries
|
||||
const convertToCTrader = (condition) => {
|
||||
return condition
|
||||
.replace(/Open\[(\d+)\]/g, 'MarketSeries.Open.Last($1)')
|
||||
.replace(/High\[(\d+)\]/g, 'MarketSeries.High.Last($1)')
|
||||
.replace(/Low\[(\d+)\]/g, 'MarketSeries.Low.Last($1)')
|
||||
.replace(/Close\[(\d+)\]/g, 'MarketSeries.Close.Last($1)');
|
||||
};
|
||||
|
||||
const buyConditionsCTrader = buyConditions.map(convertToCTrader);
|
||||
const sellConditionsCTrader = sellConditions.map(convertToCTrader);
|
||||
|
||||
return `using System;
|
||||
using System.Linq;
|
||||
using cAlgo.API;
|
||||
using cAlgo.API.Indicators;
|
||||
using cAlgo.API.Internals;
|
||||
using cAlgo.Indicators;
|
||||
|
||||
namespace cAlgo.Robots
|
||||
{
|
||||
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
|
||||
public class Strategy_${symbol} : Robot
|
||||
{
|
||||
//=== TRADE SETTINGS ===
|
||||
[Parameter("Lot Size", DefaultValue = 0.1, MinValue = 0.01)]
|
||||
public double LotSize { get; set; }
|
||||
|
||||
[Parameter("ATR Period", DefaultValue = ${atrPeriod}, MinValue = 1)]
|
||||
public int ATR_Period { get; set; }
|
||||
|
||||
[Parameter("SL ATR Multiplier", DefaultValue = ${slMultiplier.toFixed(2)}, MinValue = 0.1)]
|
||||
public double SL_Multiplier { get; set; }
|
||||
|
||||
[Parameter("TP ATR Multiplier", DefaultValue = ${tpMultiplier.toFixed(2)}, MinValue = 0.1)]
|
||||
public double TP_Multiplier { get; set; }
|
||||
|
||||
[Parameter("Magic Number", DefaultValue = ${magicNumber})]
|
||||
public int MagicNumber { get; set; }
|
||||
|
||||
[Parameter("Trade Comment", DefaultValue = "FxMath EA")]
|
||||
public string TradeComment { get; set; }
|
||||
|
||||
[Parameter("Slippage", DefaultValue = 3, MinValue = 0)]
|
||||
public int Slippage { get; set; }
|
||||
|
||||
//=== SL/TP SETTINGS ===
|
||||
[Parameter("Enable Hard SL", DefaultValue = true, Group = "SL/TP")]
|
||||
public bool EnableHardSL { get; set; }
|
||||
|
||||
[Parameter("Enable Hard TP", DefaultValue = true, Group = "SL/TP")]
|
||||
public bool EnableHardTP { get; set; }
|
||||
|
||||
//=== TRAILING STOP ===
|
||||
[Parameter("Enable Trailing", DefaultValue = false, Group = "Trailing")]
|
||||
public bool EnableTrailing { get; set; }
|
||||
|
||||
[Parameter("Trailing Start (pips)", DefaultValue = 30, MinValue = 1, Group = "Trailing")]
|
||||
public double TrailingStart { get; set; }
|
||||
|
||||
[Parameter("Trailing Step (pips)", DefaultValue = 10, MinValue = 1, Group = "Trailing")]
|
||||
public double TrailingStep { get; set; }
|
||||
|
||||
//=== BREAKEVEN ===
|
||||
[Parameter("Enable Breakeven", DefaultValue = false, Group = "Breakeven")]
|
||||
public bool EnableBreakeven { get; set; }
|
||||
|
||||
[Parameter("Breakeven Start (pips)", DefaultValue = 20, MinValue = 1, Group = "Breakeven")]
|
||||
public double BreakevenStart { get; set; }
|
||||
|
||||
[Parameter("Breakeven Offset (pips)", DefaultValue = 2, MinValue = 0, Group = "Breakeven")]
|
||||
public double BreakevenOffset { get; set; }
|
||||
|
||||
//=== TIME FILTER ===
|
||||
[Parameter("Enable Time Filter", DefaultValue = false, Group = "Time Filter")]
|
||||
public bool EnableTimeFilter { get; set; }
|
||||
|
||||
[Parameter("Start Hour", DefaultValue = 8, MinValue = 0, MaxValue = 23, Group = "Time Filter")]
|
||||
public int StartHour { get; set; }
|
||||
|
||||
[Parameter("Start Minute", DefaultValue = 0, MinValue = 0, MaxValue = 59, Group = "Time Filter")]
|
||||
public int StartMinute { get; set; }
|
||||
|
||||
[Parameter("End Hour", DefaultValue = 22, MinValue = 0, MaxValue = 23, Group = "Time Filter")]
|
||||
public int EndHour { get; set; }
|
||||
|
||||
[Parameter("End Minute", DefaultValue = 0, MinValue = 0, MaxValue = 59, Group = "Time Filter")]
|
||||
public int EndMinute { get; set; }
|
||||
|
||||
//=== SIGNAL SETTINGS ===
|
||||
[Parameter("Close on Opposite", DefaultValue = true, Group = "Signals")]
|
||||
public bool CloseOnOpposite { get; set; }
|
||||
|
||||
//=== DISPLAY SETTINGS ===
|
||||
[Parameter("Show Chart Info", DefaultValue = true, Group = "Display")]
|
||||
public bool ShowChartInfo { get; set; }
|
||||
|
||||
private AverageTrueRange atr;
|
||||
private Position currentPosition;
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
atr = Indicators.AverageTrueRange(ATR_Period, MovingAverageType.Simple);
|
||||
Print("Strategy cBot initialized for ${symbol}");
|
||||
Print("Magic Number: " + MagicNumber);
|
||||
}
|
||||
|
||||
protected override void OnTick()
|
||||
{
|
||||
currentPosition = Positions.Find(TradeComment, SymbolName);
|
||||
|
||||
if (currentPosition != null)
|
||||
{
|
||||
ManagePosition();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check time filter
|
||||
if (EnableTimeFilter && !IsTimeAllowed())
|
||||
return;
|
||||
|
||||
// Check signals
|
||||
if (CheckBuySignal())
|
||||
{
|
||||
OpenBuyOrder();
|
||||
}
|
||||
else if (CheckSellSignal())
|
||||
{
|
||||
OpenSellOrder();
|
||||
}
|
||||
}
|
||||
|
||||
if (ShowChartInfo)
|
||||
DisplayChartInfo();
|
||||
}
|
||||
|
||||
private bool IsTimeAllowed()
|
||||
{
|
||||
var currentTime = Server.Time;
|
||||
int currentMinutes = currentTime.Hour * 60 + currentTime.Minute;
|
||||
int startMinutes = StartHour * 60 + StartMinute;
|
||||
int endMinutes = EndHour * 60 + EndMinute;
|
||||
|
||||
if (startMinutes < endMinutes)
|
||||
return currentMinutes >= startMinutes && currentMinutes < endMinutes;
|
||||
else
|
||||
return currentMinutes >= startMinutes || currentMinutes < endMinutes;
|
||||
}
|
||||
|
||||
private void ManagePosition()
|
||||
{
|
||||
if (currentPosition == null)
|
||||
return;
|
||||
|
||||
// Check for opposite signal
|
||||
if (CloseOnOpposite)
|
||||
{
|
||||
if (currentPosition.TradeType == TradeType.Buy && CheckSellSignal())
|
||||
{
|
||||
ClosePosition(currentPosition);
|
||||
return;
|
||||
}
|
||||
else if (currentPosition.TradeType == TradeType.Sell && CheckBuySignal())
|
||||
{
|
||||
ClosePosition(currentPosition);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
double currentPrice = currentPosition.TradeType == TradeType.Buy ? Symbol.Bid : Symbol.Ask;
|
||||
double pipValue = Symbol.PipSize;
|
||||
|
||||
// Breakeven
|
||||
if (EnableBreakeven)
|
||||
{
|
||||
double profit = currentPosition.TradeType == TradeType.Buy ?
|
||||
(currentPrice - currentPosition.EntryPrice) / pipValue :
|
||||
(currentPosition.EntryPrice - currentPrice) / pipValue;
|
||||
|
||||
if (profit >= BreakevenStart)
|
||||
{
|
||||
double newSL = currentPosition.EntryPrice + (BreakevenOffset * pipValue *
|
||||
(currentPosition.TradeType == TradeType.Buy ? 1 : -1));
|
||||
|
||||
if ((currentPosition.TradeType == TradeType.Buy && newSL > currentPosition.StopLoss) ||
|
||||
(currentPosition.TradeType == TradeType.Sell && (currentPosition.StopLoss == null || newSL < currentPosition.StopLoss)))
|
||||
{
|
||||
ModifyPosition(currentPosition, newSL, currentPosition.TakeProfit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trailing Stop
|
||||
if (EnableTrailing)
|
||||
{
|
||||
double profit = currentPosition.TradeType == TradeType.Buy ?
|
||||
(currentPrice - currentPosition.EntryPrice) / pipValue :
|
||||
(currentPosition.EntryPrice - currentPrice) / pipValue;
|
||||
|
||||
if (profit >= TrailingStart)
|
||||
{
|
||||
double newSL = currentPrice - (TrailingStep * pipValue *
|
||||
(currentPosition.TradeType == TradeType.Buy ? 1 : -1));
|
||||
|
||||
if ((currentPosition.TradeType == TradeType.Buy && newSL > currentPosition.StopLoss) ||
|
||||
(currentPosition.TradeType == TradeType.Sell && (currentPosition.StopLoss == null || newSL < currentPosition.StopLoss)))
|
||||
{
|
||||
ModifyPosition(currentPosition, newSL, currentPosition.TakeProfit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisplayChartInfo()
|
||||
{
|
||||
string info = "\\n=== " + TradeComment + " ===\\n";
|
||||
info += "Symbol: " + SymbolName + "\\n";
|
||||
info += "Magic: " + MagicNumber + "\\n";
|
||||
|
||||
if (currentPosition != null)
|
||||
{
|
||||
info += "Position: " + currentPosition.TradeType + "\\n";
|
||||
info += "Profit: " + currentPosition.NetProfit.ToString("F2") + "\\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
info += "No Position\\n";
|
||||
}
|
||||
|
||||
Chart.DrawStaticText("info", info, VerticalAlignment.Top, HorizontalAlignment.Left, Color.White);
|
||||
}
|
||||
|
||||
private bool CheckBuySignal()
|
||||
{
|
||||
return ${buyConditionsCTrader.join(' &&\n ')};
|
||||
}
|
||||
|
||||
private bool CheckSellSignal()
|
||||
{
|
||||
return ${sellConditionsCTrader.join(' &&\n ')};
|
||||
}
|
||||
|
||||
private void OpenBuyOrder()
|
||||
{
|
||||
double atrValue = atr.Result.LastValue;
|
||||
double price = Symbol.Ask;
|
||||
double sl = EnableHardSL ? price - (atrValue * SL_Multiplier) : 0;
|
||||
double tp = EnableHardTP ? price + (atrValue * TP_Multiplier) : 0;
|
||||
|
||||
double volumeInUnits = Symbol.QuantityToVolumeInUnits(LotSize);
|
||||
|
||||
var result = ExecuteMarketOrder(TradeType.Buy, SymbolName, volumeInUnits, TradeComment,
|
||||
EnableHardSL ? sl : (double?)null,
|
||||
EnableHardTP ? tp : (double?)null);
|
||||
|
||||
if (result.IsSuccessful)
|
||||
{
|
||||
Print("Buy order opened at " + price + " SL: " + sl + " TP: " + tp);
|
||||
}
|
||||
else
|
||||
{
|
||||
Print("Error opening buy order: " + result.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenSellOrder()
|
||||
{
|
||||
double atrValue = atr.Result.LastValue;
|
||||
double price = Symbol.Bid;
|
||||
double sl = EnableHardSL ? price + (atrValue * SL_Multiplier) : 0;
|
||||
double tp = EnableHardTP ? price - (atrValue * TP_Multiplier) : 0;
|
||||
|
||||
double volumeInUnits = Symbol.QuantityToVolumeInUnits(LotSize);
|
||||
|
||||
var result = ExecuteMarketOrder(TradeType.Sell, SymbolName, volumeInUnits, TradeComment,
|
||||
EnableHardSL ? sl : (double?)null,
|
||||
EnableHardTP ? tp : (double?)null);
|
||||
|
||||
if (result.IsSuccessful)
|
||||
{
|
||||
Print("Sell order opened at " + price + " SL: " + sl + " TP: " + tp);
|
||||
}
|
||||
else
|
||||
{
|
||||
Print("Error opening sell order: " + result.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
window.CTraderConverter = CTraderConverter;
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* Genetic Algorithm Engine - Evolves trading strategies
|
||||
*/
|
||||
class GeneticOptimizer {
|
||||
constructor(data, config) {
|
||||
this.data = data;
|
||||
this.config = {
|
||||
populationSize: config.populationSize || 100,
|
||||
generations: config.generations || 50,
|
||||
strategiesCount: config.strategiesCount || 5,
|
||||
rulesRange: config.rulesRange || [3, 8],
|
||||
shiftRange: config.shiftRange || [1, 10],
|
||||
minTrades: config.minTrades || 30,
|
||||
minPF: config.minPF || 1.5,
|
||||
maxDD: config.maxDD || 25,
|
||||
minWR: config.minWR || 45
|
||||
};
|
||||
|
||||
this.population = [];
|
||||
this.foundStrategies = [];
|
||||
this.isRunning = false;
|
||||
this.currentGeneration = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize random population
|
||||
*/
|
||||
initializePopulation() {
|
||||
this.population = [];
|
||||
for (let i = 0; i < this.config.populationSize; i++) {
|
||||
const strategy = new Strategy();
|
||||
strategy.generateRandomRules(this.config.rulesRange, this.config.shiftRange);
|
||||
strategy.randomizeParameters();
|
||||
this.population.push(strategy);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate fitness for all strategies
|
||||
*/
|
||||
evaluatePopulation() {
|
||||
for (const strategy of this.population) {
|
||||
const backtester = new Backtester(this.data, strategy);
|
||||
strategy.metrics = backtester.run();
|
||||
strategy.fitness = this.calculateFitness(strategy.metrics);
|
||||
}
|
||||
|
||||
// Sort by fitness (descending)
|
||||
this.population.sort((a, b) => b.fitness - a.fitness);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate fitness score
|
||||
*/
|
||||
calculateFitness(metrics) {
|
||||
if (metrics.totalTrades === 0) return 0;
|
||||
|
||||
const pf = Math.min(metrics.profitFactor, 10);
|
||||
const trades = Math.sqrt(metrics.totalTrades);
|
||||
const wrBonus = metrics.winRate > 50 ? 1 + (metrics.winRate - 50) / 100 : 1;
|
||||
const ddPenalty = 1 + (metrics.maxDrawdown / 100);
|
||||
|
||||
// Calculate BUY/SELL balance bonus
|
||||
const buyRatio = (metrics.buyTrades / metrics.totalTrades) * 100;
|
||||
const sellRatio = (metrics.sellTrades / metrics.totalTrades) * 100;
|
||||
|
||||
// Reward strategies with 30-70% split (most balanced)
|
||||
// Penalize strategies with <20% or >80% of one type
|
||||
let balanceBonus = 1.0;
|
||||
if (buyRatio >= 30 && buyRatio <= 70) {
|
||||
balanceBonus = 1.2; // 20% bonus for good balance
|
||||
} else if (buyRatio >= 20 && buyRatio <= 80) {
|
||||
balanceBonus = 1.1; // 10% bonus for acceptable balance
|
||||
} else {
|
||||
balanceBonus = 0.5; // 50% penalty for poor balance
|
||||
}
|
||||
|
||||
return (Math.pow(pf, 1.5) * trades * wrBonus * balanceBonus) / ddPenalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if strategy meets criteria
|
||||
*/
|
||||
meetsCriteria(strategy) {
|
||||
const m = strategy.metrics;
|
||||
|
||||
// Check basic criteria
|
||||
const meetsBasic = (
|
||||
m.totalTrades >= this.config.minTrades &&
|
||||
m.profitFactor >= this.config.minPF &&
|
||||
m.maxDrawdown <= this.config.maxDD &&
|
||||
m.winRate >= this.config.minWR
|
||||
);
|
||||
|
||||
if (!meetsBasic) return false;
|
||||
|
||||
// Check BUY/SELL balance (should be between 20-80%)
|
||||
const buyRatio = (m.buyTrades / m.totalTrades) * 100;
|
||||
const sellRatio = (m.sellTrades / m.totalTrades) * 100;
|
||||
|
||||
// Require at least 20% of each type for balance
|
||||
const isBalanced = buyRatio >= 20 && sellRatio >= 20;
|
||||
|
||||
console.log(`📊 Strategy balance: BUY=${buyRatio.toFixed(1)}%, SELL=${sellRatio.toFixed(1)}%, Balanced=${isBalanced}`);
|
||||
|
||||
return isBalanced;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tournament selection
|
||||
*/
|
||||
tournamentSelection() {
|
||||
const tournamentSize = 3;
|
||||
let best = null;
|
||||
|
||||
for (let i = 0; i < tournamentSize; i++) {
|
||||
const candidate = this.population[Math.floor(Math.random() * this.population.length)];
|
||||
if (!best || candidate.fitness > best.fitness) {
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return best.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crossover two parent strategies
|
||||
*/
|
||||
crossover(parent1, parent2) {
|
||||
const child = new Strategy();
|
||||
|
||||
// Crossover rules
|
||||
const splitPoint = Math.floor(Math.random() * Math.min(parent1.rules.length, parent2.rules.length));
|
||||
child.rules = [
|
||||
...parent1.rules.slice(0, splitPoint),
|
||||
...parent2.rules.slice(splitPoint)
|
||||
];
|
||||
|
||||
// Ensure rules count is within range
|
||||
while (child.rules.length < this.config.rulesRange[0]) {
|
||||
child.rules.push(child.generateRule(this.config.shiftRange));
|
||||
}
|
||||
while (child.rules.length > this.config.rulesRange[1]) {
|
||||
child.rules.splice(Math.floor(Math.random() * child.rules.length), 1);
|
||||
}
|
||||
|
||||
// Crossover parameters
|
||||
child.atrPeriod = Math.random() < 0.5 ? parent1.atrPeriod : parent2.atrPeriod;
|
||||
child.slMultiplier = Math.random() < 0.5 ? parent1.slMultiplier : parent2.slMultiplier;
|
||||
child.tpMultiplier = Math.random() < 0.5 ? parent1.tpMultiplier : parent2.tpMultiplier;
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutate a strategy
|
||||
*/
|
||||
mutate(strategy) {
|
||||
const mutationRate = 0.2;
|
||||
|
||||
if (Math.random() < mutationRate) {
|
||||
const mutationType = Math.random();
|
||||
|
||||
if (mutationType < 0.4) {
|
||||
// Mutate a rule
|
||||
if (strategy.rules.length > 0) {
|
||||
const index = Math.floor(Math.random() * strategy.rules.length);
|
||||
strategy.rules[index] = strategy.generateRule(this.config.shiftRange);
|
||||
}
|
||||
} else if (mutationType < 0.55) {
|
||||
// Add a rule
|
||||
if (strategy.rules.length < this.config.rulesRange[1]) {
|
||||
strategy.rules.push(strategy.generateRule(this.config.shiftRange));
|
||||
}
|
||||
} else if (mutationType < 0.7) {
|
||||
// Remove a rule
|
||||
if (strategy.rules.length > this.config.rulesRange[0]) {
|
||||
strategy.rules.splice(Math.floor(Math.random() * strategy.rules.length), 1);
|
||||
}
|
||||
} else if (mutationType < 0.85) {
|
||||
// Mutate ATR period
|
||||
strategy.atrPeriod = Math.floor(Math.random() * 31) + 10;
|
||||
} else {
|
||||
// Mutate SL/TP
|
||||
strategy.slMultiplier = (Math.random() * 3) + 1;
|
||||
strategy.tpMultiplier = strategy.slMultiplier + (Math.random() * 5) + 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evolve one generation
|
||||
*/
|
||||
evolveGeneration() {
|
||||
const newPopulation = [];
|
||||
|
||||
// Elitism: keep top 2 strategies
|
||||
newPopulation.push(this.population[0].copy());
|
||||
newPopulation.push(this.population[1].copy());
|
||||
|
||||
// Generate rest through crossover and mutation
|
||||
while (newPopulation.length < this.config.populationSize) {
|
||||
const parent1 = this.tournamentSelection();
|
||||
const parent2 = this.tournamentSelection();
|
||||
|
||||
const child = this.crossover(parent1, parent2);
|
||||
this.mutate(child);
|
||||
|
||||
newPopulation.push(child);
|
||||
}
|
||||
|
||||
this.population = newPopulation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the genetic algorithm
|
||||
*/
|
||||
async run(progressCallback) {
|
||||
console.log('🧬 GA.run() started');
|
||||
console.log('📊 Data rows:', this.data.length);
|
||||
console.log('⚙️ Config:', this.config);
|
||||
this.isRunning = true;
|
||||
this.foundStrategies = [];
|
||||
this.currentGeneration = 0;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
while (this.isRunning && this.foundStrategies.length < this.config.strategiesCount) {
|
||||
console.log('🔄 Starting new GA search, found so far:', this.foundStrategies.length);
|
||||
// Initialize new population for each search
|
||||
this.initializePopulation();
|
||||
console.log('✅ Population initialized:', this.population.length, 'strategies');
|
||||
|
||||
// Evolve for specified generations
|
||||
for (let gen = 0; gen < this.config.generations && this.isRunning; gen++) {
|
||||
this.currentGeneration = gen + 1;
|
||||
|
||||
// Evaluate fitness
|
||||
console.log(`Gen ${gen + 1}/${this.config.generations}: Evaluating population...`);
|
||||
this.evaluatePopulation();
|
||||
console.log(`Gen ${gen + 1}: Best fitness = ${this.population[0].fitness.toFixed(2)}, Trades = ${this.population[0].metrics.totalTrades}`);
|
||||
|
||||
// Report progress
|
||||
if (progressCallback) {
|
||||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||
progressCallback({
|
||||
generation: gen + 1,
|
||||
totalGenerations: this.config.generations,
|
||||
bestFitness: this.population[0].fitness,
|
||||
avgFitness: this.population.reduce((sum, s) => sum + s.fitness, 0) / this.population.length,
|
||||
foundCount: this.foundStrategies.length,
|
||||
targetCount: this.config.strategiesCount,
|
||||
elapsedTime: elapsed
|
||||
});
|
||||
}
|
||||
|
||||
// Check if best strategy meets criteria
|
||||
const best = this.population[0];
|
||||
console.log(`Checking criteria: PF=${best.metrics.profitFactor.toFixed(2)}, WR=${best.metrics.winRate.toFixed(1)}%, Trades=${best.metrics.totalTrades}, DD=${best.metrics.maxDrawdown.toFixed(2)}%`);
|
||||
if (this.meetsCriteria(best) && !this.isDuplicate(best)) {
|
||||
console.log('✅ Strategy meets criteria!');
|
||||
this.foundStrategies.push(best.copy());
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback({
|
||||
strategyFound: best,
|
||||
foundCount: this.foundStrategies.length
|
||||
});
|
||||
}
|
||||
|
||||
// If we found enough, break
|
||||
if (this.foundStrategies.length >= this.config.strategiesCount) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Evolve to next generation
|
||||
if (gen < this.config.generations - 1) {
|
||||
this.evolveGeneration();
|
||||
}
|
||||
|
||||
// Allow UI to update
|
||||
await this.sleep(10);
|
||||
}
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
console.log('🏁 GA.run() complete. Returning', this.foundStrategies.length, 'strategies');
|
||||
console.log('Strategies to return:', this.foundStrategies);
|
||||
return this.foundStrategies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if strategy is duplicate
|
||||
*/
|
||||
isDuplicate(strategy) {
|
||||
for (const existing of this.foundStrategies) {
|
||||
if (this.strategiesSimilar(strategy, existing)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two strategies are similar
|
||||
*/
|
||||
strategiesSimilar(s1, s2) {
|
||||
// Simple check: if rules are identical
|
||||
if (s1.rules.length !== s2.rules.length) return false;
|
||||
|
||||
const rules1 = JSON.stringify(s1.rules);
|
||||
const rules2 = JSON.stringify(s2.rules);
|
||||
|
||||
return rules1 === rules2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the optimization
|
||||
*/
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep helper for async
|
||||
*/
|
||||
sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
/**
|
||||
* HTML Report Generator
|
||||
* Generates comprehensive HTML reports with full trading statement
|
||||
*/
|
||||
|
||||
function generateHTMLReport(strategy, index) {
|
||||
const m = strategy.metrics;
|
||||
const name = `FxMath_${String(index + 1).padStart(3, '0')}_PF${(m.profitFactor || 0).toFixed(2).replace('.', '_')}_WR${Math.round(m.winRate || 0)}`;
|
||||
|
||||
// Generate SELL rules (inverted operators)
|
||||
const sellRulesHTML = strategy.rules.map((rule, i) => {
|
||||
const invertedOp = rule.operator === '>' ? '<=' :
|
||||
rule.operator === '<' ? '>=' :
|
||||
rule.operator === '>=' ? '<' :
|
||||
rule.operator === '<=' ? '>' : rule.operator;
|
||||
|
||||
let ruleText = '';
|
||||
if (rule.type === 'simple') {
|
||||
ruleText = `${rule.left.price.toUpperCase()}[${rule.left.shift}] ${invertedOp} ${rule.right.price.toUpperCase()}[${rule.right.shift}]`;
|
||||
} else {
|
||||
ruleText = `(${rule.left.price1.toUpperCase()}[${rule.left.shift1}] ${rule.left.op} ${rule.left.price2.toUpperCase()}[${rule.left.shift2}]) ${invertedOp} (${rule.right.price.toUpperCase()}[${rule.right.shift}] × ${rule.right.multiplier})`;
|
||||
}
|
||||
return `<div class="rule-item">${i + 1}. ${ruleText}</div>`;
|
||||
}).join('');
|
||||
|
||||
// Generate BUY rules
|
||||
const buyRulesHTML = strategy.rules.map((rule, i) => {
|
||||
let ruleText = '';
|
||||
if (rule.type === 'simple') {
|
||||
ruleText = `${rule.left.price.toUpperCase()}[${rule.left.shift}] ${rule.operator} ${rule.right.price.toUpperCase()}[${rule.right.shift}]`;
|
||||
} else {
|
||||
ruleText = `(${rule.left.price1.toUpperCase()}[${rule.left.shift1}] ${rule.left.op} ${rule.left.price2.toUpperCase()}[${rule.left.shift2}]) ${rule.operator} (${rule.right.price.toUpperCase()}[${rule.right.shift}] × ${rule.right.multiplier})`;
|
||||
}
|
||||
return `<div class="rule-item">${i + 1}. ${ruleText}</div>`;
|
||||
}).join('');
|
||||
|
||||
// Generate trades table
|
||||
const tradesHTML = m.trades.map((trade, i) => {
|
||||
const profitClass = trade.profit > 0 ? 'profit-positive' : 'profit-negative';
|
||||
const typeClass = trade.type === 'BUY' ? 'type-buy' : 'type-sell';
|
||||
return `
|
||||
<tr>
|
||||
<td>${i + 1}</td>
|
||||
<td><span class="badge ${typeClass}">${trade.type}</span></td>
|
||||
<td>${new Date(trade.openTime).toLocaleString()}</td>
|
||||
<td>${new Date(trade.closeTime).toLocaleString()}</td>
|
||||
<td>$${trade.entry.toFixed(2)}</td>
|
||||
<td>$${trade.exit.toFixed(2)}</td>
|
||||
<td class="${profitClass}">$${trade.profit.toFixed(2)}</td>
|
||||
<td>${trade.reason.toUpperCase()}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Generate hourly stats table
|
||||
let hourlyStatsHTML = '';
|
||||
if (m.hourlyStats) {
|
||||
hourlyStatsHTML = m.hourlyStats.map((stats, hour) => {
|
||||
if (stats.trades === 0) return '';
|
||||
const profitClass = stats.profit > 0 ? 'profit-positive' : 'profit-negative';
|
||||
return `
|
||||
<tr>
|
||||
<td>${String(hour).padStart(2, '0')}:00</td>
|
||||
<td>${stats.trades}</td>
|
||||
<td>${stats.wins}</td>
|
||||
<td>${stats.losses}</td>
|
||||
<td class="${profitClass}">$${stats.profit.toFixed(2)}</td>
|
||||
<td>${stats.trades > 0 ? ((stats.wins / stats.trades) * 100).toFixed(1) : 0}%</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${name} - Trading Strategy Report</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.header h1 { font-size: 32px; margin-bottom: 10px; }
|
||||
.header p { font-size: 16px; opacity: 0.9; }
|
||||
.content { padding: 30px; }
|
||||
.section { margin-bottom: 40px; }
|
||||
.section h2 {
|
||||
font-size: 24px;
|
||||
color: #667eea;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
}
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.metric-card {
|
||||
background: #f7fafc;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
.metric-label {
|
||||
font-size: 12px;
|
||||
color: #718096;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.metric-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #2d3748;
|
||||
}
|
||||
.metric-value.positive { color: #48bb78; }
|
||||
.metric-value.negative { color: #f56565; }
|
||||
.rules-box {
|
||||
background: #f7fafc;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.rules-box h3 {
|
||||
font-size: 18px;
|
||||
color: #4a5568;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.rule-item {
|
||||
padding: 8px 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #2d3748;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
}
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
th {
|
||||
background: #f7fafc;
|
||||
font-weight: 600;
|
||||
color: #4a5568;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
tr:hover { background: #f7fafc; }
|
||||
.badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.type-buy {
|
||||
background: #c6f6d5;
|
||||
color: #22543d;
|
||||
}
|
||||
.type-sell {
|
||||
background: #fed7d7;
|
||||
color: #742a2a;
|
||||
}
|
||||
.profit-positive { color: #48bb78; font-weight: 600; }
|
||||
.profit-negative { color: #f56565; font-weight: 600; }
|
||||
.best-worst {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
text-align: center;
|
||||
}
|
||||
.best-worst div {
|
||||
flex: 1;
|
||||
}
|
||||
.best-worst h4 {
|
||||
font-size: 14px;
|
||||
color: #718096;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.best-worst .value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.chart-container {
|
||||
background: #f7fafc;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
height: 300px;
|
||||
}
|
||||
.footer {
|
||||
background: #f7fafc;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
}
|
||||
@media print {
|
||||
body { background: white; padding: 0; }
|
||||
.container { box-shadow: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>${name}</h1>
|
||||
<p>FxMath Quant - Automated Trading Strategy Report</p>
|
||||
<p>Generated: ${new Date().toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Performance Metrics -->
|
||||
<div class="section">
|
||||
<h2>Performance Metrics</h2>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Profit Factor</div>
|
||||
<div class="metric-value positive">${(m.profitFactor || 0).toFixed(2)}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Win Rate</div>
|
||||
<div class="metric-value">${(m.winRate || 0).toFixed(1)}%</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Total Trades</div>
|
||||
<div class="metric-value">${m.totalTrades || 0}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">BUY Trades</div>
|
||||
<div class="metric-value" style="color: #48bb78;">${m.buyTrades || 0}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">SELL Trades</div>
|
||||
<div class="metric-value" style="color: #f56565;">${m.sellTrades || 0}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Max Drawdown</div>
|
||||
<div class="metric-value negative">${(m.maxDrawdown || 0).toFixed(2)}%</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Total Profit</div>
|
||||
<div class="metric-value ${m.totalProfit >= 0 ? 'positive' : 'negative'}">$${(m.totalProfit || 0).toFixed(2)}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Avg Win</div>
|
||||
<div class="metric-value positive">$${(m.avgWin || 0).toFixed(2)}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Avg Loss</div>
|
||||
<div class="metric-value negative">$${Math.abs(m.avgLoss || 0).toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Equity Curve Chart -->
|
||||
<div class="section">
|
||||
<h2>Equity Curve</h2>
|
||||
<div class="chart-container">
|
||||
<canvas id="equityChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${m.hourlyStats ? `
|
||||
<!-- Hourly Performance Chart -->
|
||||
<div class="section">
|
||||
<h2>Hourly Performance (24h)</h2>
|
||||
<div class="chart-container">
|
||||
<canvas id="hourlyChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${m.bestHour && m.worstHour ? `
|
||||
<!-- Best/Worst Hours -->
|
||||
<div class="section">
|
||||
<h2>Time-Based Performance</h2>
|
||||
<div class="best-worst">
|
||||
<div>
|
||||
<h4>Best Trading Hour</h4>
|
||||
<div class="value positive">${String(m.bestHour.hour).padStart(2, '0')}:00</div>
|
||||
<div class="profit-positive">$${m.bestHour.profit.toFixed(2)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Worst Trading Hour</h4>
|
||||
<div class="value negative">${String(m.worstHour.hour).padStart(2, '0')}:00</div>
|
||||
<div class="profit-negative">$${m.worstHour.profit.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Strategy Rules -->
|
||||
<div class="section">
|
||||
<h2>Strategy Rules</h2>
|
||||
<div class="rules-box">
|
||||
<h3>BUY when ALL of the following conditions are true:</h3>
|
||||
${buyRulesHTML}
|
||||
</div>
|
||||
<div class="rules-box">
|
||||
<h3>SELL when ALL of the following conditions are true:</h3>
|
||||
${sellRulesHTML}
|
||||
</div>
|
||||
<div class="rules-box">
|
||||
<h3>Parameters</h3>
|
||||
<div class="rule-item">ATR Period: ${strategy.atrPeriod}</div>
|
||||
<div class="rule-item">SL Multiplier: ${strategy.slMultiplier.toFixed(2)}</div>
|
||||
<div class="rule-item">TP Multiplier: ${strategy.tpMultiplier.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${hourlyStatsHTML ? `
|
||||
<!-- Hourly Performance -->
|
||||
<div class="section">
|
||||
<h2>Hourly Performance Analysis</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hour</th>
|
||||
<th>Trades</th>
|
||||
<th>Wins</th>
|
||||
<th>Losses</th>
|
||||
<th>Profit/Loss</th>
|
||||
<th>Win Rate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${hourlyStatsHTML}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Full Trading Statement -->
|
||||
<div class="section">
|
||||
<h2>Complete Trading Statement</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Type</th>
|
||||
<th>Open Time</th>
|
||||
<th>Close Time</th>
|
||||
<th>Entry</th>
|
||||
<th>Exit</th>
|
||||
<th>Profit/Loss</th>
|
||||
<th>Exit Reason</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${tradesHTML}
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>FxMath Quant - Automated Trading Strategy Generator</p>
|
||||
<p>This report is for informational purposes only. Past performance does not guarantee future results.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Render Equity Curve Chart
|
||||
const equityCtx = document.getElementById('equityChart');
|
||||
if (equityCtx) {
|
||||
new Chart(equityCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ${JSON.stringify(m.equity.map((_, i) => i))},
|
||||
datasets: [{
|
||||
label: 'Account Balance',
|
||||
data: ${JSON.stringify(m.equity)},
|
||||
borderColor: '#667eea',
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: '#1a1f3a',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#a0aec0'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
grid: { color: 'rgba(0, 0, 0, 0.1)' },
|
||||
ticks: { color: '#4a5568' }
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: '#4a5568' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render Hourly Performance Chart
|
||||
const hourlyCtx = document.getElementById('hourlyChart');
|
||||
if (hourlyCtx) {
|
||||
const hourlyData = ${JSON.stringify(m.hourlyStats || [])};
|
||||
new Chart(hourlyCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0') + ':00'),
|
||||
datasets: [{
|
||||
label: 'Profit/Loss',
|
||||
data: hourlyData.map(h => h.profit),
|
||||
backgroundColor: hourlyData.map(h => h.profit >= 0 ? 'rgba(72, 187, 120, 0.6)' : 'rgba(245, 101, 101, 0.6)'),
|
||||
borderColor: hourlyData.map(h => h.profit >= 0 ? 'rgba(72, 187, 120, 1)' : 'rgba(245, 101, 101, 1)'),
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const hour = context.dataIndex;
|
||||
const stats = hourlyData[hour];
|
||||
return [
|
||||
'Profit: $' + stats.profit.toFixed(2),
|
||||
'Trades: ' + stats.trades,
|
||||
'Wins: ' + stats.wins + ' | Losses: ' + stats.losses
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(0, 0, 0, 0.1)' },
|
||||
ticks: { color: '#4a5568' }
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: '#4a5568', maxRotation: 45, minRotation: 45 }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// Export function
|
||||
window.generateHTMLReport = generateHTMLReport;
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* License Check Module
|
||||
* Verifies license on app load and redirects if invalid
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// Check license immediately when script loads
|
||||
checkLicense();
|
||||
|
||||
/**
|
||||
* Main license check function
|
||||
*/
|
||||
function checkLicense() {
|
||||
const licenseData = localStorage.getItem('fxmath_license');
|
||||
|
||||
// No license found
|
||||
if (!licenseData) {
|
||||
redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const license = JSON.parse(licenseData);
|
||||
|
||||
// Validate license structure
|
||||
if (!license.key || !license.type || !license.status) {
|
||||
console.warn('Invalid license data structure');
|
||||
redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if license is active
|
||||
if (license.status !== 'active') {
|
||||
console.warn('License is not active');
|
||||
redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check expiry for limited licenses
|
||||
if (license.type === 'limited') {
|
||||
if (!license.expiry_date) {
|
||||
console.warn('Limited license missing expiry date');
|
||||
redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
const expiryDate = new Date(license.expiry_date);
|
||||
const now = new Date();
|
||||
|
||||
if (now > expiryDate) {
|
||||
console.warn('License has expired');
|
||||
localStorage.removeItem('fxmath_license');
|
||||
alert('Your license has expired. Please renew your license to continue using FxMathQuant.');
|
||||
redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate days remaining
|
||||
const daysRemaining = Math.ceil((expiryDate - now) / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Warn if license is expiring soon (7 days or less)
|
||||
if (daysRemaining <= 7 && daysRemaining > 0) {
|
||||
console.warn(`License expiring in ${daysRemaining} days`);
|
||||
showExpiryWarning(daysRemaining);
|
||||
}
|
||||
}
|
||||
|
||||
// License is valid
|
||||
console.log('License validated successfully');
|
||||
displayLicenseInfo(license);
|
||||
|
||||
} catch (error) {
|
||||
console.error('License check error:', error);
|
||||
redirectToLogin();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to login page
|
||||
*/
|
||||
function redirectToLogin() {
|
||||
// Only redirect if not already on login page
|
||||
if (!window.location.pathname.includes('login.html')) {
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show expiry warning
|
||||
*/
|
||||
function showExpiryWarning(daysRemaining) {
|
||||
// Create warning banner if it doesn't exist
|
||||
let banner = document.getElementById('license-expiry-warning');
|
||||
|
||||
if (!banner) {
|
||||
banner = document.createElement('div');
|
||||
banner.id = 'license-expiry-warning';
|
||||
banner.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
`;
|
||||
|
||||
banner.innerHTML = `
|
||||
⚠️ Your license will expire in ${daysRemaining} day${daysRemaining !== 1 ? 's' : ''}.
|
||||
Please renew to continue using FxMathQuant.
|
||||
<button onclick="this.parentElement.remove()" style="
|
||||
background: rgba(255,255,255,0.3);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
margin-left: 15px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
">Dismiss</button>
|
||||
`;
|
||||
|
||||
document.body.insertBefore(banner, document.body.firstChild);
|
||||
|
||||
// Adjust body padding to account for banner
|
||||
document.body.style.paddingTop = '50px';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display license info in console (for debugging)
|
||||
*/
|
||||
function displayLicenseInfo(license) {
|
||||
console.log('%c License Information ', 'background: #667eea; color: white; font-weight: bold; padding: 5px 10px;');
|
||||
console.log('Type:', license.type);
|
||||
console.log('Status:', license.status);
|
||||
|
||||
if (license.type === 'limited') {
|
||||
console.log('Expiry Date:', license.expiry_date);
|
||||
console.log('Days Remaining:', license.days_remaining);
|
||||
} else {
|
||||
console.log('Expiry:', 'Never (Lifetime)');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose logout function globally
|
||||
*/
|
||||
window.logoutLicense = function () {
|
||||
if (confirm('Are you sure you want to logout? You will need to enter your license key again.')) {
|
||||
localStorage.removeItem('fxmath_license');
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* License Login Handler
|
||||
* FxMathQuant-Web
|
||||
*/
|
||||
|
||||
// API endpoint - Production server
|
||||
const API_URL = 'https://fxmath.com/quantw/api/validate.php';
|
||||
|
||||
// DOM elements
|
||||
const licenseForm = document.getElementById('licenseForm');
|
||||
const licenseKeyInput = document.getElementById('licenseKey');
|
||||
const activateBtn = document.getElementById('activateBtn');
|
||||
const alertBox = document.getElementById('alertBox');
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Check if already licensed
|
||||
if (isLicenseValid()) {
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
// Format license key input
|
||||
licenseKeyInput.addEventListener('input', formatLicenseKey);
|
||||
|
||||
// Handle form submission
|
||||
licenseForm.addEventListener('submit', handleLicenseSubmit);
|
||||
});
|
||||
|
||||
/**
|
||||
* Format license key as user types (add dashes)
|
||||
*/
|
||||
function formatLicenseKey(e) {
|
||||
let value = e.target.value.replace(/[^A-Z0-9]/gi, '').toUpperCase();
|
||||
let formatted = '';
|
||||
|
||||
for (let i = 0; i < value.length && i < 32; i++) {
|
||||
if (i > 0 && i % 4 === 0) {
|
||||
formatted += '-';
|
||||
}
|
||||
formatted += value[i];
|
||||
}
|
||||
|
||||
e.target.value = formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle license form submission
|
||||
*/
|
||||
async function handleLicenseSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const licenseKey = licenseKeyInput.value.trim();
|
||||
|
||||
// Validate format
|
||||
if (!isValidLicenseFormat(licenseKey)) {
|
||||
showAlert('Please enter a valid license key format', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable button and show loading
|
||||
activateBtn.disabled = true;
|
||||
activateBtn.innerHTML = '<span class="spinner"></span>Validating...';
|
||||
|
||||
try {
|
||||
// Call validation API
|
||||
const response = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ license_key: licenseKey })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Store license info in localStorage
|
||||
const licenseInfo = {
|
||||
key: licenseKey,
|
||||
type: data.license.type,
|
||||
status: data.license.status,
|
||||
validated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (data.license.type === 'limited') {
|
||||
licenseInfo.expiry_date = data.license.expiry_date;
|
||||
licenseInfo.days_remaining = data.license.days_remaining;
|
||||
}
|
||||
|
||||
localStorage.setItem('fxmath_license', JSON.stringify(licenseInfo));
|
||||
|
||||
// Show success message
|
||||
showAlert('License activated successfully! Redirecting...', 'success');
|
||||
|
||||
// Redirect to main app
|
||||
setTimeout(() => {
|
||||
window.location.href = 'index.html';
|
||||
}, 1500);
|
||||
|
||||
} else {
|
||||
// Show error message
|
||||
showAlert(data.message || 'License validation failed', 'error');
|
||||
activateBtn.disabled = false;
|
||||
activateBtn.innerHTML = 'Activate License';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Validation error:', error);
|
||||
showAlert('Connection error. Please check your server configuration.', 'error');
|
||||
activateBtn.disabled = false;
|
||||
activateBtn.innerHTML = 'Activate License';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate license key format
|
||||
*/
|
||||
function isValidLicenseFormat(key) {
|
||||
// Should be 32 alphanumeric characters with dashes every 4 characters
|
||||
const pattern = /^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/;
|
||||
return pattern.test(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if license is valid
|
||||
*/
|
||||
function isLicenseValid() {
|
||||
const licenseData = localStorage.getItem('fxmath_license');
|
||||
|
||||
if (!licenseData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const license = JSON.parse(licenseData);
|
||||
|
||||
// Check if license exists
|
||||
if (!license.key || license.status !== 'active') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check expiry for limited licenses
|
||||
if (license.type === 'limited') {
|
||||
const expiryDate = new Date(license.expiry_date);
|
||||
const now = new Date();
|
||||
|
||||
if (now > expiryDate) {
|
||||
// License expired
|
||||
localStorage.removeItem('fxmath_license');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('License validation error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show alert message
|
||||
*/
|
||||
function showAlert(message, type) {
|
||||
alertBox.textContent = message;
|
||||
alertBox.className = `alert alert-${type} show`;
|
||||
|
||||
// Auto-hide after 5 seconds for non-success messages
|
||||
if (type !== 'success') {
|
||||
setTimeout(() => {
|
||||
alertBox.classList.remove('show');
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
+725
@@ -0,0 +1,725 @@
|
||||
/**
|
||||
* Main Application Controller
|
||||
*/
|
||||
|
||||
// Global state
|
||||
let appData = {
|
||||
csvData: null,
|
||||
dataName: '',
|
||||
optimizer: null,
|
||||
foundStrategies: [],
|
||||
selectedStrategies: new Set(),
|
||||
isGenerating: false
|
||||
};
|
||||
|
||||
// Initialize app when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeApp();
|
||||
});
|
||||
|
||||
function initializeApp() {
|
||||
setupEventListeners();
|
||||
showSection('upload-section');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup all event listeners
|
||||
*/
|
||||
function setupEventListeners() {
|
||||
// Upload zone
|
||||
const uploadZone = document.getElementById('upload-zone');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const browseBtn = document.getElementById('browse-btn');
|
||||
|
||||
uploadZone.addEventListener('click', () => fileInput.click());
|
||||
browseBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
handleFileUpload(e.target.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Drag and drop
|
||||
uploadZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.add('dragover');
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('dragleave', () => {
|
||||
uploadZone.classList.remove('dragover');
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.remove('dragover');
|
||||
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
handleFileUpload(e.dataTransfer.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Navigation buttons
|
||||
document.getElementById('continue-btn').addEventListener('click', () => {
|
||||
showSection('config-section');
|
||||
});
|
||||
|
||||
document.getElementById('back-btn').addEventListener('click', () => {
|
||||
showSection('upload-section');
|
||||
});
|
||||
|
||||
document.getElementById('start-btn').addEventListener('click', startGeneration);
|
||||
document.getElementById('stop-btn').addEventListener('click', stopGeneration);
|
||||
document.getElementById('new-search-btn').addEventListener('click', () => {
|
||||
showSection('upload-section');
|
||||
appData.foundStrategies = [];
|
||||
});
|
||||
|
||||
document.getElementById('download-all-btn').addEventListener('click', downloadAllStrategies);
|
||||
|
||||
// Comparison button
|
||||
const compareBtn = document.getElementById('compare-btn');
|
||||
if (compareBtn) {
|
||||
compareBtn.addEventListener('click', () => {
|
||||
if (appData.selectedStrategies.size < 2) {
|
||||
alert('Please select at least 2 strategies to compare.');
|
||||
return;
|
||||
}
|
||||
showComparison();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CSV file upload
|
||||
*/
|
||||
function handleFileUpload(file) {
|
||||
console.log('📁 File upload started:', file.name, 'Size:', file.size);
|
||||
if (!file.name.endsWith('.csv')) {
|
||||
alert('Please upload a CSV file');
|
||||
return;
|
||||
}
|
||||
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
dynamicTyping: true,
|
||||
skipEmptyLines: true,
|
||||
complete: (results) => {
|
||||
console.log('📊 CSV parsed:', results.data.length, 'rows');
|
||||
if (results.data.length === 0) {
|
||||
alert('CSV file is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate CSV structure (accept both lowercase and capitalized column names)
|
||||
const firstRow = results.data[0];
|
||||
const requiredFields = ['open', 'high', 'low', 'close'];
|
||||
const hasRequiredFields = requiredFields.every(field => {
|
||||
// Check lowercase, capitalized, and uppercase versions
|
||||
return field in firstRow ||
|
||||
field.charAt(0).toUpperCase() + field.slice(1) in firstRow ||
|
||||
field.toUpperCase() in firstRow;
|
||||
});
|
||||
|
||||
if (!hasRequiredFields) {
|
||||
alert('CSV must contain: time, open, high, low, close columns');
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize column names to lowercase
|
||||
appData.csvData = results.data.map(row => ({
|
||||
time: row.time || row.Time || row.TIME,
|
||||
open: parseFloat(row.open || row.Open || row.OPEN),
|
||||
high: parseFloat(row.high || row.High || row.HIGH),
|
||||
low: parseFloat(row.low || row.Low || row.LOW),
|
||||
close: parseFloat(row.close || row.Close || row.CLOSE)
|
||||
}));
|
||||
|
||||
appData.dataName = file.name;
|
||||
console.log('✅ Data loaded successfully:', appData.csvData.length, 'bars');
|
||||
|
||||
// Apply data size limit
|
||||
applyDataSizeLimit();
|
||||
|
||||
displayDataPreview();
|
||||
},
|
||||
error: (error) => {
|
||||
alert('Error parsing CSV: ' + error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display data preview
|
||||
*/
|
||||
function displayDataPreview() {
|
||||
const preview = document.getElementById('data-preview');
|
||||
preview.classList.remove('hidden');
|
||||
|
||||
// Update data info
|
||||
document.getElementById('data-symbol').textContent = `File: ${appData.dataName}`;
|
||||
document.getElementById('data-bars').textContent = `Bars: ${appData.csvData.length}`;
|
||||
|
||||
// Detect timeframe (simple heuristic)
|
||||
const timeframe = detectTimeframe();
|
||||
document.getElementById('data-timeframe').textContent = `Timeframe: ${timeframe}`;
|
||||
|
||||
// Show first 10 rows
|
||||
const tbody = document.getElementById('preview-body');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
const previewRows = appData.csvData.slice(0, 10);
|
||||
previewRows.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${row.time || '-'}</td>
|
||||
<td>${row.open.toFixed(5)}</td>
|
||||
<td>${row.high.toFixed(5)}</td>
|
||||
<td>${row.low.toFixed(5)}</td>
|
||||
<td>${row.close.toFixed(5)}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply data size limit
|
||||
*/
|
||||
function applyDataSizeLimit() {
|
||||
const limit = parseInt(document.getElementById('data-size-limit').value);
|
||||
if (limit > 0 && appData.csvData.length > limit) {
|
||||
console.log(`✂️ Limiting data from ${appData.csvData.length} to ${limit} bars`);
|
||||
appData.csvData = appData.csvData.slice(-limit); // Take last N bars
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect timeframe from data
|
||||
*/
|
||||
function detectTimeframe() {
|
||||
if (appData.csvData.length < 2) return 'Unknown';
|
||||
|
||||
// Try to parse time difference
|
||||
const time1 = new Date(appData.csvData[0].time);
|
||||
const time2 = new Date(appData.csvData[1].time);
|
||||
|
||||
if (isNaN(time1) || isNaN(time2)) return 'Unknown';
|
||||
|
||||
const diffMinutes = Math.abs(time2 - time1) / (1000 * 60);
|
||||
|
||||
if (diffMinutes <= 1) return 'M1';
|
||||
if (diffMinutes <= 5) return 'M5';
|
||||
if (diffMinutes <= 15) return 'M15';
|
||||
if (diffMinutes <= 30) return 'M30';
|
||||
if (diffMinutes <= 60) return 'H1';
|
||||
if (diffMinutes <= 240) return 'H4';
|
||||
if (diffMinutes <= 1440) return 'D1';
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Start strategy generation
|
||||
*/
|
||||
async function startGeneration() {
|
||||
console.log('🚀 Starting strategy generation...');
|
||||
if (!appData.csvData) {
|
||||
console.error('❌ No data loaded!');
|
||||
alert('Please upload data first');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get configuration
|
||||
console.log('⚙️ Reading configuration...');
|
||||
const config = {
|
||||
populationSize: parseInt(document.getElementById('population').value),
|
||||
generations: parseInt(document.getElementById('generations').value),
|
||||
strategiesCount: parseInt(document.getElementById('strategies-count').value),
|
||||
rulesRange: [
|
||||
parseInt(document.getElementById('rules-min').value),
|
||||
parseInt(document.getElementById('rules-max').value)
|
||||
],
|
||||
shiftRange: [
|
||||
parseInt(document.getElementById('shift-min').value),
|
||||
parseInt(document.getElementById('shift-max').value)
|
||||
],
|
||||
minPF: parseFloat(document.getElementById('min-pf').value),
|
||||
minWR: parseFloat(document.getElementById('min-wr').value),
|
||||
maxDD: parseFloat(document.getElementById('max-dd').value),
|
||||
minTrades: parseInt(document.getElementById('min-trades').value)
|
||||
};
|
||||
|
||||
// Show progress section
|
||||
showSection('progress-section');
|
||||
appData.isGenerating = true;
|
||||
appData.foundStrategies = [];
|
||||
|
||||
// Reset buttons visibility
|
||||
const stopBtn = document.getElementById('stop-btn');
|
||||
if (stopBtn) stopBtn.classList.remove('hidden');
|
||||
|
||||
const viewResultsBtn = document.getElementById('view-results-btn');
|
||||
if (viewResultsBtn) viewResultsBtn.classList.add('hidden');
|
||||
|
||||
// Clear progress log
|
||||
document.getElementById('progress-log').innerHTML = '';
|
||||
|
||||
// Create optimizer
|
||||
console.log('🧬 Creating GA optimizer with config:', config);
|
||||
appData.optimizer = new GeneticOptimizer(appData.csvData, config);
|
||||
|
||||
// Start time
|
||||
const startTime = Date.now();
|
||||
|
||||
// Run optimization
|
||||
console.log('▶️ Starting GA run...');
|
||||
const strategies = await appData.optimizer.run((progress) => {
|
||||
updateProgress(progress, startTime);
|
||||
});
|
||||
|
||||
// Handle completion
|
||||
console.log('✅ GA complete! Found', strategies.length, 'strategies');
|
||||
appData.foundStrategies = strategies;
|
||||
appData.isGenerating = false;
|
||||
|
||||
// Stay on page as requested, show redirection button
|
||||
showGenerationCompleteUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update progress display
|
||||
*/
|
||||
function updateProgress(progress, startTime) {
|
||||
// Update stats
|
||||
if (progress.generation !== undefined) {
|
||||
document.getElementById('current-gen').textContent =
|
||||
`${progress.generation} / ${progress.totalGenerations}`;
|
||||
document.getElementById('best-fitness').textContent =
|
||||
progress.bestFitness.toFixed(2);
|
||||
document.getElementById('found-count').textContent =
|
||||
`${progress.foundCount} / ${progress.targetCount}`;
|
||||
|
||||
// Update progress bar
|
||||
const progressPercent = (progress.generation / progress.totalGenerations) * 100;
|
||||
document.getElementById('progress-bar').style.width = progressPercent + '%';
|
||||
}
|
||||
|
||||
// Update elapsed time
|
||||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||
document.getElementById('elapsed-time').textContent = formatTime(elapsed);
|
||||
|
||||
// Log strategy found
|
||||
if (progress.strategyFound) {
|
||||
// Don't add to appData.foundStrategies here - we'll get them from GA at the end
|
||||
// This prevents duplicate storage and ensures metrics are preserved
|
||||
|
||||
const log = document.getElementById('progress-log');
|
||||
const m = progress.strategyFound.metrics;
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = 'log-entry';
|
||||
logEntry.style.display = 'flex';
|
||||
logEntry.style.justifyContent = 'space-between';
|
||||
logEntry.style.alignItems = 'center';
|
||||
logEntry.style.padding = '5px 0';
|
||||
logEntry.style.color = '#48bb78';
|
||||
|
||||
const text = document.createElement('span');
|
||||
text.textContent = `✓ Strategy ${progress.foundCount}: PF=${m.profitFactor.toFixed(2)}, WR=${m.winRate.toFixed(1)}%, Trades=${m.totalTrades}`;
|
||||
|
||||
const viewBtn = document.createElement('button');
|
||||
viewBtn.className = 'btn-small';
|
||||
viewBtn.textContent = 'View Details';
|
||||
viewBtn.onclick = () => {
|
||||
const index = progress.foundCount - 1;
|
||||
viewStrategyDetails(index);
|
||||
};
|
||||
|
||||
logEntry.appendChild(text);
|
||||
logEntry.appendChild(viewBtn);
|
||||
log.appendChild(logEntry);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop generation
|
||||
*/
|
||||
function stopGeneration() {
|
||||
if (appData.optimizer) {
|
||||
appData.optimizer.stop();
|
||||
appData.isGenerating = false;
|
||||
|
||||
// Update collection with whatever was found so far
|
||||
if (appData.optimizer.foundStrategies) {
|
||||
appData.foundStrategies = appData.optimizer.foundStrategies;
|
||||
}
|
||||
}
|
||||
|
||||
if (appData.foundStrategies.length > 0) {
|
||||
showGenerationCompleteUI();
|
||||
} else {
|
||||
alert('No strategies found yet');
|
||||
showSection('config-section');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show UI when generation is complete/stopped
|
||||
*/
|
||||
function showGenerationCompleteUI() {
|
||||
const stopBtn = document.getElementById('stop-btn');
|
||||
if (stopBtn) stopBtn.classList.add('hidden');
|
||||
|
||||
let viewResultsBtn = document.getElementById('view-results-btn');
|
||||
if (!viewResultsBtn) {
|
||||
viewResultsBtn = document.createElement('button');
|
||||
viewResultsBtn.id = 'view-results-btn';
|
||||
viewResultsBtn.className = 'btn-success';
|
||||
viewResultsBtn.textContent = 'View All Results →';
|
||||
viewResultsBtn.style.marginLeft = '10px';
|
||||
viewResultsBtn.onclick = displayResults;
|
||||
if (stopBtn && stopBtn.parentNode) {
|
||||
stopBtn.parentNode.appendChild(viewResultsBtn);
|
||||
}
|
||||
} else {
|
||||
viewResultsBtn.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display results
|
||||
*/
|
||||
function displayResults() {
|
||||
console.log('📊 Displaying results...', appData.foundStrategies.length, 'strategies');
|
||||
showSection('results-section');
|
||||
|
||||
const grid = document.getElementById('strategies-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
if (appData.foundStrategies.length === 0) {
|
||||
console.warn('⚠️ No strategies to display!');
|
||||
grid.innerHTML = '<p style="color: #a0aec0; text-align: center; padding: 40px;">No strategies found. Try adjusting your criteria.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
appData.foundStrategies.forEach((strategy, index) => {
|
||||
console.log(`Creating card for strategy ${index + 1}:`, strategy.metrics);
|
||||
const card = createStrategyCard(strategy, index + 1);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
// Reset selection on display
|
||||
appData.selectedStrategies.clear();
|
||||
updateCompareButton();
|
||||
|
||||
console.log('✅ Strategy cards created:', grid.children.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create strategy card
|
||||
*/
|
||||
function createStrategyCard(strategy, number) {
|
||||
console.log('Creating card for strategy number:', number, 'Strategy:', strategy);
|
||||
|
||||
if (!strategy || !strategy.metrics) {
|
||||
console.error('Invalid strategy object:', strategy);
|
||||
const errorCard = document.createElement('div');
|
||||
errorCard.className = 'strategy-card';
|
||||
errorCard.innerHTML = '<p style="color: red;">Error: Invalid strategy data</p>';
|
||||
return errorCard;
|
||||
}
|
||||
|
||||
const m = strategy.metrics;
|
||||
const card = document.createElement('div');
|
||||
card.className = 'strategy-card';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="strategy-header">
|
||||
<div class="strategy-name">${name}</div>
|
||||
<input type="checkbox" class="strategy-select" onchange="toggleStrategySelection(${number - 1}, this.checked)">
|
||||
</div>
|
||||
<div class="strategy-metrics">
|
||||
<div class="metric">
|
||||
<span class="metric-label">Profit Factor</span>
|
||||
<span class="metric-value ${m.profitFactor >= 1.5 ? 'positive' : 'negative'}">
|
||||
${m.profitFactor.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Win Rate</span>
|
||||
<span class="metric-value">${m.winRate.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Trades</span>
|
||||
<span class="metric-value">${m.totalTrades}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Max DD</span>
|
||||
<span class="metric-value negative">${m.maxDrawdown.toFixed(2)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="strategy-actions">
|
||||
<button class="btn-secondary" onclick="viewStrategyDetails(${number - 1})">View Details</button>
|
||||
<button class="btn-primary" onclick="downloadStrategy(${number - 1}, 'mq4')">MQ4</button>
|
||||
<button class="btn-primary" onclick="downloadStrategy(${number - 1}, 'mq5')">MQ5</button>
|
||||
<button class="btn-success" onclick="downloadStrategy(${number - 1}, 'report')">Report</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
console.log('Card HTML created successfully');
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download single strategy
|
||||
*/
|
||||
function downloadStrategy(index, type) {
|
||||
console.log(`💾 Downloading strategy ${index} type ${type}...`);
|
||||
const strategy = appData.foundStrategies[index];
|
||||
if (!strategy) {
|
||||
console.error(`❌ Strategy at index ${index} not found!`);
|
||||
return;
|
||||
}
|
||||
|
||||
const m = strategy.metrics;
|
||||
const name = `FxMath_${String(index + 1).padStart(3, '0')}_PF${m.profitFactor.toFixed(2).replace('.', '_')}_WR${Math.round(m.winRate)}`;
|
||||
|
||||
let content, filename;
|
||||
|
||||
try {
|
||||
// Convert strategy to EA-Convertor format
|
||||
const strategyData = {
|
||||
parameters: {
|
||||
symbol: 'EURUSD',
|
||||
atr_period: strategy.atrPeriod,
|
||||
sl_multiplier: strategy.slMultiplier,
|
||||
tp_multiplier: strategy.tpMultiplier,
|
||||
close_at_opposite: strategy.closeAtOpposite || true
|
||||
},
|
||||
buy_rules: strategy.rules,
|
||||
sell_rules: strategy.rules // SELL rules are inverted in the converters
|
||||
};
|
||||
|
||||
if (type === 'mq4') {
|
||||
const converter = new MQ4Converter(strategyData);
|
||||
content = converter.generate();
|
||||
filename = name + '.mq4';
|
||||
} else if (type === 'mq5') {
|
||||
const converter = new MQ5Converter(strategyData);
|
||||
content = converter.generate();
|
||||
filename = name + '.mq5';
|
||||
} else if (type === 'ctrader') {
|
||||
const converter = new CTraderConverter(strategyData);
|
||||
content = converter.generate();
|
||||
filename = name + '.cs';
|
||||
} else if (type === 'pine') {
|
||||
const converter = new PineConverter(strategyData);
|
||||
content = converter.generate();
|
||||
filename = name + '.pine';
|
||||
} else if (type === 'report') {
|
||||
// Use new HTML report generator
|
||||
content = generateHTMLReport(strategy, index);
|
||||
filename = name + '_report.html';
|
||||
} else if (type === 'json') {
|
||||
content = JSON.stringify(strategy.toJSON(), null, 2);
|
||||
filename = name + '.json';
|
||||
}
|
||||
|
||||
console.log(`✅ Content generated for ${filename}, size: ${content.length}`);
|
||||
downloadFile(content, filename);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error generating ${type} for strategy ${index}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download all strategies
|
||||
*/
|
||||
function downloadAllStrategies() {
|
||||
console.log('📥 Download All Strategies started...', appData.foundStrategies.length, 'strategies');
|
||||
|
||||
if (appData.foundStrategies.length === 0) {
|
||||
alert('No strategies to download');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('download-all-btn');
|
||||
const originalText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Preparing Downloads...';
|
||||
|
||||
const total = appData.foundStrategies.length;
|
||||
let completed = 0;
|
||||
|
||||
appData.foundStrategies.forEach((strategy, index) => {
|
||||
setTimeout(() => {
|
||||
console.log(`⏱️ Triggering downloads for strategy ${index + 1}/${total}...`);
|
||||
btn.textContent = `Downloading ${index + 1}/${total}...`;
|
||||
|
||||
downloadStrategy(index, 'mq4');
|
||||
setTimeout(() => downloadStrategy(index, 'mq5'), 150);
|
||||
setTimeout(() => downloadStrategy(index, 'report'), 300);
|
||||
setTimeout(() => downloadStrategy(index, 'json'), 450);
|
||||
|
||||
completed++;
|
||||
if (completed === total) {
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = originalText;
|
||||
console.log('✅ All downloads triggered!');
|
||||
}, 1000);
|
||||
}
|
||||
}, index * 1200); // 1.2s per strategy to be safe
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file helper
|
||||
*/
|
||||
function downloadFile(content, filename) {
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Delay removal and revocation to ensure browser captures the click
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show specific section
|
||||
*/
|
||||
function showSection(sectionId) {
|
||||
const sections = document.querySelectorAll('.section');
|
||||
sections.forEach(section => section.classList.remove('active'));
|
||||
document.getElementById(sectionId).classList.add('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle strategy selection for comparison
|
||||
*/
|
||||
function toggleStrategySelection(index, isSelected) {
|
||||
if (isSelected) {
|
||||
appData.selectedStrategies.add(index);
|
||||
} else {
|
||||
appData.selectedStrategies.delete(index);
|
||||
}
|
||||
updateCompareButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update comparison button text
|
||||
*/
|
||||
function updateCompareButton() {
|
||||
const btn = document.getElementById('compare-btn');
|
||||
if (btn) {
|
||||
const count = appData.selectedStrategies.size;
|
||||
btn.textContent = `Compare Selected (${count})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show comparison section
|
||||
*/
|
||||
function showComparison() {
|
||||
const container = document.getElementById('comparison-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
appData.selectedStrategies.forEach(index => {
|
||||
const strategy = appData.foundStrategies[index];
|
||||
const m = strategy.metrics;
|
||||
const name = `Strategy ${index + 1}`;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'comparison-item';
|
||||
item.innerHTML = `
|
||||
<h3>${name}</h3>
|
||||
<div class="strategy-metrics">
|
||||
<div class="metric">
|
||||
<span class="metric-label">PF</span>
|
||||
<span class="metric-value ${m.profitFactor >= 1.5 ? 'positive' : 'negative'}">${m.profitFactor.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Win Rate</span>
|
||||
<span class="metric-value">${m.winRate.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Trades</span>
|
||||
<span class="metric-value">${m.totalTrades}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Max DD</span>
|
||||
<span class="metric-value negative">${m.maxDrawdown.toFixed(2)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 15px;">
|
||||
<canvas id="comp-chart-${index}" style="height: 150px;"></canvas>
|
||||
</div>
|
||||
<div class="strategy-actions" style="margin-top: 15px;">
|
||||
<button class="btn-small" onclick="viewStrategyDetails(${index})">Full Details</button>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
|
||||
// Draw small chart
|
||||
setTimeout(() => {
|
||||
const ctx = document.getElementById(`comp-chart-${index}`).getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: m.equity.map((_, i) => i),
|
||||
datasets: [{
|
||||
data: m.equity,
|
||||
borderColor: '#667eea',
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
fill: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { x: { display: false }, y: { display: false } }
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
||||
showSection('results-section'); // Ensure we are in results
|
||||
document.getElementById('comparison-section').classList.remove('hidden');
|
||||
document.getElementById('comparison-section').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide comparison section
|
||||
*/
|
||||
function hideComparison() {
|
||||
document.getElementById('comparison-section').classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time in seconds to readable format
|
||||
*/
|
||||
function formatTime(seconds) {
|
||||
if (seconds < 60) return seconds + 's';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${minutes}m ${secs}s`;
|
||||
}
|
||||
|
||||
// Explicitly expose functions to global scope for inline onclick handlers
|
||||
window.downloadStrategy = downloadStrategy;
|
||||
window.downloadAllStrategies = downloadAllStrategies;
|
||||
window.toggleStrategySelection = toggleStrategySelection;
|
||||
window.showComparison = showComparison;
|
||||
window.hideComparison = hideComparison;
|
||||
@@ -0,0 +1,328 @@
|
||||
// MQ4 Converter
|
||||
class MQ4Converter {
|
||||
constructor(strategy) {
|
||||
this.strategy = strategy;
|
||||
this.parser = new RuleParser();
|
||||
}
|
||||
|
||||
generate() {
|
||||
const { parameters, buy_rules, sell_rules } = this.strategy;
|
||||
const symbol = parameters.symbol || 'EURUSD';
|
||||
const atrPeriod = parameters.atr_period || 14;
|
||||
const slMultiplier = parameters.sl_multiplier || 2.0;
|
||||
const tpMultiplier = parameters.tp_multiplier || 3.0;
|
||||
|
||||
// Generate random 6-digit magic number
|
||||
const magicNumber = Math.floor(100000 + Math.random() * 900000);
|
||||
|
||||
const buyConditions = this.parser.parseRules(buy_rules, 'mql');
|
||||
const sellConditions = this.parser.parseRules(sell_rules, 'mql');
|
||||
|
||||
return `//+------------------------------------------------------------------+
|
||||
//| Strategy_${symbol}.mq4 |
|
||||
//| Generated by Strategy Converter |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "FxMath Quant"
|
||||
#property link "https://fxmath.com"
|
||||
#property version "1.00"
|
||||
#property strict
|
||||
|
||||
//=== TRADE SETTINGS ===
|
||||
input double LotSize = 0.1; // Lot Size
|
||||
input int ATR_Period = ${atrPeriod}; // ATR Period
|
||||
input double SL_Multiplier = ${slMultiplier.toFixed(2)}; // SL ATR Multiplier
|
||||
input double TP_Multiplier = ${tpMultiplier.toFixed(2)}; // TP ATR Multiplier
|
||||
input int MagicNumber = ${magicNumber}; // Magic Number
|
||||
input string TradeComment = "FxMath EA"; // Trade Comment
|
||||
input int Slippage = 3; // Slippage
|
||||
|
||||
//=== SL/TP SETTINGS ===
|
||||
input bool EnableHardSL = true; // Enable Hard Stop Loss
|
||||
input bool EnableHardTP = true; // Enable Hard Take Profit
|
||||
|
||||
//=== TRAILING STOP ===
|
||||
input bool EnableTrailing = false; // Enable Trailing Stop
|
||||
input double TrailingStart = 30; // Trailing Start (pips)
|
||||
input double TrailingStep = 10; // Trailing Step (pips)
|
||||
|
||||
//=== BREAKEVEN ===
|
||||
input bool EnableBreakeven = false; // Enable Breakeven
|
||||
input double BreakevenStart = 20; // Breakeven Start (pips)
|
||||
input double BreakevenOffset = 2; // Breakeven Offset (pips)
|
||||
|
||||
//=== TIME FILTER ===
|
||||
input bool EnableTimeFilter = false; // Enable Time Filter
|
||||
input int StartHour = 8; // Start Hour (Server Time)
|
||||
input int StartMinute = 0; // Start Minute
|
||||
input int EndHour = 22; // End Hour (Server Time)
|
||||
input int EndMinute = 0; // End Minute
|
||||
|
||||
//=== SIGNAL SETTINGS ===
|
||||
input bool CloseOnOpposite = true; // Close Trade on Opposite Signal
|
||||
|
||||
//=== DISPLAY SETTINGS ===
|
||||
input bool ShowChartInfo = true; // Show Info on Chart
|
||||
|
||||
// Global Variables
|
||||
double atrValue;
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Expert initialization function |
|
||||
//+------------------------------------------------------------------+
|
||||
int OnInit()
|
||||
{
|
||||
Print("Strategy EA initialized for ${symbol}");
|
||||
Print("Magic Number: ", MagicNumber);
|
||||
return(INIT_SUCCEEDED);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Expert deinitialization function |
|
||||
//+------------------------------------------------------------------+
|
||||
void OnDeinit(const int reason)
|
||||
{
|
||||
Print("Strategy EA deinitialized");
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Expert tick function |
|
||||
//+------------------------------------------------------------------+
|
||||
void OnTick()
|
||||
{
|
||||
// Calculate ATR
|
||||
atrValue = iATR(Symbol(), 0, ATR_Period, 1);
|
||||
|
||||
// Check for open positions
|
||||
bool hasPosition = false;
|
||||
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
||||
{
|
||||
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
|
||||
{
|
||||
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
|
||||
{
|
||||
hasPosition = true;
|
||||
ManagePosition();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no position, check for entry signals
|
||||
if(!hasPosition)
|
||||
{
|
||||
// Check time filter
|
||||
if(EnableTimeFilter && !IsTimeAllowed())
|
||||
return;
|
||||
|
||||
// Check Buy Signal
|
||||
if(CheckBuySignal())
|
||||
{
|
||||
OpenBuyOrder();
|
||||
}
|
||||
// Check Sell Signal
|
||||
else if(CheckSellSignal())
|
||||
{
|
||||
OpenSellOrder();
|
||||
}
|
||||
}
|
||||
|
||||
// Display chart info
|
||||
if(ShowChartInfo)
|
||||
DisplayChartInfo();
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check if current time is allowed for trading |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsTimeAllowed()
|
||||
{
|
||||
int currentHour = Hour();
|
||||
int currentMinute = Minute();
|
||||
int currentMinutes = currentHour * 60 + currentMinute;
|
||||
int startMinutes = StartHour * 60 + StartMinute;
|
||||
int endMinutes = EndHour * 60 + EndMinute;
|
||||
|
||||
if(startMinutes < endMinutes)
|
||||
return (currentMinutes >= startMinutes && currentMinutes < endMinutes);
|
||||
else
|
||||
return (currentMinutes >= startMinutes || currentMinutes < endMinutes);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Manage existing position |
|
||||
//+------------------------------------------------------------------+
|
||||
void ManagePosition()
|
||||
{
|
||||
if(!OrderSelect(0, SELECT_BY_POS, MODE_TRADES))
|
||||
return;
|
||||
|
||||
int orderType = OrderType();
|
||||
double orderOpenPrice = OrderOpenPrice();
|
||||
double orderSL = OrderStopLoss();
|
||||
double orderTP = OrderTakeProfit();
|
||||
|
||||
// Check for opposite signal
|
||||
if(CloseOnOpposite)
|
||||
{
|
||||
if(orderType == OP_BUY && CheckSellSignal())
|
||||
{
|
||||
if(!OrderClose(OrderTicket(), OrderLots(), Bid, Slippage, clrRed))
|
||||
Print("Error closing buy order: ", GetLastError());
|
||||
return;
|
||||
}
|
||||
else if(orderType == OP_SELL && CheckBuySignal())
|
||||
{
|
||||
if(!OrderClose(OrderTicket(), OrderLots(), Ask, Slippage, clrGreen))
|
||||
Print("Error closing sell order: ", GetLastError());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
double currentPrice = (orderType == OP_BUY) ? Bid : Ask;
|
||||
double point = Point;
|
||||
double pipValue = point * 10;
|
||||
|
||||
// Breakeven
|
||||
if(EnableBreakeven)
|
||||
{
|
||||
double profit = (orderType == OP_BUY) ?
|
||||
(currentPrice - orderOpenPrice) :
|
||||
(orderOpenPrice - currentPrice);
|
||||
|
||||
if(profit >= BreakevenStart * pipValue)
|
||||
{
|
||||
double newSL = orderOpenPrice + (BreakevenOffset * pipValue *
|
||||
((orderType == OP_BUY) ? 1 : -1));
|
||||
|
||||
if((orderType == OP_BUY && newSL > orderSL) ||
|
||||
(orderType == OP_SELL && (orderSL == 0 || newSL < orderSL)))
|
||||
{
|
||||
if(!OrderModify(OrderTicket(), orderOpenPrice, newSL, orderTP, 0, clrBlue))
|
||||
Print("Error modifying order for breakeven: ", GetLastError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trailing Stop
|
||||
if(EnableTrailing)
|
||||
{
|
||||
double profit = (orderType == OP_BUY) ?
|
||||
(currentPrice - orderOpenPrice) :
|
||||
(orderOpenPrice - currentPrice);
|
||||
|
||||
if(profit >= TrailingStart * pipValue)
|
||||
{
|
||||
double newSL = currentPrice - (TrailingStep * pipValue *
|
||||
((orderType == OP_BUY) ? 1 : -1));
|
||||
|
||||
if((orderType == OP_BUY && newSL > orderSL) ||
|
||||
(orderType == OP_SELL && (orderSL == 0 || newSL < orderSL)))
|
||||
{
|
||||
if(!OrderModify(OrderTicket(), orderOpenPrice, newSL, orderTP, 0, clrBlue))
|
||||
Print("Error modifying order for trailing stop: ", GetLastError());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Display chart information |
|
||||
//+------------------------------------------------------------------+
|
||||
void DisplayChartInfo()
|
||||
{
|
||||
string info = "\\n=== " + TradeComment + " ===\\n";
|
||||
info += "Symbol: " + Symbol() + "\\n";
|
||||
info += "Magic: " + IntegerToString(MagicNumber) + "\\n";
|
||||
|
||||
bool hasPosition = false;
|
||||
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
||||
{
|
||||
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
|
||||
{
|
||||
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
|
||||
{
|
||||
hasPosition = true;
|
||||
info += "Position: " + (OrderType() == OP_BUY ? "BUY" : "SELL") + "\\n";
|
||||
info += "Profit: " + DoubleToStr(OrderProfit(), 2) + "\\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!hasPosition)
|
||||
info += "No Position\\n";
|
||||
|
||||
Comment(info);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check Buy Signal |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CheckBuySignal()
|
||||
{
|
||||
return (${buyConditions.join(' &&\n ')});
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check Sell Signal |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CheckSellSignal()
|
||||
{
|
||||
return (${sellConditions.join(' &&\n ')});
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Open Buy Order |
|
||||
//+------------------------------------------------------------------+
|
||||
void OpenBuyOrder()
|
||||
{
|
||||
double price = Ask;
|
||||
double sl = EnableHardSL ? price - (atrValue * SL_Multiplier) : 0;
|
||||
double tp = EnableHardTP ? price + (atrValue * TP_Multiplier) : 0;
|
||||
|
||||
sl = (sl > 0) ? NormalizeDouble(sl, Digits) : 0;
|
||||
tp = (tp > 0) ? NormalizeDouble(tp, Digits) : 0;
|
||||
|
||||
int ticket = OrderSend(Symbol(), OP_BUY, LotSize, price, Slippage, sl, tp,
|
||||
TradeComment, MagicNumber, 0, clrGreen);
|
||||
|
||||
if(ticket > 0)
|
||||
{
|
||||
Print("Buy order opened at ", price, " SL: ", sl, " TP: ", tp);
|
||||
}
|
||||
else
|
||||
{
|
||||
Print("Error opening buy order: ", GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Open Sell Order |
|
||||
//+------------------------------------------------------------------+
|
||||
void OpenSellOrder()
|
||||
{
|
||||
double price = Bid;
|
||||
double sl = EnableHardSL ? price + (atrValue * SL_Multiplier) : 0;
|
||||
double tp = EnableHardTP ? price - (atrValue * TP_Multiplier) : 0;
|
||||
|
||||
sl = (sl > 0) ? NormalizeDouble(sl, Digits) : 0;
|
||||
tp = (tp > 0) ? NormalizeDouble(tp, Digits) : 0;
|
||||
|
||||
int ticket = OrderSend(Symbol(), OP_SELL, LotSize, price, Slippage, sl, tp,
|
||||
TradeComment, MagicNumber, 0, clrRed);
|
||||
|
||||
if(ticket > 0)
|
||||
{
|
||||
Print("Sell order opened at ", price, " SL: ", sl, " TP: ", tp);
|
||||
}
|
||||
else
|
||||
{
|
||||
Print("Error opening sell order: ", GetLastError());
|
||||
}
|
||||
}
|
||||
//+------------------------------------------------------------------+
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
window.MQ4Converter = MQ4Converter;
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* MQ4 Code Generator - Generates MetaTrader 4 Expert Advisor code
|
||||
*/
|
||||
class MQ4Generator {
|
||||
constructor(strategy, strategyName) {
|
||||
this.strategy = strategy;
|
||||
this.name = strategyName || this.generateName();
|
||||
}
|
||||
|
||||
generateName() {
|
||||
const pf = this.strategy.metrics.profitFactor.toFixed(2).replace('.', '_');
|
||||
const wr = Math.round(this.strategy.metrics.winRate);
|
||||
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
return `FxMath_PF${pf}_WR${wr}_${timestamp}`;
|
||||
}
|
||||
|
||||
generate() {
|
||||
return `//+------------------------------------------------------------------+
|
||||
//| ${this.name}.mq4 |
|
||||
//| Generated by FxMathQuant Web |
|
||||
//| https://fxmathquant.com |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "FxMathQuant"
|
||||
#property link "https://fxmathquant.com"
|
||||
#property version "1.00"
|
||||
#property strict
|
||||
|
||||
//--- Input Parameters
|
||||
input double LotSize = 0.01; // Lot size
|
||||
input int MagicNumber = ${Math.floor(Math.random() * 9000) + 1000}; // Magic number
|
||||
input int ATR_Period = ${this.strategy.atrPeriod}; // ATR period
|
||||
input double SL_Multiplier = ${this.strategy.slMultiplier.toFixed(2)}; // Stop Loss multiplier
|
||||
input double TP_Multiplier = ${this.strategy.tpMultiplier.toFixed(2)}; // Take Profit multiplier
|
||||
input int Slippage = 3; // Slippage in points
|
||||
|
||||
//--- Global Variables
|
||||
int ticket = 0;
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Expert initialization function |
|
||||
//+------------------------------------------------------------------+
|
||||
int OnInit()
|
||||
{
|
||||
Print("${this.name} initialized");
|
||||
Print("Strategy Performance: PF=${this.strategy.metrics.profitFactor.toFixed(2)}, WR=${this.strategy.metrics.winRate.toFixed(1)}%");
|
||||
return(INIT_SUCCEEDED);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Expert deinitialization function |
|
||||
//+------------------------------------------------------------------+
|
||||
void OnDeinit(const int reason)
|
||||
{
|
||||
Print("${this.name} stopped");
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Expert tick function |
|
||||
//+------------------------------------------------------------------+
|
||||
void OnTick()
|
||||
{
|
||||
// Check if we already have an open position
|
||||
if(OrdersTotal() > 0)
|
||||
{
|
||||
for(int i = 0; i < OrdersTotal(); i++)
|
||||
{
|
||||
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
|
||||
{
|
||||
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
|
||||
return; // Position already open
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate ATR
|
||||
double atr = iATR(Symbol(), Period(), ATR_Period, 0);
|
||||
if(atr == 0) return;
|
||||
|
||||
// Check for BUY signal
|
||||
if(CheckBuySignal())
|
||||
{
|
||||
double entry = Ask;
|
||||
double sl = entry - (atr * SL_Multiplier);
|
||||
double tp = entry + (atr * TP_Multiplier);
|
||||
|
||||
// Normalize prices
|
||||
sl = NormalizeDouble(sl, Digits);
|
||||
tp = NormalizeDouble(tp, Digits);
|
||||
|
||||
// Open BUY order
|
||||
ticket = OrderSend(Symbol(), OP_BUY, LotSize, entry, Slippage, sl, tp,
|
||||
"${this.name}", MagicNumber, 0, clrGreen);
|
||||
|
||||
if(ticket > 0)
|
||||
Print("BUY order opened: ", ticket, " @ ", entry, " SL:", sl, " TP:", tp);
|
||||
else
|
||||
Print("Error opening BUY order: ", GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check BUY signal based on strategy rules |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CheckBuySignal()
|
||||
{
|
||||
${this.generateRulesCode()}
|
||||
|
||||
return true; // All rules passed
|
||||
}
|
||||
|
||||
${this.generateHelperFunctions()}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
`;
|
||||
}
|
||||
|
||||
generateRulesCode() {
|
||||
return this.strategy.rules.map((rule, index) => {
|
||||
const condition = this.ruleToMQ4(rule);
|
||||
return ` // Rule ${index + 1}
|
||||
if(!(${condition})) return false;`;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
ruleToMQ4(rule) {
|
||||
if (rule.type === 'simple') {
|
||||
const left = this.priceToMQ4(rule.left.price, rule.left.shift);
|
||||
const right = this.priceToMQ4(rule.right.price, rule.right.shift);
|
||||
return `${left} ${rule.operator} ${right}`;
|
||||
} else {
|
||||
// Arithmetic rule
|
||||
const left1 = this.priceToMQ4(rule.left.price1, rule.left.shift1);
|
||||
const left2 = this.priceToMQ4(rule.left.price2, rule.left.shift2);
|
||||
const right = this.priceToMQ4(rule.right.price, rule.right.shift);
|
||||
|
||||
return `(${left1} ${rule.left.op} ${left2}) ${rule.operator} (${right} * ${rule.right.multiplier})`;
|
||||
}
|
||||
}
|
||||
|
||||
priceToMQ4(priceType, shift) {
|
||||
const type = priceType.charAt(0).toUpperCase() + priceType.slice(1);
|
||||
return `i${type}(Symbol(), Period(), ${shift})`;
|
||||
}
|
||||
|
||||
generateHelperFunctions() {
|
||||
return `//+------------------------------------------------------------------+
|
||||
//| Helper Functions |
|
||||
//+------------------------------------------------------------------+
|
||||
// Add any additional helper functions here
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// MQ5 Converter
|
||||
class MQ5Converter {
|
||||
constructor(strategy) {
|
||||
this.strategy = strategy;
|
||||
this.parser = new RuleParser();
|
||||
}
|
||||
|
||||
generate() {
|
||||
const { parameters, buy_rules, sell_rules } = this.strategy;
|
||||
const symbol = parameters.symbol || 'EURUSD';
|
||||
const atrPeriod = parameters.atr_period || 14;
|
||||
const slMultiplier = parameters.sl_multiplier || 2.0;
|
||||
const tpMultiplier = parameters.tp_multiplier || 3.0;
|
||||
|
||||
// Generate random 6-digit magic number
|
||||
const magicNumber = Math.floor(100000 + Math.random() * 900000);
|
||||
|
||||
const buyConditions = this.parser.parseRules(buy_rules, 'mq5');
|
||||
const sellConditions = this.parser.parseRules(sell_rules, 'mq5');
|
||||
|
||||
return `//+------------------------------------------------------------------+
|
||||
//| Strategy_${symbol}.mq5 |
|
||||
//| Generated by Strategy Converter |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "FxMath Quant"
|
||||
#property link "https://fxmath.com"
|
||||
#property version "1.00"
|
||||
|
||||
#include <Trade\\Trade.mqh>
|
||||
|
||||
//=== TRADING SETTINGS ===
|
||||
input group "=== Trade Settings ==="
|
||||
input double LotSize = 0.1; // Lot Size
|
||||
input int ATR_Period = ${atrPeriod}; // ATR Period
|
||||
input double SL_Multiplier = ${slMultiplier.toFixed(2)}; // SL ATR Multiplier
|
||||
input double TP_Multiplier = ${tpMultiplier.toFixed(2)}; // TP ATR Multiplier
|
||||
input ulong MagicNumber = ${magicNumber}; // Magic Number
|
||||
input string TradeComment = "FxMath EA"; // Trade Comment
|
||||
input int Slippage = 3; // Slippage
|
||||
|
||||
//=== SL/TP SETTINGS ===
|
||||
input group "=== SL/TP Settings ==="
|
||||
input bool EnableHardSL = true; // Enable Hard Stop Loss
|
||||
input bool EnableHardTP = true; // Enable Hard Take Profit
|
||||
|
||||
//=== TRAILING STOP ===
|
||||
input group "=== Trailing Stop ==="
|
||||
input bool EnableTrailing = false; // Enable Trailing Stop
|
||||
input double TrailingStart = 30; // Trailing Start (pips)
|
||||
input double TrailingStep = 10; // Trailing Step (pips)
|
||||
|
||||
//=== BREAKEVEN ===
|
||||
input group "=== Breakeven ==="
|
||||
input bool EnableBreakeven = false; // Enable Breakeven
|
||||
input double BreakevenStart = 20; // Breakeven Start (pips)
|
||||
input double BreakevenOffset = 2; // Breakeven Offset (pips)
|
||||
|
||||
//=== TIME FILTER ===
|
||||
input group "=== Time Filter ==="
|
||||
input bool EnableTimeFilter = false; // Enable Time Filter
|
||||
input int StartHour = 8; // Start Hour (Server Time)
|
||||
input int StartMinute = 0; // Start Minute
|
||||
input int EndHour = 22; // End Hour (Server Time)
|
||||
input int EndMinute = 0; // End Minute
|
||||
|
||||
//=== SIGNAL SETTINGS ===
|
||||
input group "=== Signal Settings ==="
|
||||
input bool CloseOnOpposite = true; // Close Trade on Opposite Signal
|
||||
|
||||
//=== DISPLAY SETTINGS ===
|
||||
input group "=== Display Settings ==="
|
||||
input bool ShowChartInfo = true; // Show Info on Chart
|
||||
|
||||
// Global Variables
|
||||
CTrade trade;
|
||||
int atrHandle;
|
||||
double atrBuffer[];
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Expert initialization function |
|
||||
//+------------------------------------------------------------------+
|
||||
int OnInit()
|
||||
{
|
||||
// Set trade parameters
|
||||
trade.SetExpertMagicNumber(MagicNumber);
|
||||
trade.SetDeviationInPoints(Slippage);
|
||||
trade.SetTypeFilling(ORDER_FILLING_FOK);
|
||||
|
||||
// Create ATR indicator handle
|
||||
atrHandle = iATR(_Symbol, PERIOD_CURRENT, ATR_Period);
|
||||
|
||||
if(atrHandle == INVALID_HANDLE)
|
||||
{
|
||||
Print("Error creating ATR indicator");
|
||||
return(INIT_FAILED);
|
||||
}
|
||||
|
||||
ArraySetAsSeries(atrBuffer, true);
|
||||
|
||||
Print("Strategy EA initialized for ${symbol}");
|
||||
Print("Magic Number: ", MagicNumber);
|
||||
return(INIT_SUCCEEDED);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Expert deinitialization function |
|
||||
//+------------------------------------------------------------------+
|
||||
void OnDeinit(const int reason)
|
||||
{
|
||||
if(atrHandle != INVALID_HANDLE)
|
||||
IndicatorRelease(atrHandle);
|
||||
|
||||
Print("Strategy EA deinitialized");
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Expert tick function |
|
||||
//+------------------------------------------------------------------+
|
||||
void OnTick()
|
||||
{
|
||||
// Copy ATR values
|
||||
if(CopyBuffer(atrHandle, 0, 0, 2, atrBuffer) < 2)
|
||||
{
|
||||
Print("Error copying ATR buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
double atrValue = atrBuffer[1];
|
||||
|
||||
// Check if we have an open position
|
||||
if(PositionSelect(_Symbol))
|
||||
{
|
||||
// Position exists - manage it
|
||||
ManagePosition(atrValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No position - check for entry signals
|
||||
|
||||
// Check time filter
|
||||
if(EnableTimeFilter && !IsTimeAllowed())
|
||||
return;
|
||||
|
||||
// Check Buy Signal
|
||||
if(CheckBuySignal())
|
||||
{
|
||||
OpenBuyOrder(atrValue);
|
||||
}
|
||||
// Check Sell Signal
|
||||
else if(CheckSellSignal())
|
||||
{
|
||||
OpenSellOrder(atrValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Display chart info
|
||||
if(ShowChartInfo)
|
||||
DisplayChartInfo();
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check if current time is allowed for trading |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsTimeAllowed()
|
||||
{
|
||||
MqlDateTime dt;
|
||||
TimeToStruct(TimeCurrent(), dt);
|
||||
|
||||
int currentMinutes = dt.hour * 60 + dt.min;
|
||||
int startMinutes = StartHour * 60 + StartMinute;
|
||||
int endMinutes = EndHour * 60 + EndMinute;
|
||||
|
||||
if(startMinutes < endMinutes)
|
||||
return (currentMinutes >= startMinutes && currentMinutes < endMinutes);
|
||||
else
|
||||
return (currentMinutes >= startMinutes || currentMinutes < endMinutes);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Manage existing position |
|
||||
//+------------------------------------------------------------------+
|
||||
void ManagePosition(double atrValue)
|
||||
{
|
||||
if(!PositionSelect(_Symbol))
|
||||
return;
|
||||
|
||||
long posType = PositionGetInteger(POSITION_TYPE);
|
||||
double posOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN);
|
||||
double posSL = PositionGetDouble(POSITION_SL);
|
||||
double posTP = PositionGetDouble(POSITION_TP);
|
||||
|
||||
// Check for opposite signal
|
||||
if(CloseOnOpposite)
|
||||
{
|
||||
if(posType == POSITION_TYPE_BUY && CheckSellSignal())
|
||||
{
|
||||
trade.PositionClose(_Symbol);
|
||||
return;
|
||||
}
|
||||
else if(posType == POSITION_TYPE_SELL && CheckBuySignal())
|
||||
{
|
||||
trade.PositionClose(_Symbol);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
double currentPrice = (posType == POSITION_TYPE_BUY) ?
|
||||
SymbolInfoDouble(_Symbol, SYMBOL_BID) :
|
||||
SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
||||
|
||||
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
|
||||
double pipValue = point * 10;
|
||||
|
||||
// Breakeven
|
||||
if(EnableBreakeven)
|
||||
{
|
||||
double profit = (posType == POSITION_TYPE_BUY) ?
|
||||
(currentPrice - posOpenPrice) :
|
||||
(posOpenPrice - currentPrice);
|
||||
|
||||
if(profit >= BreakevenStart * pipValue)
|
||||
{
|
||||
double newSL = posOpenPrice + (BreakevenOffset * pipValue *
|
||||
((posType == POSITION_TYPE_BUY) ? 1 : -1));
|
||||
|
||||
if((posType == POSITION_TYPE_BUY && newSL > posSL) ||
|
||||
(posType == POSITION_TYPE_SELL && (posSL == 0 || newSL < posSL)))
|
||||
{
|
||||
trade.PositionModify(_Symbol, newSL, posTP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trailing Stop
|
||||
if(EnableTrailing)
|
||||
{
|
||||
double profit = (posType == POSITION_TYPE_BUY) ?
|
||||
(currentPrice - posOpenPrice) :
|
||||
(posOpenPrice - currentPrice);
|
||||
|
||||
if(profit >= TrailingStart * pipValue)
|
||||
{
|
||||
double newSL = currentPrice - (TrailingStep * pipValue *
|
||||
((posType == POSITION_TYPE_BUY) ? 1 : -1));
|
||||
|
||||
if((posType == POSITION_TYPE_BUY && newSL > posSL) ||
|
||||
(posType == POSITION_TYPE_SELL && (posSL == 0 || newSL < posSL)))
|
||||
{
|
||||
trade.PositionModify(_Symbol, newSL, posTP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Display chart information |
|
||||
//+------------------------------------------------------------------+
|
||||
void DisplayChartInfo()
|
||||
{
|
||||
string info = "\\n=== " + TradeComment + " ===\\n";
|
||||
info += "Symbol: " + _Symbol + "\\n";
|
||||
info += "Magic: " + IntegerToString(MagicNumber) + "\\n";
|
||||
|
||||
if(PositionSelect(_Symbol))
|
||||
{
|
||||
info += "Position: " + EnumToString((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE)) + "\\n";
|
||||
info += "Profit: " + DoubleToString(PositionGetDouble(POSITION_PROFIT), 2) + "\\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
info += "No Position\\n";
|
||||
}
|
||||
|
||||
Comment(info);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check Buy Signal |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CheckBuySignal()
|
||||
{
|
||||
return (${buyConditions.join(' &&\n ')});
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check Sell Signal |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CheckSellSignal()
|
||||
{
|
||||
return (${sellConditions.join(' &&\n ')});
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Open Buy Order |
|
||||
//+------------------------------------------------------------------+
|
||||
void OpenBuyOrder(double atrValue)
|
||||
{
|
||||
double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
||||
double sl = EnableHardSL ? price - (atrValue * SL_Multiplier) : 0;
|
||||
double tp = EnableHardTP ? price + (atrValue * TP_Multiplier) : 0;
|
||||
|
||||
sl = (sl > 0) ? NormalizeDouble(sl, _Digits) : 0;
|
||||
tp = (tp > 0) ? NormalizeDouble(tp, _Digits) : 0;
|
||||
|
||||
if(trade.Buy(LotSize, _Symbol, price, sl, tp, TradeComment))
|
||||
{
|
||||
Print("Buy order opened at ", price, " SL: ", sl, " TP: ", tp);
|
||||
}
|
||||
else
|
||||
{
|
||||
Print("Error opening buy order: ", GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Open Sell Order |
|
||||
//+------------------------------------------------------------------+
|
||||
void OpenSellOrder(double atrValue)
|
||||
{
|
||||
double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
||||
double sl = EnableHardSL ? price + (atrValue * SL_Multiplier) : 0;
|
||||
double tp = EnableHardTP ? price - (atrValue * TP_Multiplier) : 0;
|
||||
|
||||
sl = (sl > 0) ? NormalizeDouble(sl, _Digits) : 0;
|
||||
tp = (tp > 0) ? NormalizeDouble(tp, _Digits) : 0;
|
||||
|
||||
if(trade.Sell(LotSize, _Symbol, price, sl, tp, TradeComment))
|
||||
{
|
||||
Print("Sell order opened at ", price, " SL: ", sl, " TP: ", tp);
|
||||
}
|
||||
else
|
||||
{
|
||||
Print("Error opening sell order: ", GetLastError());
|
||||
}
|
||||
}
|
||||
//+------------------------------------------------------------------+
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
window.MQ5Converter = MQ5Converter;
|
||||
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* MQ5 Code Generator - Generates MetaTrader 5 Expert Advisor code
|
||||
*/
|
||||
class MQ5Generator {
|
||||
constructor(strategy, strategyName) {
|
||||
this.strategy = strategy;
|
||||
this.name = strategyName || this.generateName();
|
||||
}
|
||||
|
||||
generateName() {
|
||||
const pf = this.strategy.metrics.profitFactor.toFixed(2).replace('.', '_');
|
||||
const wr = Math.round(this.strategy.metrics.winRate);
|
||||
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
return `FxMath_PF${pf}_WR${wr}_${timestamp}`;
|
||||
}
|
||||
|
||||
generate() {
|
||||
return `//+------------------------------------------------------------------+
|
||||
//| ${this.name}.mq5 |
|
||||
//| Generated by FxMathQuant Web |
|
||||
//| https://fxmathquant.com |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "FxMathQuant"
|
||||
#property link "https://fxmathquant.com"
|
||||
#property version "1.00"
|
||||
|
||||
//--- Include libraries
|
||||
#include <Trade\\Trade.mqh>
|
||||
|
||||
//--- Input Parameters
|
||||
input double LotSize = 0.01; // Lot size
|
||||
input int MagicNumber = ${Math.floor(Math.random() * 9000) + 1000}; // Magic number
|
||||
input int ATR_Period = ${this.strategy.atrPeriod}; // ATR period
|
||||
input double SL_Multiplier = ${this.strategy.slMultiplier.toFixed(2)}; // Stop Loss multiplier
|
||||
input double TP_Multiplier = ${this.strategy.tpMultiplier.toFixed(2)}; // Take Profit multiplier
|
||||
input int Slippage = 3; // Slippage in points
|
||||
|
||||
//--- Global Variables
|
||||
CTrade trade;
|
||||
int atrHandle;
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Expert initialization function |
|
||||
//+------------------------------------------------------------------+
|
||||
int OnInit()
|
||||
{
|
||||
// Set magic number
|
||||
trade.SetExpertMagicNumber(MagicNumber);
|
||||
trade.SetDeviationInPoints(Slippage);
|
||||
|
||||
// Create ATR indicator handle
|
||||
atrHandle = iATR(_Symbol, _Period, ATR_Period);
|
||||
if(atrHandle == INVALID_HANDLE)
|
||||
{
|
||||
Print("Error creating ATR indicator");
|
||||
return(INIT_FAILED);
|
||||
}
|
||||
|
||||
Print("${this.name} initialized");
|
||||
Print("Strategy Performance: PF=${this.strategy.metrics.profitFactor.toFixed(2)}, WR=${this.strategy.metrics.winRate.toFixed(1)}%");
|
||||
|
||||
return(INIT_SUCCEEDED);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Expert deinitialization function |
|
||||
//+------------------------------------------------------------------+
|
||||
void OnDeinit(const int reason)
|
||||
{
|
||||
// Release indicator handle
|
||||
if(atrHandle != INVALID_HANDLE)
|
||||
IndicatorRelease(atrHandle);
|
||||
|
||||
Print("${this.name} stopped");
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Expert tick function |
|
||||
//+------------------------------------------------------------------+
|
||||
void OnTick()
|
||||
{
|
||||
// Check if we already have an open position
|
||||
if(PositionSelect(_Symbol))
|
||||
{
|
||||
if(PositionGetInteger(POSITION_MAGIC) == MagicNumber)
|
||||
return; // Position already open
|
||||
}
|
||||
|
||||
// Get ATR value
|
||||
double atrBuffer[];
|
||||
ArraySetAsSeries(atrBuffer, true);
|
||||
if(CopyBuffer(atrHandle, 0, 0, 1, atrBuffer) <= 0)
|
||||
return;
|
||||
|
||||
double atr = atrBuffer[0];
|
||||
if(atr == 0) return;
|
||||
|
||||
// Check for BUY signal
|
||||
if(CheckBuySignal())
|
||||
{
|
||||
double entry = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
||||
double sl = entry - (atr * SL_Multiplier);
|
||||
double tp = entry + (atr * TP_Multiplier);
|
||||
|
||||
// Normalize prices
|
||||
sl = NormalizeDouble(sl, _Digits);
|
||||
tp = NormalizeDouble(tp, _Digits);
|
||||
|
||||
// Open BUY position
|
||||
if(trade.Buy(LotSize, _Symbol, entry, sl, tp, "${this.name}"))
|
||||
Print("BUY position opened @ ", entry, " SL:", sl, " TP:", tp);
|
||||
else
|
||||
Print("Error opening BUY position: ", trade.ResultRetcode());
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| Check BUY signal based on strategy rules |
|
||||
//+------------------------------------------------------------------+
|
||||
bool CheckBuySignal()
|
||||
{
|
||||
${this.generateRulesCode()}
|
||||
|
||||
return true; // All rules passed
|
||||
}
|
||||
|
||||
${this.generateHelperFunctions()}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
`;
|
||||
}
|
||||
|
||||
generateRulesCode() {
|
||||
return this.strategy.rules.map((rule, index) => {
|
||||
const condition = this.ruleToMQ5(rule);
|
||||
return ` // Rule ${index + 1}
|
||||
if(!(${condition})) return false;`;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
ruleToMQ5(rule) {
|
||||
if (rule.type === 'simple') {
|
||||
const left = this.priceToMQ5(rule.left.price, rule.left.shift);
|
||||
const right = this.priceToMQ5(rule.right.price, rule.right.shift);
|
||||
return `${left} ${rule.operator} ${right}`;
|
||||
} else {
|
||||
// Arithmetic rule
|
||||
const left1 = this.priceToMQ5(rule.left.price1, rule.left.shift1);
|
||||
const left2 = this.priceToMQ5(rule.left.price2, rule.left.shift2);
|
||||
const right = this.priceToMQ5(rule.right.price, rule.right.shift);
|
||||
|
||||
return `(${left1} ${rule.left.op} ${left2}) ${rule.operator} (${right} * ${rule.right.multiplier})`;
|
||||
}
|
||||
}
|
||||
|
||||
priceToMQ5(priceType, shift) {
|
||||
const type = priceType.charAt(0).toUpperCase() + priceType.slice(1).toLowerCase();
|
||||
return `iClose(_Symbol, _Period, ${shift})`.replace('Close', type);
|
||||
}
|
||||
|
||||
generateHelperFunctions() {
|
||||
return `//+------------------------------------------------------------------+
|
||||
//| Get price value at specific shift |
|
||||
//+------------------------------------------------------------------+
|
||||
double iOpen(string symbol, ENUM_TIMEFRAMES timeframe, int shift)
|
||||
{
|
||||
double buffer[];
|
||||
ArraySetAsSeries(buffer, true);
|
||||
if(CopyOpen(symbol, timeframe, shift, 1, buffer) > 0)
|
||||
return buffer[0];
|
||||
return 0;
|
||||
}
|
||||
|
||||
double iHigh(string symbol, ENUM_TIMEFRAMES timeframe, int shift)
|
||||
{
|
||||
double buffer[];
|
||||
ArraySetAsSeries(buffer, true);
|
||||
if(CopyHigh(symbol, timeframe, shift, 1, buffer) > 0)
|
||||
return buffer[0];
|
||||
return 0;
|
||||
}
|
||||
|
||||
double iLow(string symbol, ENUM_TIMEFRAMES timeframe, int shift)
|
||||
{
|
||||
double buffer[];
|
||||
ArraySetAsSeries(buffer, true);
|
||||
if(CopyLow(symbol, timeframe, shift, 1, buffer) > 0)
|
||||
return buffer[0];
|
||||
return 0;
|
||||
}
|
||||
|
||||
double iClose(string symbol, ENUM_TIMEFRAMES timeframe, int shift)
|
||||
{
|
||||
double buffer[];
|
||||
ArraySetAsSeries(buffer, true);
|
||||
if(CopyClose(symbol, timeframe, shift, 1, buffer) > 0)
|
||||
return buffer[0];
|
||||
return 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Pine Script Converter
|
||||
class PineConverter {
|
||||
constructor(strategy) {
|
||||
this.strategy = strategy;
|
||||
this.parser = new RuleParser();
|
||||
}
|
||||
|
||||
generate() {
|
||||
const { parameters, buy_rules, sell_rules } = this.strategy;
|
||||
const symbol = parameters.symbol || 'EURUSD';
|
||||
const atrPeriod = parameters.atr_period || 14;
|
||||
const slMultiplier = parameters.sl_multiplier || 2.0;
|
||||
const tpMultiplier = parameters.tp_multiplier || 3.0;
|
||||
|
||||
const buyConditions = this.parser.parseRules(buy_rules, 'pine');
|
||||
const sellConditions = this.parser.parseRules(sell_rules, 'pine');
|
||||
|
||||
return `//@version=5
|
||||
strategy("Strategy ${symbol}", overlay=true,
|
||||
initial_capital=10000,
|
||||
default_qty_type=strategy.percent_of_equity,
|
||||
default_qty_value=100,
|
||||
commission_type=strategy.commission.percent,
|
||||
commission_value=0.1)
|
||||
|
||||
// Input Parameters
|
||||
lotSize = input.float(0.01, "Lot Size", minval=0.01, step=0.01)
|
||||
atrPeriod = input.int(${atrPeriod}, "ATR Period", minval=1)
|
||||
slMultiplier = input.float(${slMultiplier.toFixed(2)}, "Stop Loss Multiplier", minval=0.1, step=0.1)
|
||||
tpMultiplier = input.float(${tpMultiplier.toFixed(2)}, "Take Profit Multiplier", minval=0.1, step=0.1)
|
||||
|
||||
// Calculate ATR
|
||||
atrValue = ta.atr(atrPeriod)
|
||||
|
||||
// Buy Signal Conditions
|
||||
buySignal = ${buyConditions.join(' and\n ')}
|
||||
|
||||
// Sell Signal Conditions
|
||||
sellSignal = ${sellConditions.join(' and\n ')}
|
||||
|
||||
// Calculate Stop Loss and Take Profit levels
|
||||
longStopLoss = close - (atrValue * slMultiplier)
|
||||
longTakeProfit = close + (atrValue * tpMultiplier)
|
||||
shortStopLoss = close + (atrValue * slMultiplier)
|
||||
shortTakeProfit = close - (atrValue * tpMultiplier)
|
||||
|
||||
// Strategy Entry and Exit
|
||||
if (buySignal)
|
||||
strategy.entry("Long", strategy.long, comment="Buy Signal")
|
||||
strategy.exit("Long Exit", "Long", stop=longStopLoss, limit=longTakeProfit)
|
||||
|
||||
if (sellSignal)
|
||||
strategy.entry("Short", strategy.short, comment="Sell Signal")
|
||||
strategy.exit("Short Exit", "Short", stop=shortStopLoss, limit=shortTakeProfit)
|
||||
|
||||
// Plot Buy and Sell signals
|
||||
plotshape(buySignal, title="Buy Signal", location=location.belowbar,
|
||||
color=color.green, style=shape.triangleup, size=size.small)
|
||||
plotshape(sellSignal, title="Sell Signal", location=location.abovebar,
|
||||
color=color.red, style=shape.triangledown, size=size.small)
|
||||
|
||||
// Plot Stop Loss and Take Profit levels
|
||||
plot(strategy.position_size > 0 ? longStopLoss : na,
|
||||
title="Long SL", color=color.red, style=plot.style_linebr, linewidth=1)
|
||||
plot(strategy.position_size > 0 ? longTakeProfit : na,
|
||||
title="Long TP", color=color.green, style=plot.style_linebr, linewidth=1)
|
||||
plot(strategy.position_size < 0 ? shortStopLoss : na,
|
||||
title="Short SL", color=color.red, style=plot.style_linebr, linewidth=1)
|
||||
plot(strategy.position_size < 0 ? shortTakeProfit : na,
|
||||
title="Short TP", color=color.green, style=plot.style_linebr, linewidth=1)
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
window.PineConverter = PineConverter;
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* HTML Report Generator - Creates interactive performance reports
|
||||
*/
|
||||
class ReportGenerator {
|
||||
constructor(strategy, strategyName) {
|
||||
this.strategy = strategy;
|
||||
this.name = strategyName || 'Strategy Report';
|
||||
}
|
||||
|
||||
generate() {
|
||||
const m = this.strategy.metrics;
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${this.name} - Performance Report</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, sans-serif;
|
||||
background: linear-gradient(135deg, #0a0e27 0%, #1a1f3a 100%);
|
||||
color: #ffffff;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
h1 { font-size: 32px; margin-bottom: 10px; color: #667eea; }
|
||||
.subtitle { color: #a0aec0; margin-bottom: 30px; }
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.metric-card {
|
||||
background: #1a1f3a;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #2d3748;
|
||||
}
|
||||
.metric-label {
|
||||
font-size: 12px;
|
||||
color: #718096;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.metric-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.positive { color: #48bb78; }
|
||||
.negative { color: #f56565; }
|
||||
.neutral { color: #667eea; }
|
||||
.chart-container {
|
||||
background: #1a1f3a;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #2d3748;
|
||||
}
|
||||
.rules-section {
|
||||
background: #1a1f3a;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #2d3748;
|
||||
}
|
||||
.rule {
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
background: #0a0e27;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #1a1f3a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #2d3748;
|
||||
}
|
||||
th {
|
||||
background: #0a0e27;
|
||||
color: #a0aec0;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
}
|
||||
tr:hover { background: #252b4a; }
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>${this.name}</h1>
|
||||
<p class="subtitle">Generated by FxMathQuant Web - AI-Powered Strategy Generator</p>
|
||||
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Profit Factor</div>
|
||||
<div class="metric-value ${m.profitFactor >= 1.5 ? 'positive' : 'negative'}">
|
||||
${m.profitFactor.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Win Rate</div>
|
||||
<div class="metric-value neutral">${m.winRate.toFixed(1)}%</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Total Trades</div>
|
||||
<div class="metric-value neutral">${m.totalTrades}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Max Drawdown</div>
|
||||
<div class="metric-value negative">${m.maxDrawdown.toFixed(2)}%</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Total Profit</div>
|
||||
<div class="metric-value ${m.totalProfit >= 0 ? 'positive' : 'negative'}">
|
||||
$${m.totalProfit.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Final Balance</div>
|
||||
<div class="metric-value positive">$${m.finalBalance.toFixed(2)}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Avg Win</div>
|
||||
<div class="metric-value positive">$${m.avgWin.toFixed(2)}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Avg Loss</div>
|
||||
<div class="metric-value negative">$${Math.abs(m.avgLoss).toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<h2 style="margin-bottom: 20px;">Equity Curve</h2>
|
||||
<canvas id="equityChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="rules-section">
|
||||
<h2 style="margin-bottom: 20px;">Strategy Rules</h2>
|
||||
<p style="margin-bottom: 15px; color: #a0aec0;">BUY when ALL of the following conditions are true:</p>
|
||||
${this.generateRulesHTML()}
|
||||
<div style="margin-top: 20px; padding: 15px; background: #0a0e27; border-radius: 6px;">
|
||||
<strong>Parameters:</strong><br>
|
||||
ATR Period: ${this.strategy.atrPeriod} |
|
||||
SL Multiplier: ${this.strategy.slMultiplier.toFixed(2)} |
|
||||
TP Multiplier: ${this.strategy.tpMultiplier.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.generateTradesTable()}
|
||||
|
||||
<div class="footer">
|
||||
<p>© 2025 FxMathQuant. All rights reserved.</p>
|
||||
<p>This report was generated automatically by AI-powered genetic algorithms.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Equity Curve Chart
|
||||
const ctx = document.getElementById('equityChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ${JSON.stringify(m.equity.map((_, i) => i))},
|
||||
datasets: [{
|
||||
label: 'Account Balance',
|
||||
data: ${JSON.stringify(m.equity)},
|
||||
borderColor: '#667eea',
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: '#1a1f3a',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#a0aec0',
|
||||
borderColor: '#2d3748',
|
||||
borderWidth: 1
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
grid: { color: '#2d3748' },
|
||||
ticks: { color: '#a0aec0' }
|
||||
},
|
||||
x: {
|
||||
grid: { color: '#2d3748' },
|
||||
ticks: { color: '#a0aec0' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
generateRulesHTML() {
|
||||
return this.strategy.rules.map((rule, index) => {
|
||||
let ruleText = '';
|
||||
if (rule.type === 'simple') {
|
||||
ruleText = `${rule.left.price.toUpperCase()}[${rule.left.shift}] ${rule.operator} ${rule.right.price.toUpperCase()}[${rule.right.shift}]`;
|
||||
} else {
|
||||
ruleText = `(${rule.left.price1.toUpperCase()}[${rule.left.shift1}] ${rule.left.op} ${rule.left.price2.toUpperCase()}[${rule.left.shift2}]) ${rule.operator} (${rule.right.price.toUpperCase()}[${rule.right.shift}] × ${rule.right.multiplier})`;
|
||||
}
|
||||
return `<div class="rule">${index + 1}. ${ruleText}</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
generateTradesTable() {
|
||||
const trades = this.strategy.metrics.trades || [];
|
||||
if (trades.length === 0) return '';
|
||||
|
||||
// Show first 50 trades
|
||||
const displayTrades = trades.slice(0, 50);
|
||||
|
||||
return `
|
||||
<div style="background: #1a1f3a; padding: 30px; border-radius: 12px; border: 1px solid #2d3748;">
|
||||
<h2 style="margin-bottom: 20px;">Trade History ${trades.length > 50 ? '(First 50 trades)' : ''}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Type</th>
|
||||
<th>Entry</th>
|
||||
<th>Exit</th>
|
||||
<th>Profit</th>
|
||||
<th>Reason</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${displayTrades.map((trade, i) => `
|
||||
<tr>
|
||||
<td>${i + 1}</td>
|
||||
<td>${trade.type}</td>
|
||||
<td>${trade.entry.toFixed(5)}</td>
|
||||
<td>${trade.exit.toFixed(5)}</td>
|
||||
<td class="${trade.profit >= 0 ? 'positive' : 'negative'}">
|
||||
$${trade.profit.toFixed(2)}
|
||||
</td>
|
||||
<td>${trade.reason.replace('_', ' ')}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Rule Parser - Converts strategy rule objects to platform-specific syntax
|
||||
class RuleParser {
|
||||
constructor() { }
|
||||
|
||||
parseRules(rules, platform = 'mql') {
|
||||
return rules.map(rule => this.parseRule(rule, platform));
|
||||
}
|
||||
|
||||
parseRule(rule, platform = 'mql') {
|
||||
if (rule.type === 'simple') {
|
||||
return this.parseSimpleRule(rule, platform);
|
||||
} else {
|
||||
return this.parseArithmeticRule(rule, platform);
|
||||
}
|
||||
}
|
||||
|
||||
parseSimpleRule(rule, platform) {
|
||||
const left = this.getPriceReference(rule.left.price, rule.left.shift, platform);
|
||||
const right = this.getPriceReference(rule.right.price, rule.right.shift, platform);
|
||||
const operator = rule.operator;
|
||||
|
||||
return `${left} ${operator} ${right}`;
|
||||
}
|
||||
|
||||
parseArithmeticRule(rule, platform) {
|
||||
const left1 = this.getPriceReference(rule.left.price1, rule.left.shift1, platform);
|
||||
const left2 = this.getPriceReference(rule.left.price2, rule.left.shift2, platform);
|
||||
const leftOp = rule.left.op;
|
||||
const right = this.getPriceReference(rule.right.price, rule.right.shift, platform);
|
||||
const multiplier = rule.right.multiplier;
|
||||
const operator = rule.operator;
|
||||
|
||||
return `(${left1} ${leftOp} ${left2}) ${operator} (${right} * ${multiplier})`;
|
||||
}
|
||||
|
||||
getPriceReference(priceType, shift, platform) {
|
||||
const price = priceType.charAt(0).toUpperCase() + priceType.slice(1).toLowerCase();
|
||||
|
||||
if (platform === 'mql') {
|
||||
// MQ4 style
|
||||
return `${price}[${shift}]`;
|
||||
} else if (platform === 'mq5') {
|
||||
// MQ5 style
|
||||
return `i${price}(_Symbol, PERIOD_CURRENT, ${shift})`;
|
||||
} else if (platform === 'pine') {
|
||||
// Pine Script style
|
||||
const priceLower = priceType.toLowerCase();
|
||||
return shift === 0 ? priceLower : `${priceLower}[${shift}]`;
|
||||
} else if (platform === 'csharp') {
|
||||
// cTrader C# style
|
||||
return `MarketSeries.${price}.Last(${shift})`;
|
||||
}
|
||||
|
||||
return `${price}[${shift}]`;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
window.RuleParser = RuleParser;
|
||||
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* View detailed strategy information in a modal
|
||||
*/
|
||||
function viewStrategyDetails(index) {
|
||||
console.log('🔍 viewStrategyDetails called with index:', index);
|
||||
console.log('📊 appData.foundStrategies:', appData?.foundStrategies);
|
||||
|
||||
if (!appData || !appData.foundStrategies) {
|
||||
console.error('❌ appData or foundStrategies is undefined!');
|
||||
alert('Error: Strategy data not available');
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= appData.foundStrategies.length) {
|
||||
console.error('❌ Invalid index:', index, 'Length:', appData.foundStrategies.length);
|
||||
alert('Error: Invalid strategy index');
|
||||
return;
|
||||
}
|
||||
|
||||
const strategy = appData.foundStrategies[index];
|
||||
if (!strategy || !strategy.metrics) {
|
||||
console.error('❌ Invalid strategy at index:', index);
|
||||
alert('Error: Strategy data is corrupted');
|
||||
return;
|
||||
}
|
||||
|
||||
const m = strategy.metrics;
|
||||
|
||||
// Additional validation for required metrics
|
||||
if (!m.profitFactor || !m.winRate || !m.totalTrades) {
|
||||
console.error('❌ Missing required metrics:', m);
|
||||
alert('Error: Strategy metrics are incomplete');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create modal if it doesn't exist
|
||||
let modal = document.getElementById('strategy-modal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'strategy-modal';
|
||||
modal.className = 'modal';
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
const name = `FxMath_${String(index + 1).padStart(3, '0')}_PF${(m.profitFactor || 0).toFixed(2).replace('.', '_')}_WR${Math.round(m.winRate || 0)}`;
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>${name}</h2>
|
||||
<button class="modal-close" onclick="closeStrategyModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="metrics-grid-detailed">
|
||||
<div class="metric-detailed">
|
||||
<span class="metric-label">Profit Factor</span>
|
||||
<span class="metric-value ${m.profitFactor >= 1.5 ? 'positive' : 'negative'}">${m.profitFactor.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="metric-detailed">
|
||||
<span class="metric-label">Win Rate</span>
|
||||
<span class="metric-value">${m.winRate.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div class="metric-detailed">
|
||||
<span class="metric-label">Total Trades</span>
|
||||
<span class="metric-value">${m.totalTrades}</span>
|
||||
</div>
|
||||
<div class="metric-detailed">
|
||||
<span class="metric-label">BUY Trades</span>
|
||||
<span class="metric-value" style="color: #48bb78;">${m.buyTrades || 0}</span>
|
||||
</div>
|
||||
<div class="metric-detailed">
|
||||
<span class="metric-label">SELL Trades</span>
|
||||
<span class="metric-value" style="color: #f56565;">${m.sellTrades || 0}</span>
|
||||
</div>
|
||||
<div class="metric-detailed">
|
||||
<span class="metric-label">Winning Trades</span>
|
||||
<span class="metric-value positive">${m.winningTrades || 0}</span>
|
||||
</div>
|
||||
<div class="metric-detailed">
|
||||
<span class="metric-label">Losing Trades</span>
|
||||
<span class="metric-value negative">${m.losingTrades || 0}</span>
|
||||
</div>
|
||||
<div class="metric-detailed">
|
||||
<span class="metric-label">Max Drawdown</span>
|
||||
<span class="metric-value negative">${m.maxDrawdown.toFixed(2)}%</span>
|
||||
</div>
|
||||
<div class="metric-detailed">
|
||||
<span class="metric-label">Total Profit</span>
|
||||
<span class="metric-value ${m.totalProfit >= 0 ? 'positive' : 'negative'}">$${m.totalProfit.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="metric-detailed">
|
||||
<span class="metric-label">Avg Win</span>
|
||||
<span class="metric-value positive">$${m.avgWin.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="metric-detailed">
|
||||
<span class="metric-label">Avg Loss</span>
|
||||
<span class="metric-value negative">$${Math.abs(m.avgLoss).toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="metric-detailed">
|
||||
<span class="metric-label">Largest Win</span>
|
||||
<span class="metric-value positive">$${m.largestWin.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="metric-detailed">
|
||||
<span class="metric-label">Largest Loss</span>
|
||||
<span class="metric-value negative">$${Math.abs(m.largestLoss).toFixed(2)}</span>
|
||||
</div>
|
||||
${m.bestHour && m.worstHour ? `
|
||||
<div class="metric-detailed" style="grid-column: 1 / -1; margin-top: 10px; padding: 10px; background: rgba(66, 153, 225, 0.1); border-radius: 6px;">
|
||||
<span class="metric-label">Best Hour:</span>
|
||||
<span class="metric-value positive">${String(m.bestHour.hour).padStart(2, '0')}:00 ($${m.bestHour.profit.toFixed(2)})</span>
|
||||
<span style="margin: 0 15px;">|</span>
|
||||
<span class="metric-label">Worst Hour:</span>
|
||||
<span class="metric-value negative">${String(m.worstHour.hour).padStart(2, '0')}:00 ($${m.worstHour.profit.toFixed(2)})</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="chart-section">
|
||||
<h3>Equity Curve</h3>
|
||||
<canvas id="equity-chart"></canvas>
|
||||
</div>
|
||||
|
||||
${m.hourlyStats ? `
|
||||
<div class="chart-section">
|
||||
<h3>Hourly Performance (24h)</h3>
|
||||
<canvas id="hourly-chart"></canvas>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="rules-section">
|
||||
<h3>Strategy Rules</h3>
|
||||
<p style="margin-bottom: 10px; color: #a0aec0;">BUY when ALL of the following conditions are true:</p>
|
||||
<div class="rules-list">
|
||||
${strategy.rules.map((rule, i) => {
|
||||
let ruleText = '';
|
||||
if (rule.type === 'simple') {
|
||||
ruleText = `${rule.left.price.toUpperCase()}[${rule.left.shift}] ${rule.operator} ${rule.right.price.toUpperCase()}[${rule.right.shift}]`;
|
||||
} else {
|
||||
ruleText = `(${rule.left.price1.toUpperCase()}[${rule.left.shift1}] ${rule.left.op} ${rule.left.price2.toUpperCase()}[${rule.left.shift2}]) ${rule.operator} (${rule.right.price.toUpperCase()}[${rule.right.shift}] × ${rule.right.multiplier})`;
|
||||
}
|
||||
return `<div class="rule-item">${i + 1}. ${ruleText}</div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
|
||||
<p style="margin: 20px 0 10px 0; color: #a0aec0;">SELL when ALL of the following conditions are true:</p>
|
||||
<div class="rules-list">
|
||||
${strategy.rules.map((rule, i) => {
|
||||
let ruleText = '';
|
||||
// Invert the operator for SELL rules
|
||||
const invertedOp = rule.operator === '>' ? '<=' :
|
||||
rule.operator === '<' ? '>=' :
|
||||
rule.operator === '>=' ? '<' :
|
||||
rule.operator === '<=' ? '>' : rule.operator;
|
||||
|
||||
if (rule.type === 'simple') {
|
||||
ruleText = `${rule.left.price.toUpperCase()}[${rule.left.shift}] ${invertedOp} ${rule.right.price.toUpperCase()}[${rule.right.shift}]`;
|
||||
} else {
|
||||
ruleText = `(${rule.left.price1.toUpperCase()}[${rule.left.shift1}] ${rule.left.op} ${rule.left.price2.toUpperCase()}[${rule.left.shift2}]) ${invertedOp} (${rule.right.price.toUpperCase()}[${rule.right.shift}] × ${rule.right.multiplier})`;
|
||||
}
|
||||
return `<div class="rule-item">${i + 1}. ${ruleText}</div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
<div class="parameters-box">
|
||||
<strong>Parameters:</strong> ATR Period: ${strategy.atrPeriod} | SL Multiplier: ${strategy.slMultiplier.toFixed(2)} | TP Multiplier: ${strategy.tpMultiplier.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trades-section">
|
||||
<h3>Trade Statement</h3>
|
||||
<div class="trades-table-container">
|
||||
<table class="trades-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Type</th>
|
||||
<th>Entry</th>
|
||||
<th>Exit</th>
|
||||
<th>Profit</th>
|
||||
<th>Reason</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${(m.trades && m.trades.length > 0) ? m.trades.map((t, i) => `
|
||||
<tr>
|
||||
<td>${i + 1}</td>
|
||||
<td><span class="badge">${t.type}</span></td>
|
||||
<td>${t.entry.toFixed(5)}</td>
|
||||
<td>${t.exit.toFixed(5)}</td>
|
||||
<td class="${t.profit >= 0 ? 'positive' : 'negative'}">$${t.profit.toFixed(2)}</td>
|
||||
<td>${t.reason.replace('_', ' ')}</td>
|
||||
</tr>
|
||||
`).join('') : '<tr><td colspan="6" style="text-align: center; color: #a0aec0;">No trades recorded</td></tr>'}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer-actions">
|
||||
<button class="btn-primary" onclick="downloadStrategy(${index}, 'mq4')">Download MQ4</button>
|
||||
<button class="btn-primary" onclick="downloadStrategy(${index}, 'mq5')">Download MQ5</button>
|
||||
<button class="btn-primary" onclick="downloadStrategy(${index}, 'ctrader')">cTrader</button>
|
||||
<button class="btn-primary" onclick="downloadStrategy(${index}, 'pine')">Pine Script</button>
|
||||
<button class="btn-success" onclick="downloadStrategy(${index}, 'report')">HTML Report</button>
|
||||
<button class="btn-secondary" onclick="downloadStrategy(${index}, 'json')">JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.style.display = 'flex';
|
||||
|
||||
// Draw equity chart
|
||||
setTimeout(() => {
|
||||
const ctx = document.getElementById('equity-chart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: m.equity.map((_, i) => i),
|
||||
datasets: [{
|
||||
label: 'Account Balance',
|
||||
data: m.equity,
|
||||
borderColor: '#667eea',
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: '#1a1f3a',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#a0aec0',
|
||||
borderColor: '#2d3748',
|
||||
borderWidth: 1
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
grid: { color: '#2d3748' },
|
||||
ticks: { color: '#a0aec0' }
|
||||
},
|
||||
x: {
|
||||
grid: { color: '#2d3748' },
|
||||
ticks: { color: '#a0aec0' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Render hourly performance chart if data exists
|
||||
if (m.hourlyStats) {
|
||||
const hourlyCanvas = document.getElementById('hourly-chart');
|
||||
if (hourlyCanvas) {
|
||||
new Chart(hourlyCanvas, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, '0')}:00`),
|
||||
datasets: [{
|
||||
label: 'Profit/Loss',
|
||||
data: m.hourlyStats.map(h => h.profit),
|
||||
backgroundColor: m.hourlyStats.map(h => h.profit >= 0 ? 'rgba(72, 187, 120, 0.6)' : 'rgba(245, 101, 101, 0.6)'),
|
||||
borderColor: m.hourlyStats.map(h => h.profit >= 0 ? 'rgba(72, 187, 120, 1)' : 'rgba(245, 101, 101, 1)'),
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
const hour = context.dataIndex;
|
||||
const stats = m.hourlyStats[hour];
|
||||
return [
|
||||
`Profit: $${stats.profit.toFixed(2)}`,
|
||||
`Trades: ${stats.trades}`,
|
||||
`Wins: ${stats.wins} | Losses: ${stats.losses}`
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||
ticks: { color: '#a0aec0' }
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: '#a0aec0', maxRotation: 45, minRotation: 45 }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close strategy modal
|
||||
*/
|
||||
function closeStrategyModal() {
|
||||
const modal = document.getElementById('strategy-modal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.onclick = function (event) {
|
||||
const modal = document.getElementById('strategy-modal');
|
||||
if (event.target === modal) {
|
||||
closeStrategyModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly expose functions to global scope for inline onclick handlers
|
||||
window.viewStrategyDetails = viewStrategyDetails;
|
||||
window.closeStrategyModal = closeStrategyModal;
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Strategy Class - Represents a trading strategy with rules and parameters
|
||||
*/
|
||||
class Strategy {
|
||||
constructor() {
|
||||
this.rules = [];
|
||||
this.atrPeriod = 20;
|
||||
this.slMultiplier = 2.0;
|
||||
this.tpMultiplier = 3.0;
|
||||
this.closeAtOpposite = false; // Disable to allow independent BUY/SELL signals
|
||||
this.fitness = 0;
|
||||
this.metrics = {};
|
||||
this.id = this.generateId();
|
||||
}
|
||||
|
||||
generateId() {
|
||||
return 'strategy_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random trading rules
|
||||
*/
|
||||
generateRandomRules(count, shiftRange) {
|
||||
const minRules = count[0] || 3;
|
||||
const maxRules = count[1] || 8;
|
||||
const numRules = Math.floor(Math.random() * (maxRules - minRules + 1)) + minRules;
|
||||
|
||||
for (let i = 0; i < numRules; i++) {
|
||||
this.rules.push(this.generateRule(shiftRange));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single random rule
|
||||
*/
|
||||
generateRule(shiftRange) {
|
||||
const priceTypes = ['open', 'high', 'low', 'close'];
|
||||
const operators = ['>', '<', '>=', '<='];
|
||||
|
||||
const minShift = shiftRange[0] || 1;
|
||||
const maxShift = shiftRange[1] || 10;
|
||||
|
||||
// 70% simple rules, 30% arithmetic rules
|
||||
if (Math.random() < 0.7) {
|
||||
// Simple rule: CLOSE[1] > OPEN[2]
|
||||
return {
|
||||
type: 'simple',
|
||||
left: {
|
||||
price: priceTypes[Math.floor(Math.random() * 4)],
|
||||
shift: Math.floor(Math.random() * (maxShift - minShift + 1)) + minShift
|
||||
},
|
||||
operator: operators[Math.floor(Math.random() * 4)],
|
||||
right: {
|
||||
price: priceTypes[Math.floor(Math.random() * 4)],
|
||||
shift: Math.floor(Math.random() * (maxShift - minShift + 1)) + minShift
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Arithmetic rule: (HIGH[1] + LOW[1]) > (CLOSE[2] * 1.01)
|
||||
const arithmeticOps = ['+', '-', '*'];
|
||||
const multipliers = [0.99, 1.01, 1.02, 0.98];
|
||||
|
||||
return {
|
||||
type: 'arithmetic',
|
||||
left: {
|
||||
price1: priceTypes[Math.floor(Math.random() * 4)],
|
||||
shift1: Math.floor(Math.random() * (maxShift - minShift + 1)) + minShift,
|
||||
op: arithmeticOps[Math.floor(Math.random() * 3)],
|
||||
price2: priceTypes[Math.floor(Math.random() * 4)],
|
||||
shift2: Math.floor(Math.random() * (maxShift - minShift + 1)) + minShift
|
||||
},
|
||||
operator: operators[Math.floor(Math.random() * 4)],
|
||||
right: {
|
||||
price: priceTypes[Math.floor(Math.random() * 4)],
|
||||
shift: Math.floor(Math.random() * (maxShift - minShift + 1)) + minShift,
|
||||
multiplier: multipliers[Math.floor(Math.random() * 4)]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Randomize ATR and SL/TP parameters
|
||||
*/
|
||||
randomizeParameters() {
|
||||
this.atrPeriod = Math.floor(Math.random() * 31) + 10; // 10-40
|
||||
this.slMultiplier = (Math.random() * 3) + 1; // 1.0-4.0
|
||||
this.tpMultiplier = this.slMultiplier + (Math.random() * 5) + 0.5; // SL + 0.5 to 5.5
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a deep copy of the strategy
|
||||
*/
|
||||
copy() {
|
||||
const newStrategy = new Strategy();
|
||||
newStrategy.rules = JSON.parse(JSON.stringify(this.rules));
|
||||
newStrategy.atrPeriod = this.atrPeriod;
|
||||
newStrategy.slMultiplier = this.slMultiplier;
|
||||
newStrategy.tpMultiplier = this.tpMultiplier;
|
||||
newStrategy.closeAtOpposite = this.closeAtOpposite;
|
||||
newStrategy.metrics = this.metrics ? JSON.parse(JSON.stringify(this.metrics)) : {};
|
||||
newStrategy.fitness = this.fitness || 0;
|
||||
return newStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert rules to human-readable format
|
||||
*/
|
||||
getRulesText() {
|
||||
return this.rules.map((rule, index) => {
|
||||
if (rule.type === 'simple') {
|
||||
return `${index + 1}. ${rule.left.price.toUpperCase()}[${rule.left.shift}] ${rule.operator} ${rule.right.price.toUpperCase()}[${rule.right.shift}]`;
|
||||
} else {
|
||||
return `${index + 1}. (${rule.left.price1.toUpperCase()}[${rule.left.shift1}] ${rule.left.op} ${rule.left.price2.toUpperCase()}[${rule.left.shift2}]) ${rule.operator} (${rule.right.price.toUpperCase()}[${rule.right.shift}] * ${rule.right.multiplier})`;
|
||||
}
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Export to JSON
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
rules: this.rules,
|
||||
parameters: {
|
||||
atr_period: this.atrPeriod,
|
||||
sl_multiplier: this.slMultiplier,
|
||||
tp_multiplier: this.tpMultiplier,
|
||||
close_at_opposite: this.closeAtOpposite
|
||||
},
|
||||
performance: this.metrics,
|
||||
fitness: this.fitness
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Import from JSON
|
||||
*/
|
||||
static fromJSON(json) {
|
||||
const strategy = new Strategy();
|
||||
strategy.id = json.id;
|
||||
strategy.rules = json.rules;
|
||||
strategy.atrPeriod = json.parameters.atr_period;
|
||||
strategy.slMultiplier = json.parameters.sl_multiplier;
|
||||
strategy.tpMultiplier = json.parameters.tp_multiplier;
|
||||
strategy.closeAtOpposite = json.parameters.close_at_opposite || false;
|
||||
strategy.metrics = json.performance || {};
|
||||
strategy.fitness = json.fitness || 0;
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// UI Controller - Simple helper functions for UI updates
|
||||
// (Most UI logic is in main.js)
|
||||
Reference in New Issue
Block a user