
时间序列挖掘的数据标签(第1部分):通过EA操作图制作具有趋势标记的数据集
汇总
当我们设计人工智能模型时,我们通常需要首先准备数据。良好的数据质量将使我们在模型训练和验证方面事半功倍。但我们的外汇或股票数据是特殊的,其中包含复杂的市场信息和时间信息,数据标注很困难,但我们可以很容易地在图表上分析历史数据的趋势。
本节介绍了一种通过EA操作图制作带有趋势标记的数据集的方法,您可以根据自己的想法直观地操作数据,当然您也可以使用相同的方法来扩展和自定义自己的数据集!
目录:
定义标签数据格式
当我们从客户那里获得外汇或股票数据时(本文不讨论从文件中读取或从其他网站下载的外部数据),一般情况是:
Time(时间) | Open(开盘价) | High(最高价) | Low(最低价) | Close(收盘价) | Tick_volume(分时交易量) |
---|---|---|---|---|---|
2021-12-10 01:15:00 | 1775.94 | 1775.96 | 1775.58 | 1775.58 | 173 |
2021-12-10 01:30:00 | 1775.58 | 1776.11 | 1775.48 | 1775.88 | 210 |
2021-12-10 01:45:00 | 1775.88 | 1776.22 | 1775.68 | 1776.22 | 212 |
2021-12-10 02:00:00 | 1776.22 | 1777.57 | 1775.98 | 1777.02 | 392 |
2021-12-10 02:15:00 | 1776.99 | 1777.72 | 1776.89 | 1777.72 | 264 |
以上是5个时间序列数据的样子。它们的收盘价(Close)和开盘价(Open)自始至终都是相互联系的,连贯性很强。假设我们认为前两个是上升趋势,其他是下降趋势(以上5个数据为例)。一般标记方法将数据分为两部分:
Time(时间) | Open(开盘价) | High(最高价) | Low(最低价) | Close(收盘价) | Tick_volume(分时交易量) |
---|---|---|---|---|---|
2021-12-10 01:15:00 | 1775.94 | 1775.96 | 1775.58 | 1775.58 | 173 |
2021-12-10 01:30:00 | 1775.58 | 1776.11 | 1775.48 | 1775.88 | 210 |
Time(时间) | Open(开盘价) | High(最高价) | Low(最低价) | Close(收盘价) | Tick_volume(分时交易量) |
---|---|---|---|---|---|
2021-12-10 01:45:00 | 1775.88 | 1776.22 | 1775.68 | 1776.22 | 212 |
2021-12-10 02:00:00 | 1776.22 | 1777.57 | 1775.98 | 1777.02 | 392 |
2021-12-10 02:15:00 | 1776.99 | 1777.72 | 1776.89 | 1777.72 | 264 |
然后告诉我们的模型哪个部分是上升趋势,哪个部分是下降趋势,但这忽略了它们的整体属性,会破坏数据的完整性,那么我们如何解决这个问题呢?
一种可行的方法是在我们的时间序列中添加趋势分组,如下所示(以上述5条数据为例,或遵循上述假设):
Time(时间) | Open(开盘价) | High(最高价) | Low(最低价) | Close(收盘价) | Tick_volume(分时交易量) | Trend_group(趋势组) |
---|---|---|---|---|---|---|
2021-12-10 01:15:00 | 1775.94 | 1775.96 | 1775.58 | 1775.58 | 173 | 0 |
2021-12-10 01:30:00 | 1775.58 | 1776.11 | 1775.48 | 1775.88 | 210 | 0 |
2021-12-10 01:45:00 | 1775.88 | 1776.22 | 1775.68 | 1776.22 | 212 | 1 |
2021-12-10 02:00:00 | 1776.22 | 1777.57 | 1775.98 | 1777.02 | 392 | 1 |
2021-12-10 02:15:00 | 1776.99 | 1777.72 | 1776.89 | 1777.72 | 264 | 1 |
但如果我们想在模型中进行趋势发展分析,比如当前趋势发展到什么程度(例如波动理论告诉我们,一般趋势一般包括趋势阶段和调整阶段,趋势阶段有5个波动阶段,调整阶段有3个波动调整等),我们需要进一步标注数据,我们可以通过添加另一个索引列来实现这一点,该列表示数据中趋势的发展(假设以下10个数据中的前2个数据为上升趋势,后5个数据为上涨趋势,中间的其余数据为下降趋势),如下所示:
Time(时间) | Open(开盘价) | High(最高价) | Low(最低价) | Close(收盘价) | Tick_volume(分时交易量) | Trend_group(趋势组) | Trend_index(趋势索引) |
---|---|---|---|---|---|---|---|
2021-12-10 03:15:00 | 1776.38 | 1777.94 | 1775.47 | 1777.71 | 565 | 0 | 0 |
2021-12-10 03:30:00 | 1777.75 | 1778.93 | 1777.68 | 1778.61 | 406 | 0 | 1 |
2021-12-10 03:45:00 | 1778.58 | 1778.78 | 1777.65 | 1778.16 | 388 | 1 | 0 |
2021-12-10 04:00:00 | 1778.14 | 1779.42 | 1778.06 | 1779.14 | 393 | 1 | 1 |
2021-12-10 04:15:00 | 1779.16 | 1779.49 | 1778.42 | 1779.31 | 451 | 1 | 2 |
2021-12-10 04:30:00 | 1779.22 | 1779.42 | 1778.36 | 1778.37 | 306 | 0 | 0 |
2021-12-10 04:45:00 | 1778.42 | 1778.51 | 1777.60 | 1777.78 | 411 | 0 | 1 |
2021-12-10 05:00:00 | 1777.81 | 1778.68 | 1777.61 | 1778.57 | 372 | 0 | 2 |
2021-12-10 05:15:00 | 1778.54 | 1779.29 | 1778.42 | 1779.02 | 413 | 0 | 3 |
2021-12-10 05:30:00 | 1778.97 | 1779.49 | 1778.48 | 1778.50 | 278 | 0 | 4 |
注意:
1. 定义上升趋势的Trend_group为0
2. 定义下降趋势的Trend_group为1
接下来,我们将开始在客户端操作图表,根据我们想要的模式标记数据。
初始化图表和文件
因为我们需要看图表来标记数据,所以图表不能随意滚动,必须根据我们的手动操作进行滚动,因此我们需要禁用 CHART_AUTOSCROLL 和 CHART_SHIFT:
ChartSetInteger (0, CHART_AUTOSCROLL, false); ChartSetInteger (0, CHART_SHIFT, true); ChartSetInteger (0, CHART_MOUSE_SCROLL ,1);注意:代码的绿色部分旨在允许我们使用鼠标滚轮控制图表
文件的初始化应首先检查是否存在现有标签文件,如果存在历史文件,则将文件名保存到变量“reName”中:
do { //---Find if there are files that match the chart if (StringFind(name, Symbol())!=-1 && StringFind(name,".csv")!=-1) reName=name; } while (FileFindNext(hd,name));注意:这里应该注意的是,我们使用的是“do-while”循环,它与“while”循环的不同之处在于,它首先执行运算符,然后计算表达式。但是名称的初始化是个问题,我们可以这样做
int hd= FileFindFirst("*",name,0);
如果有一个原始的标记文件,打开该文件并使用read_csv()函数获取标记的最后时间:
read_csv(file_handle,a);然后将图表滚动到最后标记的时间:
shift = - iBarShift(Symbol(),PERIOD_CURRENT,(datetime)a[i-8]); ChartNavigate(0, CHART_END ,shift);
如果没有历史记录文件,就创建一个文件:
file_handle = FileOpen(StringFormat("%s%d-%d.csv",Symbol(),Period(),start_t), FILE_WRITE | FILE_CSV | FILE_READ);然后将图表滚动到全局变量“start_t”指定的位置
shift = -iBarShift(Symbol(),PERIOD_CURRENT,(datetime)start_t); ChartNavigate(0,CHART_END,shift);添加一条垂直红线以标记起始列:
ObjectCreate (0,"Start",OBJ_VLINE,0,(datetime)start_t,0)这部分的逻辑是这样组织的:
if (FileIsExist(reName)) { file_handle = FileOpen(reName, FILE_WRITE | FILE_CSV | FILE_READ ); string a[]; int i= 0 ; read_csv(file_handle,a); i = ArraySize (a); shift = -iBarShift(Symbol(), PERIOD_CURRENT,(datetime)a[i-8]); ChartNavigate(0,CHART_END,shift); } else { file_handle = FileOpen (StringFormat ("%s%d-%d.csv", Symbol(), Period(),start_t), FILE_WRITE | FILE_CSV | FILE_READ ); Print ("There is no history file,create file:" , StringFormat ( "%s%d-%d",Symbol(), Period(),start_t)); shift = - iBarShift (Symbol(), PERIOD_CURRENT ,(datetime)start_t); ChartNavigate (0, CHART_END ,shift); ObjectCreate (0,"Start", OBJ_VLINE,0,(datetime)start_t,0); }注意:由于我们想将图表向左移动,我们必须在“iBarShift()”函数之前添加“-”
shift = -iBarShift(Symbol(), PERIOD_CURRENT ,(datetime)start_t);当然,它也可以在ChartNavigate()函数中实现,例如:
ChartNavigate(0,CHART_END,-shift);本文中的代码仍然是根据第一种方法实现的。
int OnInit() { //---initial string name; string reName="1"; int hd=FileFindFirst("*",name,0); int shift; ChartSetInteger(0,CHART_AUTOSCROLL,false); ChartSetInteger(0,CHART_SHIFT,false); ChartSetInteger(0,CHART_MOUSE_SCROLL,1); do { //---check File if(StringFind(name,Symbol())!=-1 && StringFind(name,".csv")!=-1) reName=name; } while(FileFindNext(hd,name)); if(FileIsExist(reName)) { file_handle = FileOpen(reName,FILE_WRITE|FILE_CSV|FILE_READ); string a[]; int i=0; read_csv(file_handle,a); i = ArraySize(a); shift = -iBarShift(Symbol(),PERIOD_CURRENT,(datetime)a[i-8]); ChartNavigate(0,CHART_END,shift); } else { file_handle = FileOpen(StringFormat("%s%d-%d.csv",Symbol(),Period(),start_t),FILE_WRITE|FILE_CSV|FILE_READ); Print(FileTell(file_handle)); Print("No history file,create file:",StringFormat("%s%d-%d",Symbol(),Period(),start_t)); shift = -iBarShift(Symbol(),PERIOD_CURRENT,(datetime)start_t); ChartNavigate(0,CHART_END,shift); ObjectCreate(0,"Start",OBJ_VLINE,0,(datetime)start_t,0); } return(INIT_SUCCEEDED); }
注意:
1. start_t变量-指定开始的时间范围;
2. shift 变量-指定要移位的列数,代码示例通过转换指定的时间显示要移位的列数;
3. read_csv()函数将在稍后定义。
void read_csv(int hd, string &arry[]) { int i= 0; while(!FileIsEnding(hd)) { ArrayResize(arry,i+1); arry[i]= FileReadString(hd); i++; } }
注意:我们使用“while”循环来查找历史注释文件的结束行,获取文件中的最后一行数据,并查找我们最后一次注释的结束时间。此注释将图表滚动到此柱状图,以便我们可以从此处继续进行注释。
设计和标记操作逻辑
- Home —移动到图表的最后一个柱;
- End — 移动到图表的第一个柱;
- Page Up — 将图表向后移动一个窗口的距离;
- Page Down — 将图表向前移动一个窗口的距离;
- Ctrl+I — 打开一个包含指标列表的窗口;
- Ctrl+B — 打开一个包含对象列表的窗口;
- Alt+1—图表显示为一系列柱形;
- Alt+2 — 该图表被显示为日式蜡烛的序列;
- Alt+3— 图表显示为连接收盘价格的线;
- Ctrl+G — 在图表窗口中显示/隐藏网格;
- "+"— 放大图表;
- "-" - 缩小图表;
- F12 — 逐步滚动图表(逐条滚动);
- F8 — 打开属性窗口;
- Backspace — 从图表中删除最后添加的对象;
- Delete — 删除所有选定的对象;
- Ctrl+Z — 取消删除最后一个对象。
#define KEY_B 66 #define KEY_S 83

2) 按's'标记上升趋势结束,“typ”变量仍然为0,“tp”变量设置为“end”,箭头颜色仍然为“clrBlue”,标签计数“Num”保持不变。需要注意的是,我们只需要在数据段的开头增加变量,而‘first’的反转用于指定再次按下按钮将执行标记数据段的“start”部分。

3) 执行switch语句后,调用函数ChartRedraw()来重新绘制图表。
if(id==CHARTEVENT_KEYDOWN) { switch(lparam) { case KEY_B: if(first) { col=clrBlue ; typ =0; Num+=1; tp = "start"; } else { col=clrRed ; typ = 1; tp = "end"; } ob =OBJ_ARROW_BUY; first = !first; Name = StringFormat("%d-%d-%s",typ,Num,tp); break; case KEY_S: if(first) { col=clrRed ; typ =1; Num+=1; tp = "start"; } else { col=clrBlue ; typ = 0; tp = "end"; } ob =OBJ_ARROW_SELL; first = !first; Name = StringFormat("%d-%d-%s",typ,Num,tp); break; default: Print("You pressed:"+lparam+" key, do nothing!"); } ChartRedraw(0); }
注意:
1. “typ”变量 - 0表示上升趋势,1表示下降趋势;
2. “Num”变量 —标记计数,会直观地显示在图表上;
3. “first”变量 - 控制我们的标签总是成对的,确保每个组都是“b”和“s”或“s”和“b”,而不会混淆;
4. “tp”变量 - 用于确定数据段的开始或结束。
2. 在图表上单击鼠标左键以确定标记的位置
if(id==CHARTEVENT_CLICK) { //--- definition int x=(int)lparam; int y=(int)dparam; datetime dt =0; double price =0; int window=0; if(ChartXYToTimePrice(0,x,y,window,dt,price)) { ObjectCreate(0,Name,ob,window,dt,price); ObjectSetInteger(0,Name,OBJPROP_COLOR,col); //Print("time:",dt,"shift:",iBarShift(Symbol(),PERIOD_CURRENT,dt)); if(tp=="start") Start=dt; else { if(file_handle) file_write(Start,dt); } ChartRedraw(0); } else Print("ChartXYToTimePrice return error code: ",GetLastError()); } //--- object delete if(id==CHARTEVENT_OBJECT_DELETE) { Print("The object with name ",sparam," has been deleted"); } //--- object create if(id==CHARTEVENT_OBJECT_CREATE) { Print("The object with name ",sparam," has been created!"); }
注意:
1. ChartXYToTimePrice()函数主要用于获取我们鼠标点击位置的柱的属性,包括当前时间和价格。我们使用全局变量“dt”来取得当前时间;
2. 当我们点击鼠标时,我们还需要判断当前动作是数据段的开始还是结束。我们使用全局变量“tp”来判断。
3. 具体操作流程
如果要标记上升趋势,请先按“b”键,在图表上开始标记的列上单击鼠标左键,然后按“s”键,然后在图标上的列末尾单击鼠标左按钮,完成标记。图表上显示成对的蓝色箭头,如下图所示:

如果要标记下降趋势,请先按‘s’键,在图表上开始标记的列上单击鼠标左键,然后按‘b’键,然后在图表上的列末尾单击鼠标左按钮。标记完成后,将显示成对的红色箭头,如下图所示:

贴标输出栏会随时显示贴标动作,非常直观地监控贴标过程,如图所示:
注意:这个部分实际上可以更好地优化,比如增加撤销最后一个动作的功能,然后你可以随时调整标记的位置,也可以避免错误的操作,但我是个懒人,所以…(^o^)
组织数据并写入文件
datetime Start; MqlRates rates[]; ArraySetAsSeries(rates, false);
if(id==CHARTEVENT_CLICK) { //--- definition int x=(int)lparam; int y=(int)dparam; datetime dt =0; double price =0; int window=0; if(ChartXYToTimePrice(0,x,y,window,dt,price)) { ObjectCreate(0,Name,ob,window,dt,price); ObjectSetInteger(0,Name,OBJPROP_COLOR,col); //Print("time:",dt,"shift:",iBarShift(Symbol(),PERIOD_CURRENT,dt)); if(tp=="start") Start=dt; else { if(file_handle) file_write(Start,dt); } ChartRedraw(0); } else Print("ChartXYToTimePrice return error code: ",GetLastError()); }
void file_write(datetime start, datetime end) { MqlRates rates[]; ArraySetAsSeries(rates,false); int n_cp=CopyRates(Symbol(),PERIOD_CURRENT,start,end,rates); if(n_cp>0) { if(FileTell(file_handle)==2) { FileWrite(file_handle,"time","open","high","low","close","tick_volume","trend_group","trend_index"); for(int i=0; i<n_cp; i++) { FileWrite(file_handle, rates[i].time, rates[i].open, rates[i].high, rates[i].low, rates[i].close, rates[i].tick_volume, typ, i); } } else { for(int i=0; i<n_cp; i++) { FileWrite(file_handle, rates[i].time, rates[i].open, rates[i].high, rates[i].low, rates[i].close, rates[i].tick_volume, typ, i); } } } else Print("No data copied!"); FileFlush(file_handle); typ=3; }
注意:
1. 第一次写入文件时,我们需要写入索引头;
2. Trend_group实际上是全局变量“typ”;
3. 我们没有在此函数中调用FileClose()函数,因为我们的标记尚未完成。我们将在OnDeinit()函数中调用此函数,将最终结果写入文件。
4. 应特别注意此处使用的代码的黄色部分
if(FileTell(file_handle)==2)要确定文件中是否有数据(当然,也可以使用其他方法,例如在初始化过程中添加变量为其赋值),如果文件中没有数据,则需要添加这样的头:
FileWrite(file_handle,"time","open","high","low","close","tick_volume","trend_group","trend_index");如果文件中有数据,则无需添加标头,否则数据将被截断,这一点非常重要

让我们检查不同数据段之间的一致性,发现数据是完美的:

附件:完整的EA代码示例
1. 全局变量和常量的定义。参数“start_t”可以由1970年1月1日起的每秒数据定义。当然,它也可以由标准的“datetime”定义,也可以由输入变量“input int start_t=1403037112;”定义,以便在以后运行EA时随时更改:#define KEY_B 66 #define KEY_S 83 int Num= 0; int typ= 3; string Name; string tp; color col; bool first= true; ENUM_OBJECT ob; int file_handle=0; int start_t=1403037112; datetime Start;
注意:当然,您也可以根据个人喜好将按钮定义为输入变量。
input int KEY_B=66; input int KEY_S=83;
这样做的好处是,如果你觉得按钮不容易使用,你可以在每次执行EA时随意更改按钮,直到你满意为止,我们的代码暂时不会更改。
2. OnInit()函数,在这里我们初始化我们的准备工作:
int OnInit() { //---initial string name; string reName="1"; int hd=FileFindFirst("*",name,0); int shift; ChartSetInteger(0,CHART_AUTOSCROLL,false); ChartSetInteger(0,CHART_SHIFT,false); ChartSetInteger(0,CHART_MOUSE_SCROLL,1); do { //---check File if(StringFind(name,Symbol())!=-1 && StringFind(name,".csv")!=-1) reName=name; } while(FileFindNext(hd,name)); if(FileIsExist(reName)) { file_handle = FileOpen(reName,FILE_WRITE|FILE_CSV|FILE_READ); string a[]; int i=0; read_csv(file_handle,a); i = ArraySize(a); shift = -iBarShift(Symbol(),PERIOD_CURRENT,(datetime)a[i-8]); ChartNavigate(0,CHART_END,shift); } else { file_handle = FileOpen(StringFormat("%s%d-%d.csv",Symbol(),Period(),start_t),FILE_WRITE|FILE_CSV|FILE_READ); Print(FileTell(file_handle)); Print("No history file,create file:",StringFormat("%s%d-%d",Symbol(),Period(),start_t)); shift = -iBarShift(Symbol(),PERIOD_CURRENT,(datetime)start_t); ChartNavigate(0,CHART_END,shift); ObjectCreate(0,"Start",OBJ_VLINE,0,(datetime)start_t,0); } //--- Print("EA:",MQL5InfoString(MQL5_PROGRAM_NAME),"Working!"); //--- ChartSetInteger(ChartID(),CHART_EVENT_OBJECT_CREATE,true); //--- ChartSetInteger(ChartID(),CHART_EVENT_OBJECT_DELETE,true); //--- ChartRedraw(0); //--- return(INIT_SUCCEEDED); }
3. 因为我们所有的键盘和鼠标操作都是在图表上完成的,所以我们将主要的逻辑函数放入OnChartEvent()函数中,以实现:
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //Comment(__FUNCTION__,": id=",id," lparam=",lparam," dparam=",dparam," sparam=",sparam); if(id==CHARTEVENT_KEYDOWN) { switch(lparam) { case KEY_B: if(first) { col=clrBlue ; typ =0; Num+=1; tp = "start"; } else { col=clrRed ; typ = 1; tp = "end"; } ob =OBJ_ARROW_BUY; first = !first; Name = StringFormat("%d-%d-%s",typ,Num,tp); break; case KEY_S: if(first) { col=clrRed ; typ =1; Num+=1; tp = "start"; } else { col=clrBlue ; typ = 0; tp = "end"; } ob =OBJ_ARROW_SELL; first = !first; Name = StringFormat("%d-%d-%s",typ,Num,tp); break; default: Print("You pressed:"+lparam+" key, do nothing!"); } ChartRedraw(0); } //--- if(id==CHARTEVENT_CLICK&&(typ!=3)) { //--- definition int x=(int)lparam; int y=(int)dparam; datetime dt =0; double price =0; int window=0; if(ChartXYToTimePrice(0,x,y,window,dt,price)) { ObjectCreate(0,Name,ob,window,dt,price); ObjectSetInteger(0,Name,OBJPROP_COLOR,col); //Print("time:",dt,"shift:",iBarShift(Symbol(),PERIOD_CURRENT,dt)); if(tp=="start") Start=dt; else { if(file_handle) file_write(Start,dt); } ChartRedraw(0); } else Print("ChartXYToTimePrice return error code: ",GetLastError()); } //--- object delete if(id==CHARTEVENT_OBJECT_DELETE) { Print("The object with name ",sparam," has been deleted"); } //--- object create if(id==CHARTEVENT_OBJECT_CREATE) { Print("The object with name ",sparam," has been created!"); } }
注意:在实现此函数时,我们更改了上面的代码
if (id==CHARTEVENT_CLICK&&(typ!=3))
我们这样做的原因很简单,我们避免了意外点击鼠标引起的错误操作,并使用“typ”变量来控制鼠标操作是否有效。当我们标记一个趋势时,我们将执行file_write()函数。我们在该函数的末尾添加此行
typ=3;
然后你可以在开始下一段标记之前随意地用鼠标在图表上操作,不需要任何动作,直到你找到合适的位置并准备标记下一个趋势。
4. 写入数据函数 - file_write()的实现:
void file_write(datetime start, datetime end) { MqlRates rates[]; ArraySetAsSeries(rates,false); int n_cp=CopyRates(Symbol(),PERIOD_CURRENT,start,end,rates); if(n_cp>0) { if(FileTell(file_handle)==2) { FileWrite(file_handle,"time","open","high","low","close","tick_volume","trend_group","trend_index"); for(int i=0; i<n_cp; i++) { FileWrite(file_handle, rates[i].time, rates[i].open, rates[i].high, rates[i].low, rates[i].close, rates[i].tick_volume, typ, i); } } else { for(int i=0; i<n_cp; i++) { FileWrite(file_handle, rates[i].time, rates[i].open, rates[i].high, rates[i].low, rates[i].close, rates[i].tick_volume, typ, i); } } } else Print("No data copied!"); FileFlush(file_handle); typ=3; }
5. 读取文件函数 - read_csv()的实现:
void read_csv(int hd, string &arry[]) { int i=0; while(!FileIsEnding(hd)) { ArrayResize(arry,i+1); arry[i]=FileReadString(hd); i++; } }
6. 这里仍然有一个重要的问题没有得到解决,即初始化EA时打开的文件句柄“file_handle”没有被释放。我们在最后一个OnDeinit()函数中释放句柄。当调用函数“FileClose(file_handle)”时,所有数据实际上都会写入csv文件,因此在EA仍在运行时不要试图打开csv文件尤为重要:
void OnDeinit(const int reason) { FileClose(file_handle); Print("Write data!"); }
注意:本文中显示的代码仅用于演示。如果您想在实践中使用它,建议您进一步改进代码。在文章的最后,将提供演示中涉及的CSV文件和最终MQL5文件。本系列的下一篇文章将介绍如何通过与python结合的客户端对数据进行注释。
感谢您的耐心阅读,希望您有所收获,祝您生活愉快,下一章再见!
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/13225



