Add files via upload

This commit is contained in:
FxPouya
2025-12-24 13:07:51 +03:30
committed by GitHub
commit ad50cc66cb
28 changed files with 22747 additions and 0 deletions
+464
View File
@@ -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;
}
}
+314
View File
@@ -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
View File
@@ -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));
}
}
+493
View File
@@ -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;
+162
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+328
View File
@@ -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;
+152
View File
@@ -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
`;
}
}
+341
View File
@@ -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;
+202
View File
@@ -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;
}
`;
}
}
+75
View File
@@ -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;
+272
View File
@@ -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>&copy; 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>`;
}
}
+59
View File
@@ -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;
+329
View File
@@ -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()">&times;</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
View File
@@ -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;
}
}
+2
View File
@@ -0,0 +1,2 @@
// UI Controller - Simple helper functions for UI updates
// (Most UI logic is in main.js)