1 (2019-11-01 14:47:30 отредактировано toxa)

Тема: Получение данных из quik через "экспорт в системы тех.анализа..."

Это довольно прикольная фича, она позволяет получать "свечные" данные из квика. Экспорт идет через named pipes, обеспечивает приемлемую скорость и довольно прост. Что-то я не разобрался, можно ли тут аттачить архивы, но вот исходники двух примеров - один из них мониторит появляющиеся в системе квиковые пайпы, другой - дампит данные в консоль. Чтобы нужные пайпы появились, в квике нужно выбрать "экспорт в системы тех.анализа...", далее добавить вывод нужной бумаги с нужным таймфреймом в велслаб, потом запустить экспорт.

Оба проекта собираются fpc и работают, только для первого нужно взять модуль masks из дельфи или лазаруса, так как в дистрибутив fpc он не входит.

Монитор:

{$APPTYPE CONSOLE}
uses Windows, Classes, SysUtils, Masks;

const pipes_prefix = '\\.\pipe\';

type  tPipeList = class(tStringList)
      private
        FNotify : THandle;
        FMask   : string;
      public
        constructor Create;
        destructor  Destroy; override;
        procedure   RefreshDirectoryList;
        function    WaitForChanges(aTimeOut: longint): boolean;
        procedure   AfterAddObject(const aobject: string); virtual;
        procedure   BeforeDeleteObject(const aobject: string); virtual;
        property    Mask: string read FMask write FMask;
      end;

var   FPipes  : tPipeList;
      FExit   : boolean = false;

function CtrlHandler(CtrlType: Longint): bool; stdcall;
begin FExit:= true; result:= true; end;

{ tPipeList }

constructor tPipeList.Create;
begin
  inherited Create;
  Sorted:= true;
  FNotify:= FindFirstChangeNotification(pipes_prefix, false, FILE_NOTIFY_CHANGE_FILE_NAME);
end;

destructor tPipeList.Destroy;
begin
  if (FNotify <> INVALID_HANDLE_VALUE) then FindCloseChangeNotification(FNotify);
  inherited destroy;
end;

procedure tPipeList.RefreshDirectoryList;
const seq : longint = 0;
var   idx : longint;
      sr  : tSearchRec;
begin
  inc(seq);
  try
    if findfirst(format('%s*', [pipes_prefix]), faAnyFile, sr) = 0 then
      repeat
        if MatchesMask(sr.name, FMask) then begin
          idx:= IndexOf(sr.name);
          if (idx < 0) then begin
            AddObject(sr.name, pointer(seq));
            AfterAddObject(sr.name);
          end else Objects[idx]:= pointer(seq);
        end;
      until (findnext(sr) <> 0);
  finally findclose(sr); end;
  for idx:= count - 1 downto 0 do
    if (Objects[idx] <> pointer(seq)) then begin
      BeforeDeleteObject(Strings[idx]);
      delete(idx);
    end;
end;

function tPipeList.WaitForChanges(aTimeOut: longint): boolean;
begin
  result:= (FNotify <> INVALID_HANDLE_VALUE) and (WaitForSingleObject(FNotify, aTimeOut) = WAIT_OBJECT_0);
  if result then FindNextChangeNotification(FNotify);
end;

procedure tPipeList.AfterAddObject(const aobject: string);
begin
  writeln(formatdatetime('HH:NN:SS.ZZZ', now), ' ADD: ', aobject);
end;

procedure tPipeList.BeforeDeleteObject(const aobject: string);
begin
  writeln(formatdatetime('HH:NN:SS.ZZZ', now), ' DEL: ', aobject);
end;


begin
  SetConsoleCtrlHandler(@CtrlHandler, true);
  
  writeln('Start export to wealthlab from QUIK or press Ctrl-C to exit...');

  FPipes:= tPipeList.Create;
  FPipes.Mask:= 'QUIK_*';
  FPipes.RefreshDirectoryList;

  while not FExit do begin
    if FPipes.WaitForChanges(1000) then begin
      FPipes.RefreshDirectoryList;
    end;
  end;

  FreeAndNil(FPipes);
  readln;
end.

Дампер:

{$APPTYPE CONSOLE}
uses Windows, SysUtils;

var   fh      : longint;
      buf     : array[0..1024] of ansichar;

function SafeReadPipe(fh: longint; buf: pAnsiChar; toread: longint): boolean;
var idx  : longint;
    read : DWORD;
begin
  idx:= 0;
  if assigned(buf) then
    while (toread > 0) and ReadFile(fh, buf[idx], toread, read, nil) do begin
      dec(toread, read);
      inc(idx, read);
    end;
  result:= (toread = 0);
end;

function SafeReadHeader(fh: longint; var id: word): boolean;
var tmp: word;
begin
  if SafeReadPipe(fh, @tmp, 2) and (tmp = 1) then result:= SafeReadPipe(fh, @id, 2) else result:= false;
end;

var id, tmp : word;
    tmp_i   : longint;
    tmp_d   : double;
    tmp_i64 : int64;
    strbuf  : ansistring;
    st      : TSystemTime;
begin
  fillchar(buf, sizeof(buf), 0);

  strbuf:= format('\\.\pipe\QUIK_%s_%s', [paramstr(1), paramstr(2)]);

  if not fileexists(strbuf) then begin
    writeln('WARNING (pipe not found): ', strbuf);
    writeln('usage: logpipedata.exe <TICKER> <TIMEFRAME>'#$0d#$0a'example: logpipedata.exe LKOH TICKS'#$0d#$0a);
  end;

  WaitNamedPipe(PAnsiChar(strbuf), 1000);

  fh:= longint(CreateFile(PAnsiChar(strbuf),
                          GENERIC_READ,
                          FILE_SHARE_READ,
                          nil,
                          OPEN_EXISTING,
                          0,
                          0));

  if (fh <> -1) then begin

    while SafeReadPipe(fh, @id, 2) do begin
      case id of
        1       : SafeReadPipe(fh, @tmp, 2);
        2       : if SafeReadPipe(fh, @tmp, 2) then begin
                    setlength(strbuf, tmp);
                    SafeReadPipe(fh, @strbuf[1], tmp);
                    writeln('instrument: ', strbuf);
                  end;
        6, 7, 9 : SafeReadPipe(fh, @tmp_i, 4);
        8       : begin
                    SafeReadPipe(fh, @tmp_i64, 8);
                    FileTimeToSystemTime(tFileTime(tmp_i64), st);
                    write(formatdatetime('DD-MM-YYYY HH:NN:SS ', SystemTimeToDateTime(st)));
                    // open
                    SafeReadPipe(fh, @tmp_d, 8);
                    write(tmp_d:10:6, ' ');
                    // high
                    SafeReadPipe(fh, @tmp_d, 8);
                    write(tmp_d:10:6, ' ');
                    // low
                    SafeReadPipe(fh, @tmp_d, 8);
                    write(tmp_d:10:6, ' ');
                    // close
                    SafeReadPipe(fh, @tmp_d, 8);
                    write(tmp_d:10:6, ' ');
                    // volume
                    SafeReadPipe(fh, @tmp_d, 8);
                    writeln(tmp_d:10:6, ' ');
                  end;
      end;
    end;

    CloseHandle(fh);
  end else writeln('ERROR Opening: ', strbuf, ' error: ', GetLastError);
end.

2 (2019-11-01 14:17:15 отредактировано toxa)

Re: Получение данных из quik через "экспорт в системы тех.анализа..."

Z:\quik\wld_export>listquikpipes.exe
Start export to wealthlab from QUIK or press Ctrl-C to exit...
13:14:49.461 ADD: QUIK_RIZ9_1MINUTES
13:14:49.461 ADD: QUIK_RIZ9_1MINUTES_EXISTING_DATA
13:14:49.461 ADD: QUIK_MSNG_5MINUTES
13:14:49.461 ADD: QUIK_MSNG_5MINUTES_EXISTING_DATA

далее:
logpipedata.exe RIZ9 1MINUTES_EXISTING_DATA

или:
logpipedata.exe RIZ9 1MINUTES

тогда программа не завершится, а будет идти онлайн.

3

Re: Получение данных из quik через "экспорт в системы тех.анализа..."

Спасибо за такую шикарную информацию!

4

Re: Получение данных из quik через "экспорт в системы тех.анализа..."

да было б чо. как сюда файлы аттачить?

5

Re: Получение данных из quik через "экспорт в системы тех.анализа..."

у меня и разобранный протокол trans2quik есть, в принципе, но там надо из проекта выдирать, а написать доку я не удосужился. если есть запал ковыряться - могу помочь.

6

Re: Получение данных из quik через "экспорт в системы тех.анализа..."

toxa пишет:

да было б чо. как сюда файлы аттачить?

Просто выложите файлы куда-нибудь, на файлообменник и ссылку здесь оставьте, админ перенесёт их на местный сервер, чтобы не потерялось.

7 (2019-11-01 17:33:00 отредактировано toxa)

Re: Получение данных из quik через "экспорт в системы тех.анализа..."

для исследования trans2quik я наваял небольшой патч, который дампит всю работу с пайпами. вот, выложил на dropbox: [url]https://www.dropbox.com/s/crg8w3fp5y3m9q9/api_tester_patched.zip?dl=0[/url]

это стандартное квиковое приложение, просто я в их родной trans2quik.dll внедрил импорт своей dll, которая перехватывает нужные вызовы и пишет лог. соединяемся с квиком, делаем действия, потом анализируем лог-файл. к счастью, trans2quik.dll написан по рабоче-крестьянски, на каждое поле делается readfile() и в логе видны отдельные поля (поля переменной длины вычитываются в два вызова, первый readfile - это size, второй - содержимое).

общий смысл протокола таков: после начального обмена хендшейком с неким "magic number", обмен идет так:
<command_id><command_data>
где command_id - это int, а <command_data> это некий набор из int, double и <string>, где string - это <длина><chars[длина]>

команды такие:
0x02 - handshake result
0x0A - connection status
0x11 - заявка
0x12 - сделка
0x0B - orders request
0x0D - start orders
0x0E - trades request
0x10 - start trades
0x06 - transaction reply

ps: да, все это работает через два пайпа - один только на чтение, другой только на запись. программирование "от сохи", что называется.

8 (2019-11-01 18:37:57 отредактировано toxa)

Re: Получение данных из quik через "экспорт в системы тех.анализа..."

вот еще. функция для чтения структур такая:

function tQUIKTradePipeReader.ReadStructure(hPipe: THandle; atag: longint; adesc: pAnsiChar; abuf: pAnsiChar; abufsize: longint): boolean;
const errormsg : ansistring = 'error receiving struct, type "%s"';
var   len      : longint;
      tmpi     : longint;
      tmpi64   : int64;
      stop     : boolean;
  function getid(var desc: pAnsiChar): ansichar;
  begin result:= upcase(desc^); inc(desc); end;
  function getlen(var desc: pAnsiChar): longint;
  begin
    result:= 0;
    while (desc^ in ['0'..'9']) do begin
      result:= result * 10 + (byte(desc^) - byte('0'));
      inc(desc);
    end;
  end;
begin
  result:= false;
  if not ((atag = -1) or (ReadInt(hPipe) = atag)) then raise Exception.Create('struct tag missmatch');
  if assigned(adesc) then begin
    stop:= false;
    while (not stop) do begin
      case getid(adesc) of
        #0  : begin
                result:= (abufsize = 0);
                stop:= true;
              end;
        'I' : begin
                len:= getlen(adesc);
                if (abufsize < len) or not SafeReadPipe(hPipe, abuf, len) then raise Exception.CreateFmt(errormsg, ['I']);
                inc(abuf, len);
                dec(abufsize, len);
              end;
        'D' : begin
                if (abufsize < 8) or not SafeReadPipe(hPipe, abuf, sizeof(double)) then raise Exception.CreateFmt(errormsg, ['D']);
                inc(abuf, sizeof(double));
                dec(abufsize, sizeof(double));
              end;
        'S' : begin
                len:= getlen(adesc) + 1;
                if (abufsize < len) then raise Exception.CreateFmt(errormsg, ['S']);
                if not SafeReadPipe(hPipe, @tmpi, sizeof(tmpi)) then raise Exception.CreateFmt(errormsg, ['S']);
                if (tmpi >= 0) and (tmpi <= 255) then begin
                  byte(abuf^):= tmpi;
                  if (tmpi > 0) then
                    if not SafeReadPipe(hPipe, abuf + 1, tmpi) then raise Exception.CreateFmt(errormsg, ['S']);
                end else Exception.CreateFmt(errormsg, ['S']);
                inc(abuf, len);
                dec(abufsize, len);
              end;
        '-' : begin
                len:= getlen(adesc);
                while (len > 0) do begin
                  if not SafeReadPipe(hPipe, @tmpi64, min(len, sizeof(tmpi64))) then raise Exception.CreateFmt(errormsg, ['-']);
                  dec(len, sizeof(tmpi64));
                end;
              end;
        '+' : begin
                len:= getlen(adesc);
                if(abufsize < len) then raise Exception.CreateFmt(errormsg, ['+']);
                inc(abuf, len);
                dec(abufsize, len);
              end;
        '?' : begin
                if not SafeReadPipe(hPipe, @tmpi, sizeof(tmpi)) then raise Exception.CreateFmt(errormsg, ['?']);
                if (tmpi >= 0) then begin
                  while (tmpi > 0) do begin
                    if not SafeReadPipe(hPipe, @tmpi64, min(tmpi, sizeof(tmpi64))) then raise Exception.CreateFmt(errormsg, ['?']);
                    dec(tmpi, sizeof(tmpi64));
                  end;
                end else raise Exception.CreateFmt(errormsg, ['?']);
              end;
        else  raise Exception.CreateFmt(errormsg, ['unknown']);
      end;
    end;
  end;
end;

структуры и их маппинг на принимаемые данные:

type  tSecBoard            = string[8];
      tSecCode             = string[20];
      tCurrCode            = string[6];
      tAccount             = string[40];
      tBrokerSymbolName    = string[128];
      tQUIKShortName       = string[30];


const QUIKOrderDesc : pAnsiChar = 'I4S8S20DI4???S40DI4I4D-8-8I4S40-8?-4?-4-8-4-4-4I8I8';

type  pQUIKOrder    = ^tQUIKOrder;
      tQUIKOrder    = packed record
        mode        : longint;
        secboard    : tSecBoard;
        seccode     : tSecCode;
        orderno     : double;
        flags       : longint;  // Flags: buysell, status 1E = 00011110
        account     : tAccount;
        price       : double;
        quantity    : longint;
        balance     : longint;
        value       : double;
        trsid       : longint;
        clientcode  : tAccount;
        ordertime   : TFileTime;
        wdtime      : TFileTime;
      end;

const QUIKTradeDesc : pAnsiChar = 'I4S8S20DD???S40DI4D-8-8??I4-8-8S40-8-8-8-4-8-8-8-4-8-8-8-8I4?S6??-4I8-4';

type  pQUIKTrade    = ^tQUIKTrade;
      tQUIKTrade    = packed record
        mode        : longint;
        secboard    : tSecBoard;
        seccode     : tSecCode;
        tradeno     : double;
        orderno     : double;
        account     : tAccount;
        price       : double;
        quantity    : longint;
        value       : double;
        flags       : longint;
        clientcode  : tAccount;
        settledate  : longint;
        curcode     : tCurrCode;
        tradetime   : TFileTime;
      end;

const QUIKTrsReply  : pAnsiChar = 'I4I4I4I4DS255';

type  pQUIKTrsReply = ^tQUIKTrsReply;
      tQUIKTrsReply = packed record
        trs_result  : longint;
        error_code  : longint;
        internal_id : longint;
        reply_code  : longint;
        orderno     : double;
        errortext   : shortstring;
      end;

чтение из пайпа и tag:

заявка:
ReadStructure(hpipes[pipe_out], 100, QUIKOrderDesc, @order, sizeof(order));

сделка:
ReadStructure(hpipes[pipe_out], 200, QUIKTradeDesc, @trade, sizeof(trade));

transaction_result:
ReadStructure(hpipes[pipe_out], -1, QUIKTrsReply, @trsreply, sizeof(trsreply));

надеюсь, это не очень сложно для понимания. смысл в том, чтобы прочитать данные из пайпа в struct по описанию. "S" - это "паскалевские" короткие строки shortstring длиной до 255 символов, нулевой символ - длина. таким образом, sizeof(tSecBoard) == 9; чтение в нее определяется описанием "S8". при этом в пайпе ожидается <int len><[len bytes]>. если при чтении в такую строку считалось len > 8, то поднимаем exception.